예제 #1
0
class MDBottomSheet(ThemableBehavior, ModalView):
    background = f"{images_path}transparent.png"
    """Private attribute."""

    duration_opening = NumericProperty(0.15)
    """
    The duration of the bottom sheet dialog opening animation.

    :attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.15`.
    """

    duration_closing = NumericProperty(0.15)
    """
    The duration of the bottom sheet dialog closing animation.

    :attr:`duration_closing` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.15`.
    """

    radius = NumericProperty(25)
    """
    The value of the rounding of the corners of the dialog.

    :attr:`radius` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `25`.
    """

    radius_from = OptionProperty(
        None,
        options=[
            "top_left",
            "top_right",
            "top",
            "bottom_right",
            "bottom_left",
            "bottom",
        ],
        allownone=True,
    )
    """
    Sets which corners to cut from the dialog. Available options are:
    (`"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, `"bottom"`).

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-radius-from.png
        :align: center

    :attr:`radius_from` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    animation = BooleanProperty(False)
    """
    Whether to use animation for opening and closing of the bottomsheet or not.

    :attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    bg_color = ColorProperty(None)
    """
    Dialog background color in ``rgba`` format.

    :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[]`.
    """

    value_transparent = ColorProperty([0, 0, 0, 0.8])
    """
    Background transparency value when opening a dialog.

    :attr:`value_transparent` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0.8]`.
    """

    _upper_padding = ObjectProperty()
    _gl_content = ObjectProperty()
    _position_content = NumericProperty()

    def open(self, *args):
        super().open(*args)

    def add_widget(self, widget, index=0, canvas=None):
        super().add_widget(widget, index, canvas)

    def dismiss(self, *args, **kwargs):
        def dismiss(*args):
            self.dispatch("on_pre_dismiss")
            self._gl_content.clear_widgets()
            self._real_remove_widget()
            self.dispatch("on_dismiss")

        if self.animation:
            a = Animation(height=0, d=self.duration_closing)
            a.bind(on_complete=dismiss)
            a.start(self._gl_content)
        else:
            dismiss()

    def resize_content_layout(self, content, layout, interval=0):
        if not layout.ids.get("box_sheet_list"):
            _layout = layout
        else:
            _layout = layout.ids.box_sheet_list

        if _layout.height > Window.height / 2:
            height = Window.height / 2
        else:
            height = _layout.height

        if self.animation:
            Animation(height=height, d=self.duration_opening).start(_layout)
            Animation(height=height, d=self.duration_opening).start(content)
        else:
            layout.height = height
            content.height = height
예제 #2
0
class MDNavigationDrawer(MDCard):
    type = OptionProperty("modal", options=("standard", "modal"))
    """
    Type of drawer. Modal type will be on top of screen. Standard type will be
    at left or right of screen. Also it automatically disables
    :attr:`close_on_click` and :attr:`enable_swiping` to prevent closing
    drawer for standard type.

    :attr:`type` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `modal`.
    """

    anchor = OptionProperty("left", options=("left", "right"))
    """
    Anchoring screen edge for drawer. Set it to `'right'` for right-to-left
    languages. Available options are: `'left'`, `'right'`.

    :attr:`anchor` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `left`.
    """

    close_on_click = BooleanProperty(True)
    """
    Close when click on scrim or keyboard escape. It automatically sets to
    False for "standard" type.

    :attr:`close_on_click` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    state = OptionProperty("close", options=("close", "open"))
    """
    Indicates if panel closed or opened. Sets after :attr:`status` change.
    Available options are: `'close'`, `'open'`.

    :attr:`state` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'close'`.
    """

    status = OptionProperty(
        "closed",
        options=(
            "closed",
            "opening_with_swipe",
            "opening_with_animation",
            "opened",
            "closing_with_swipe",
            "closing_with_animation",
        ),
    )
    """
    Detailed state. Sets before :attr:`state`. Bind to :attr:`state` instead
    of :attr:`status`. Available options are: `'closed'`,
    `'opening_with_swipe'`, `'opening_with_animation'`, `'opened'`,
    `'closing_with_swipe'`, `'closing_with_animation'`.

    :attr:`status` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'closed'`.
    """

    open_progress = NumericProperty(0.0)
    """
    Percent of visible part of side panel. The percent is specified as a
    floating point number in the range 0-1. 0.0 if panel is closed and 1.0 if
    panel is opened.

    :attr:`open_progress` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.0`.
    """

    enable_swiping = BooleanProperty(True)
    """
    Allow to open or close navigation drawer with swipe. It automatically
    sets to False for "standard" type.

    :attr:`enable_swiping` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    swipe_distance = NumericProperty(10)
    """
    The distance of the swipe with which the movement of navigation drawer
    begins.

    :attr:`swipe_distance` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `10`.
    """

    swipe_edge_width = NumericProperty(20)
    """
    The size of the area in px inside which should start swipe to drag
    navigation drawer.

    :attr:`swipe_edge_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `20`.
    """

    scrim_color = ListProperty([0, 0, 0, 0.5])
    """
    Color for scrim. Alpha channel will be multiplied with
    :attr:`_scrim_alpha`. Set fourth channel to 0 if you want to disable
    scrim.

    :attr:`scrim_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0.5]`.
    """

    _radius = ListProperty([0, 0, 0, 0])

    def _get_scrim_alpha(self):
        _scrim_alpha = 0
        if self.type == "modal":
            _scrim_alpha = self._scrim_alpha_transition(self.open_progress)
        if (isinstance(self.parent, MDNavigationLayout)
                and self.parent._scrim_color):
            self.parent._scrim_color.rgba = self.scrim_color[:3] + [
                self.scrim_color[3] * _scrim_alpha
            ]
        return _scrim_alpha

    _scrim_alpha = AliasProperty(
        _get_scrim_alpha,
        None,
        bind=("_scrim_alpha_transition", "open_progress", "scrim_color"),
    )
    """
    Multiplier for alpha channel of :attr:`scrim_color`. For internal
    usage only.
    """

    scrim_alpha_transition = StringProperty("linear")
    """
    The name of the animation transition type to use for changing
    :attr:`scrim_alpha`.

    :attr:`scrim_alpha_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'linear'`.
    """

    def _get_scrim_alpha_transition(self):
        return getattr(AnimationTransition, self.scrim_alpha_transition)

    _scrim_alpha_transition = AliasProperty(
        _get_scrim_alpha_transition,
        None,
        bind=("scrim_alpha_transition", ),
        cache=True,
    )

    opening_transition = StringProperty("out_cubic")
    """
    The name of the animation transition type to use when animating to
    the :attr:`state` `'open'`.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    opening_time = NumericProperty(0.2)
    """
    The time taken for the panel to slide to the :attr:`state` `'open'`.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    closing_transition = StringProperty("out_sine")
    """The name of the animation transition type to use when animating to
    the :attr:`state` 'close'.

    :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_sine'`.
    """

    closing_time = NumericProperty(0.2)
    """
    The time taken for the panel to slide to the :attr:`state` `'close'`.

    :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(
            open_progress=self.update_status,
            status=self.update_status,
            state=self.update_status,
        )
        Window.bind(on_keyboard=self._handle_keyboard)

    def set_state(self, new_state="toggle", animation=True):
        """Change state of the side panel.
        New_state can be one of `"toggle"`, `"open"` or `"close"`.
        """

        if new_state == "toggle":
            new_state = "close" if self.state == "open" else "open"

        if new_state == "open":
            Animation.cancel_all(self, "open_progress")
            self.status = "opening_with_animation"
            if animation:
                Animation(
                    open_progress=1.0,
                    d=self.opening_time * (1 - self.open_progress),
                    t=self.opening_transition,
                ).start(self)
            else:
                self.open_progress = 1
        else:  # "close"
            Animation.cancel_all(self, "open_progress")
            self.status = "closing_with_animation"
            if animation:
                Animation(
                    open_progress=0.0,
                    d=self.closing_time * self.open_progress,
                    t=self.closing_transition,
                ).start(self)
            else:
                self.open_progress = 0

    def update_status(self, *_):
        status = self.status
        if status == "closed":
            self.state = "close"
        elif status == "opened":
            self.state = "open"
        elif self.open_progress == 1 and status == "opening_with_animation":
            self.status = "opened"
            self.state = "open"
        elif self.open_progress == 0 and status == "closing_with_animation":
            self.status = "closed"
            self.state = "close"
        elif status in (
                "opening_with_swipe",
                "opening_with_animation",
                "closing_with_swipe",
                "closing_with_animation",
        ):
            pass
        if self.status == "closed":
            self._elevation = 0
            self._update_shadow(self, self._elevation)
        else:
            self._elevation = self.elevation
            self._update_shadow(self, self._elevation)

    def get_dist_from_side(self, x):
        if self.anchor == "left":
            return 0 if x < 0 else x
        return 0 if x > Window.width else Window.width - x

    def on_touch_down(self, touch):
        if self.status == "closed":
            return False
        elif self.status == "opened":
            for child in self.children[:]:
                if child.dispatch("on_touch_down", touch):
                    return True
        if self.type == "standard" and not self.collide_point(
                touch.ox, touch.oy):
            return False
        return True

    def on_touch_move(self, touch):
        if self.enable_swiping:
            if self.status == "closed":
                if (self.get_dist_from_side(touch.ox) <= self.swipe_edge_width
                        and abs(touch.x - touch.ox) > self.swipe_distance):
                    self.status = "opening_with_swipe"
            elif self.status == "opened":
                self.status = "closing_with_swipe"

        if self.status in ("opening_with_swipe", "closing_with_swipe"):
            self.open_progress = max(
                min(
                    self.open_progress +
                    (touch.dx if self.anchor == "left" else -touch.dx) /
                    self.width,
                    1,
                ),
                0,
            )
            return True
        return super().on_touch_move(touch)

    def on_touch_up(self, touch):
        if self.status == "opening_with_swipe":
            if self.open_progress > 0.5:
                self.set_state("open", animation=True)
            else:
                self.set_state("close", animation=True)
        elif self.status == "closing_with_swipe":
            if self.open_progress < 0.5:
                self.set_state("close", animation=True)
            else:
                self.set_state("open", animation=True)
        elif self.status == "opened":
            if self.close_on_click and not self.collide_point(
                    touch.ox, touch.oy):
                self.set_state("close", animation=True)
            elif self.type == "standard" and not self.collide_point(
                    touch.ox, touch.oy):
                return False
        elif self.status == "closed":
            return False
        return True

    def on_radius(self, instance, value):
        self._radius = value

    def on_type(self, *args):
        if self.type == "standard":
            self.enable_swiping = False
            self.close_on_click = False
        else:
            self.enable_swiping = True
            self.close_on_click = True

    def _handle_keyboard(self, window, key, *largs):
        if key == 27 and self.status == "opened" and self.close_on_click:
            self.set_state("close")
            return True
예제 #3
0
class ElectrumWindow(App):

    electrum_config = ObjectProperty(None)

    language = StringProperty('en')

    def set_URI(self, uri):
        self.switch_to('send')
        self.send_screen.set_URI(uri)

    def on_new_intent(self, intent):
        if intent.getScheme() != 'bitcoin':
            return
        uri = intent.getDataString()
        self.set_URI(uri)

    def on_language(self, instance, language):
        Logger.info('language: {}'.format(language))
        _.switch_lang(language)

    def on_quotes(self, d):
        #Logger.info("on_quotes")
        pass

    def on_history(self, d):
        #Logger.info("on_history")
        if self.history_screen:
            Clock.schedule_once(lambda dt: self.history_screen.update())

    def _get_bu(self):
        return self.electrum_config.get('base_unit', 'mBTC')

    def _set_bu(self, value):
        assert value in base_units.keys()
        self.electrum_config.set_key('base_unit', value, True)
        self.update_status()
        if self.history_screen:
            self.history_screen.update()

    base_unit = AliasProperty(_get_bu, _set_bu)
    status = StringProperty('')
    fiat_unit = StringProperty('')

    def on_fiat_unit(self, a, b):
        if self.history_screen:
            self.history_screen.update()

    def decimal_point(self):
        return base_units[self.base_unit]

    def btc_to_fiat(self, amount_str):
        if not amount_str:
            return ''
        rate = run_hook('exchange_rate')
        if not rate:
            return ''
        fiat_amount = self.get_amount(amount_str + ' ' +
                                      self.base_unit) * rate / pow(10, 8)
        return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')

    def fiat_to_btc(self, fiat_amount):
        if not fiat_amount:
            return ''
        rate = run_hook('exchange_rate')
        if not rate:
            return ''
        satoshis = int(pow(10, 8) * Decimal(fiat_amount) / Decimal(rate))
        return format_satoshis_plain(satoshis, self.decimal_point())

    def get_amount(self, amount_str):
        a, u = amount_str.split()
        assert u == self.base_unit
        try:
            x = Decimal(a)
        except:
            return None
        p = pow(10, self.decimal_point())
        return int(p * x)

    _orientation = OptionProperty('landscape',
                                  options=('landscape', 'portrait'))

    def _get_orientation(self):
        return self._orientation

    orientation = AliasProperty(_get_orientation,
                                None,
                                bind=('_orientation', ))
    '''Tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
    '''

    _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))

    def _get_ui_mode(self):
        return self._ui_mode

    ui_mode = AliasProperty(_get_ui_mode, None, bind=('_ui_mode', ))
    '''Defines tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
    '''

    def __init__(self, **kwargs):
        # initialize variables
        self._clipboard = Clipboard
        self.info_bubble = None
        self.nfcscanner = None
        self.tabs = None
        self.is_exit = False
        self.wallet = None

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

        title = _('Electrum App')
        self.electrum_config = config = kwargs.get('config', None)
        self.language = config.get('language', 'en')
        self.network = network = kwargs.get('network', None)
        self.plugins = kwargs.get('plugins', [])

        self.gui_object = kwargs.get('gui_object', None)
        self.daemon = self.gui_object.daemon

        self.contacts = Contacts(self.electrum_config)
        self.invoices = InvoiceStore(self.electrum_config)

        # create triggers so as to minimize updation a max of 2 times a sec
        self._trigger_update_wallet =\
            Clock.create_trigger(self.update_wallet, .5)
        self._trigger_update_status =\
            Clock.create_trigger(self.update_status, .5)
        # cached dialogs
        self._settings_dialog = None
        self._password_dialog = None

    def wallet_name(self):
        return os.path.basename(
            self.wallet.storage.path) if self.wallet else ' '

    def on_pr(self, pr):
        if pr.verify(self.contacts):
            key = self.invoices.add(pr)
            if self.invoices_screen:
                self.invoices_screen.update()
            status = self.invoices.get_status(key)
            if status == PR_PAID:
                self.show_error("invoice already paid")
                self.send_screen.do_clear()
            else:
                if pr.has_expired():
                    self.show_error(_('Payment request has expired'))
                else:
                    self.switch_to('send')
                    self.send_screen.set_request(pr)
        else:
            self.show_error("invoice error:" + pr.error)
            self.send_screen.do_clear()

    def on_qr(self, data):
        from electrum.bitcoin import base_decode, is_address
        data = data.strip()
        if is_address(data):
            self.set_URI(data)
            return
        if data.startswith('bitcoin:'):
            self.set_URI(data)
            return
        # try to decode transaction
        from electrum.transaction import Transaction
        try:
            text = base_decode(data, None, base=43).encode('hex')
            tx = Transaction(text)
            tx.deserialize()
        except:
            tx = None
        if tx:
            self.tx_dialog(tx)
            return
        # show error
        self.show_error("Unable to decode QR data")

    def update_tab(self, name):
        s = getattr(self, name + '_screen', None)
        if s:
            s.update()

    @profiler
    def update_tabs(self):
        for tab in ['invoices', 'send', 'history', 'receive', 'requests']:
            self.update_tab(tab)

    def switch_to(self, name):
        s = getattr(self, name + '_screen', None)
        if self.send_screen is None:
            s = self.tabs.ids[name + '_screen']
            s.load_screen()
        panel = self.tabs.ids.panel
        tab = self.tabs.ids[name + '_tab']
        panel.switch_to(tab)

    def show_request(self, addr):
        self.switch_to('receive')
        self.receive_screen.screen.address = addr

    def show_pr_details(self, req, status, is_invoice):
        from electrum.util import format_time
        requestor = req.get('requestor')
        exp = req.get('exp')
        memo = req.get('memo')
        amount = req.get('amount')
        popup = Builder.load_file('gui/kivy/uix/ui_screens/invoice.kv')
        popup.is_invoice = is_invoice
        popup.amount = amount
        popup.requestor = requestor if is_invoice else req.get('address')
        popup.exp = format_time(exp) if exp else ''
        popup.description = memo if memo else ''
        popup.signature = req.get('signature', '')
        popup.status = status
        txid = req.get('txid')
        popup.tx_hash = txid or ''
        popup.on_open = lambda: popup.ids.output_list.update(
            req.get('outputs', []))
        popup.open()

    def qr_dialog(self, title, data, show_text=False):
        from uix.dialogs.qr_dialog import QRDialog
        popup = QRDialog(title, data, show_text)
        popup.open()

    def scan_qr(self, on_complete):
        if platform != 'android':
            return
        from jnius import autoclass
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        Intent = autoclass('android.content.Intent')
        intent = Intent("com.google.zxing.client.android.SCAN")
        intent.putExtra("SCAN_MODE", "QR_CODE_MODE")

        def on_qr_result(requestCode, resultCode, intent):
            if requestCode == 0:
                if resultCode == -1:  # RESULT_OK:
                    contents = intent.getStringExtra("SCAN_RESULT")
                    if intent.getStringExtra(
                            "SCAN_RESULT_FORMAT") == 'QR_CODE':
                        on_complete(contents)
                    else:
                        self.show_error(
                            "wrong format " +
                            intent.getStringExtra("SCAN_RESULT_FORMAT"))

        activity.bind(on_activity_result=on_qr_result)
        try:
            PythonActivity.mActivity.startActivityForResult(intent, 0)
        except:
            self.show_error(
                _('Could not start Barcode Scanner.') + ' ' +
                _('Please install the Barcode Scanner app from ZXing'))

    def scan_qr_zxing(self, on_complete):
        # uses zxing embedded lib
        if platform != 'android':
            return
        from jnius import autoclass
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        IntentIntegrator = autoclass(
            'com.google.zxing.integration.android.IntentIntegrator')
        integrator = IntentIntegrator(PythonActivity.mActivity)

        def on_qr_result(requestCode, resultCode, intent):
            if requestCode == 0:
                if resultCode == -1:  # RESULT_OK:
                    contents = intent.getStringExtra("SCAN_RESULT")
                    if intent.getStringExtra(
                            "SCAN_RESULT_FORMAT") == 'QR_CODE':
                        on_complete(contents)
                    else:
                        self.show_error(
                            "wrong format " +
                            intent.getStringExtra("SCAN_RESULT_FORMAT"))

        activity.bind(on_activity_result=on_qr_result)
        integrator.initiateScan()

    def do_share(self, data, title):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        JS = autoclass('java.lang.String')
        Intent = autoclass('android.content.Intent')
        sendIntent = Intent()
        sendIntent.setAction(Intent.ACTION_SEND)
        sendIntent.setType("text/plain")
        sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        currentActivity = cast('android.app.Activity',
                               PythonActivity.mActivity)
        it = Intent.createChooser(sendIntent,
                                  cast('java.lang.CharSequence', JS(title)))
        currentActivity.startActivity(it)

    def build(self):
        return Builder.load_file('gui/kivy/main.kv')

    def _pause(self):
        if platform == 'android':
            # move activity to back
            from jnius import autoclass
            python_act = autoclass('org.kivy.android.PythonActivity')
            mActivity = python_act.mActivity
            mActivity.moveTaskToBack(True)

    def on_start(self):
        ''' This is the start point of the kivy ui
        '''
        import time
        Logger.info('Time to on_start: {} <<<<<<<<'.format(time.clock()))
        win = Window
        win.bind(size=self.on_size, on_keyboard=self.on_keyboard)
        win.bind(on_key_down=self.on_key_down)
        win.softinput_mode = 'below_target'
        self.on_size(win, win.size)
        self.init_ui()
        self.load_wallet_by_name(self.electrum_config.get_wallet_path())
        # init plugins
        run_hook('init_kivy', self)
        # default tab
        self.switch_to('history')
        # bind intent for bitcoin: URI scheme
        if platform == 'android':
            from android import activity
            from jnius import autoclass
            PythonActivity = autoclass('org.kivy.android.PythonActivity')
            mactivity = PythonActivity.mActivity
            self.on_new_intent(mactivity.getIntent())
            activity.bind(on_new_intent=self.on_new_intent)

        # URI passed in config
        uri = self.electrum_config.get('url')
        if uri:
            self.set_URI(uri)

    def get_wallet_path(self):
        if self.wallet:
            return self.wallet.storage.path
        else:
            return ''

    def on_wizard_complete(self, instance, wallet):
        if wallet:
            self.daemon.add_wallet(wallet)
            self.load_wallet(wallet)
        self.on_resume()

    def load_wallet_by_name(self, path):
        if not path:
            return
        wallet = self.daemon.load_wallet(path)
        if wallet:
            self.load_wallet(wallet)
            self.on_resume()
        else:
            Logger.debug(
                'Electrum: Wallet not found. Launching install wizard')
            wizard = Factory.InstallWizard(self.electrum_config, self.network,
                                           path)
            wizard.bind(on_wizard_complete=self.on_wizard_complete)
            action = wizard.get_action()
            wizard.run(action)

    def on_stop(self):
        self.stop_wallet()

    def stop_wallet(self):
        if self.wallet:
            self.daemon.stop_wallet(self.wallet.storage.path)
            self.wallet = None

    def on_key_down(self, instance, key, keycode, codepoint, modifiers):
        if 'ctrl' in modifiers:
            # q=24 w=25
            if keycode in (24, 25):
                self.stop()
            elif keycode == 27:
                # r=27
                # force update wallet
                self.update_wallet()
            elif keycode == 112:
                # pageup
                #TODO move to next tab
                pass
            elif keycode == 117:
                # pagedown
                #TODO move to prev tab
                pass
        #TODO: alt+tab_number to activate the particular tab

    def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
        if key == 27 and self.is_exit is False:
            self.is_exit = True
            self.show_info(_('Press again to exit'))
            return True
        # override settings button
        if key in (319, 282):  #f1/settings button on android
            #self.gui.main_gui.toggle_settings(self)
            return True

    def settings_dialog(self):
        if self._settings_dialog is None:
            from uix.dialogs.settings import SettingsDialog
            self._settings_dialog = SettingsDialog(self)
        self._settings_dialog.update()
        self._settings_dialog.open()

    def popup_dialog(self, name):
        if name == 'settings':
            self.settings_dialog()
        elif name == 'wallets':
            from uix.dialogs.wallets import WalletDialog
            d = WalletDialog()
            d.open()
        else:
            popup = Builder.load_file('gui/kivy/uix/ui_screens/' + name +
                                      '.kv')
            popup.open()

    @profiler
    def init_ui(self):
        ''' Initialize The Ux part of electrum. This function performs the basic
        tasks of setting up the ui.
        '''
        from weakref import ref

        self.funds_error = False
        # setup UX
        self.screens = {}

        #setup lazy imports for mainscreen
        Factory.register('AnimatedPopup',
                         module='electrum_gui.kivy.uix.dialogs')
        Factory.register('QRCodeWidget',
                         module='electrum_gui.kivy.uix.qrcodewidget')

        # preload widgets. Remove this if you want to load the widgets on demand
        #Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
        #Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget())

        # load and focus the ui
        self.root.manager = self.root.ids['manager']

        self.history_screen = None
        self.contacts_screen = None
        self.send_screen = None
        self.invoices_screen = None
        self.receive_screen = None
        self.requests_screen = None

        self.icon = "icons/electrum.png"

        # connect callbacks
        if self.network:
            interests = ['updated', 'status', 'new_transaction', 'verified']
            self.network.register_callback(self.on_network, interests)

        self.tabs = self.root.ids['tabs']

    def on_network(self, event, *args):
        if event == 'updated':
            self._trigger_update_wallet()
        elif event == 'status':
            self._trigger_update_status()
        elif event == 'new_transaction':
            self._trigger_update_wallet()
        elif event == 'verified':
            self._trigger_update_wallet()

    @profiler
    def load_wallet(self, wallet):
        print "load wallet", wallet.storage.path

        self.stop_wallet()
        self.wallet = wallet
        self.current_account = self.wallet.storage.get('current_account', None)
        self.update_wallet()
        # Once GUI has been initialized check if we want to announce something
        # since the callback has been called before the GUI was initialized
        if self.receive_screen:
            self.receive_screen.clear()
        self.update_tabs()
        run_hook('load_wallet', wallet, self)

    def update_status(self, *dt):
        if not self.wallet:
            self.status = _("No Wallet")
            return
        if self.network is None or not self.network.is_running():
            status = _("Offline")
        elif self.network.is_connected():
            server_height = self.network.get_server_height()
            server_lag = self.network.get_local_height() - server_height
            if not self.wallet.up_to_date or server_height == 0:
                status = _("Synchronizing...")
            elif server_lag > 1:
                status = _("Server lagging (%d blocks)" % server_lag)
            else:
                c, u, x = self.wallet.get_account_balance(self.current_account)
                text = self.format_amount(c + x + u)
                status = str(text.strip() + ' ' + self.base_unit)
        else:
            status = _("Not connected")
        n = self.wallet.basename()
        self.status = '[size=15dp]%s[/size]\n%s' % (
            n, status) if n != 'default_wallet' else status

    def get_max_amount(self):
        inputs = self.wallet.get_spendable_coins(None)
        addr = str(
            self.send_screen.screen.address) or self.wallet.dummy_address()
        amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs,
                                                 (TYPE_ADDRESS, addr), None)
        return format_satoshis_plain(amount, self.decimal_point())

    def format_amount(self, x, is_diff=False, whitespaces=False):
        return format_satoshis(x, is_diff, 0, self.decimal_point(),
                               whitespaces)

    def format_amount_and_units(self, x):
        return format_satoshis_plain(
            x, self.decimal_point()) + ' ' + self.base_unit

    @profiler
    def update_wallet(self, *dt):
        self._trigger_update_status()
        if self.wallet and (self.wallet.up_to_date or not self.network
                            or not self.network.is_connected()):
            self.update_tabs()

    def notify(self, message):
        try:
            global notification, os
            if not notification:
                from plyer import notification
            icon = (os.path.dirname(os.path.realpath(__file__)) + '/../../' +
                    self.icon)
            notification.notify('Electrum',
                                message,
                                app_icon=icon,
                                app_name='Electrum')
        except ImportError:
            Logger.Error('Notification: needs plyer; `sudo pip install plyer`')

    def on_pause(self):
        # pause nfc
        if self.nfcscanner:
            self.nfcscanner.nfc_disable()
        return True

    def on_resume(self):
        if self.nfcscanner:
            self.nfcscanner.nfc_enable()

    def on_size(self, instance, value):
        width, height = value
        self._orientation = 'landscape' if width > height else 'portrait'
        self._ui_mode = 'tablet' if min(width,
                                        height) > inch(3.51) else 'phone'

    def on_ref_label(self, label, touch):
        if label.touched:
            label.touched = False
            self.qr_dialog(label.name, label.data, True)
        else:
            label.touched = True
            self._clipboard.copy(label.data)
            Clock.schedule_once(lambda dt: self.show_info(
                _('Text copied to clipboard.\nTap again to display it as QR code.'
                  )))

    def set_send(self, address, amount, label, message):
        self.send_payment(address, amount=amount, label=label, message=message)

    def show_error(self,
                   error,
                   width='200dp',
                   pos=None,
                   arrow_pos=None,
                   exit=False,
                   icon='atlas://gui/kivy/theming/light/error',
                   duration=0,
                   modal=False):
        ''' Show a error Message Bubble.
        '''
        self.show_info_bubble(text=error,
                              icon=icon,
                              width=width,
                              pos=pos or Window.center,
                              arrow_pos=arrow_pos,
                              exit=exit,
                              duration=duration,
                              modal=modal)

    def show_info(self,
                  error,
                  width='200dp',
                  pos=None,
                  arrow_pos=None,
                  exit=False,
                  duration=0,
                  modal=False):
        ''' Show a Info Message Bubble.
        '''
        self.show_error(error,
                        icon='atlas://gui/kivy/theming/light/important',
                        duration=duration,
                        modal=modal,
                        exit=exit,
                        pos=pos,
                        arrow_pos=arrow_pos)

    def show_info_bubble(self,
                         text=_('Hello World'),
                         pos=None,
                         duration=0,
                         arrow_pos='bottom_mid',
                         width=None,
                         icon='',
                         modal=False,
                         exit=False):
        '''Method to show a Information Bubble

        .. parameters::
            text: Message to be displayed
            pos: position for the bubble
            duration: duration the bubble remains on screen. 0 = click to hide
            width: width of the Bubble
            arrow_pos: arrow position for the bubble
        '''
        info_bubble = self.info_bubble
        if not info_bubble:
            info_bubble = self.info_bubble = Factory.InfoBubble()

        win = Window
        if info_bubble.parent:
            win.remove_widget(info_bubble if not info_bubble.modal else
                              info_bubble._modal_view)

        if not arrow_pos:
            info_bubble.show_arrow = False
        else:
            info_bubble.show_arrow = True
            info_bubble.arrow_pos = arrow_pos
        img = info_bubble.ids.img
        if text == 'texture':
            # icon holds a texture not a source image
            # display the texture in full screen
            text = ''
            img.texture = icon
            info_bubble.fs = True
            info_bubble.show_arrow = False
            img.allow_stretch = True
            info_bubble.dim_background = True
            info_bubble.background_image = 'atlas://gui/kivy/theming/light/card'
        else:
            info_bubble.fs = False
            info_bubble.icon = icon
            #if img.texture and img._coreimage:
            #    img.reload()
            img.allow_stretch = False
            info_bubble.dim_background = False
            info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
        info_bubble.message = text
        if not pos:
            pos = (win.center[0], win.center[1] - (info_bubble.height / 2))
        info_bubble.show(pos, duration, width, modal=modal, exit=exit)

    def tx_dialog(self, tx):
        from uix.dialogs.tx_dialog import TxDialog
        d = TxDialog(self, tx)
        d.open()

    def sign_tx(self, *args):
        threading.Thread(target=self._sign_tx, args=args).start()

    def _sign_tx(self, tx, password, on_success, on_failure):
        try:
            self.wallet.sign_transaction(tx, password)
        except InvalidPassword:
            Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
            return
        Clock.schedule_once(lambda dt: on_success(tx))

    def _broadcast_thread(self, tx, on_complete):
        ok, txid = self.network.broadcast(tx)
        Clock.schedule_once(lambda dt: on_complete(ok, txid))

    def broadcast(self, tx, pr=None):
        def on_complete(ok, txid):
            self.show_info(txid)
            if ok and pr:
                pr.set_paid(tx.hash())
                self.invoices.save()
                self.update_tab('invoices')

        if self.network and self.network.is_connected():
            self.show_info(_('Sending'))
            threading.Thread(target=self._broadcast_thread,
                             args=(tx, on_complete)).start()
        else:
            self.show_info(
                _('Cannot broadcast transaction') + ':\n' + _('Not connected'))

    def description_dialog(self, screen):
        from uix.dialogs.label_dialog import LabelDialog
        text = screen.message

        def callback(text):
            screen.message = text

        d = LabelDialog(_('Enter description'), text, callback)
        d.open()

    @profiler
    def amount_dialog(self, screen, show_max):
        from uix.dialogs.amount_dialog import AmountDialog
        amount = screen.amount
        if amount:
            amount, u = str(amount).split()
            assert u == self.base_unit

        def cb(amount):
            screen.amount = amount

        popup = AmountDialog(show_max, amount, cb)
        popup.open()

    def protected(self, msg, f, args):
        if self.wallet.use_encryption:
            self.password_dialog(msg, f, args)
        else:
            apply(f, args + (None, ))

    def delete_wallet(self):
        from uix.dialogs.question import Question
        basename = os.path.basename(self.wallet.storage.path)
        d = Question(
            _('Delete wallet?') + '\n' + basename, self._delete_wallet)
        d.open()

    def _delete_wallet(self, b):
        if b:
            basename = os.path.basename(self.wallet.storage.path)
            self.protected(
                _("Enter your PIN code to confirm deletion of %s") % basename,
                self.__delete_wallet, ())

    def __delete_wallet(self, pw):
        wallet_path = self.get_wallet_path()
        dirname = os.path.dirname(wallet_path)
        basename = os.path.basename(wallet_path)
        if self.wallet.use_encryption:
            try:
                self.wallet.check_password(pw)
            except:
                self.show_error("Invalid PIN")
                return
        self.stop_wallet()
        os.unlink(wallet_path)
        self.show_error("Wallet removed:" + basename)
        d = os.listdir(dirname)
        name = 'default_wallet'
        new_path = os.path.join(dirname, name)
        self.load_wallet_by_name(new_path)

    def show_seed(self, label):
        self.protected(_("Enter your PIN code in order to decrypt your seed"),
                       self._show_seed, (label, ))

    def _show_seed(self, label, password):
        if self.wallet.use_encryption and password is None:
            return
        try:
            seed = self.wallet.get_seed(password)
        except:
            self.show_error("Invalid PIN")
            return
        label.text = _('Seed') + ':\n' + seed

    def change_password(self, cb):
        if self.wallet.use_encryption:
            self.protected(
                _("Changing PIN code.") + '\n' + _("Enter your current PIN:"),
                self._change_password, (cb, ))
        else:
            self._change_password(cb, None)

    def _change_password(self, cb, old_password):
        if self.wallet.use_encryption:
            if old_password is None:
                return
            try:
                self.wallet.check_password(old_password)
            except InvalidPassword:
                self.show_error("Invalid PIN")
                return
        self.password_dialog(_('Enter new PIN'), self._change_password2, (
            cb,
            old_password,
        ))

    def _change_password2(self, cb, old_password, new_password):
        self.password_dialog(_('Confirm new PIN'), self._change_password3,
                             (cb, old_password, new_password))

    def _change_password3(self, cb, old_password, new_password,
                          confirmed_password):
        if new_password == confirmed_password:
            self.wallet.update_password(old_password, new_password)
            cb()
        else:
            self.show_error("PIN numbers do not match")

    def password_dialog(self, msg, f, args):
        def callback(pw):
            Clock.schedule_once(lambda x: apply(f, args + (pw, )), 0.1)

        if self._password_dialog is None:
            from uix.dialogs.password_dialog import PasswordDialog
            self._password_dialog = PasswordDialog()
        self._password_dialog.init(msg, callback)
        self._password_dialog.open()
예제 #4
0
class MDDropdownMenu(CircularRippleBehavior, ThemableBehavior, BoxLayout):
    items = ListProperty()
    '''See :attr:`~kivy.uix.recycleview.RecycleView.data`
    '''

    width_mult = NumericProperty(1)
    '''This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.
    '''

    max_height = NumericProperty()
    '''The menu will grow no bigger than this number.

    Set to 0 for no limit. Defaults to 0.
    '''

    border_margin = NumericProperty(dp(4))
    '''Margin between Window border and menu
    '''

    ver_growth = OptionProperty(None, allownone=True, options=['up', 'down'])
    '''Where the menu will grow vertically to when opening

    Set to None to let the widget pick for you. Defaults to None.
    '''

    hor_growth = OptionProperty(None,
                                allownone=True,
                                options=['left', 'right'])
    '''Where the menu will grow horizontally to when opening

    Set to None to let the widget pick for you. Defaults to None.
    '''
    def open(self, *largs):
        Window.add_widget(self)
        Clock.schedule_once(lambda x: self.display_menu(largs[0]), -1)

    def display_menu(self, caller):
        # We need to pick a starting point, see how big we need to be,
        # and where to grow to.

        c = caller.to_window(caller.center_x,
                             caller.center_y)  # Starting coords

        # ---ESTABLISH INITIAL TARGET SIZE ESTIMATE---
        target_width = self.width_mult * m_res.STANDARD_INCREMENT
        # If we're wider than the Window...
        if target_width > Window.width:
            # ...reduce our multiplier to max allowed.
            target_width = int(
                Window.width /
                m_res.STANDARD_INCREMENT) * m_res.STANDARD_INCREMENT

        target_height = sum([dp(48) for i in self.items])
        # If we're over max_height...
        if 0 < self.max_height < target_height:
            target_height = self.max_height

        # ---ESTABLISH VERTICAL GROWTH DIRECTION---
        if self.ver_growth is not None:
            ver_growth = self.ver_growth
        else:
            # If there's enough space below us:
            if target_height <= c[1] - self.border_margin:
                ver_growth = 'down'
            # if there's enough space above us:
            elif target_height < Window.height - c[1] - self.border_margin:
                ver_growth = 'up'
            # otherwise, let's pick the one with more space and adjust ourselves
            else:
                # if there's more space below us:
                if c[1] >= Window.height - c[1]:
                    ver_growth = 'down'
                    target_height = c[1] - self.border_margin
                # if there's more space above us:
                else:
                    ver_growth = 'up'
                    target_height = Window.height - c[1] - self.border_margin

        if self.hor_growth is not None:
            hor_growth = self.hor_growth
        else:
            # If there's enough space to the right:
            if target_width <= Window.width - c[0] - self.border_margin:
                hor_growth = 'right'
            # if there's enough space to the left:
            elif target_width < c[0] - self.border_margin:
                hor_growth = 'left'
            # otherwise, let's pick the one with more space and adjust ourselves
            else:
                # if there's more space to the right:
                if Window.width - c[0] >= c[0]:
                    hor_growth = 'right'
                    target_width = Window.width - c[0] - self.border_margin
                # if there's more space to the left:
                else:
                    hor_growth = 'left'
                    target_width = c[0] - self.border_margin

        if ver_growth == 'down':
            tar_y = c[1] - target_height
        else:  # should always be 'up'
            tar_y = c[1]

        if hor_growth == 'right':
            tar_x = c[0]
        else:  # should always be 'left'
            tar_x = c[0] - target_width
        anim = Animation(x=tar_x,
                         y=tar_y,
                         width=target_width,
                         height=target_height,
                         duration=.3,
                         transition='out_quint')
        menu = self.ids['md_menu']
        menu.pos = c
        anim.start(menu)

    def on_touch_down(self, touch):
        if not self.ids['md_menu'].collide_point(*touch.pos):
            self.dismiss()
            return True
        super(MDDropdownMenu, self).on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super(MDDropdownMenu, self).on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super(MDDropdownMenu, self).on_touch_up(touch)
        return True

    def dismiss(self):
        Window.remove_widget(self)
예제 #5
0
class ElectrumWindow(App):

    electrum_config = ObjectProperty(None)
    language = StringProperty('en')

    # properties might be updated by the network
    num_blocks = NumericProperty(0)
    num_nodes = NumericProperty(0)
    server_host = StringProperty('')
    server_port = StringProperty('')
    num_chains = NumericProperty(0)
    blockchain_name = StringProperty('')
    fee_status = StringProperty('Fee')
    balance = StringProperty('')
    fiat_balance = StringProperty('')
    is_fiat = BooleanProperty(False)
    blockchain_forkpoint = NumericProperty(0)

    auto_connect = BooleanProperty(False)
    def on_auto_connect(self, instance, x):
        net_params = self.network.get_parameters()
        net_params = net_params._replace(auto_connect=self.auto_connect)
        self.network.run_from_another_thread(self.network.set_parameters(net_params))
    def toggle_auto_connect(self, x):
        self.auto_connect = not self.auto_connect

    oneserver = BooleanProperty(False)
    def on_oneserver(self, instance, x):
        net_params = self.network.get_parameters()
        net_params = net_params._replace(oneserver=self.oneserver)
        self.network.run_from_another_thread(self.network.set_parameters(net_params))
    def toggle_oneserver(self, x):
        self.oneserver = not self.oneserver

    proxy_str = StringProperty('')
    def update_proxy_str(self, proxy: dict):
        mode = proxy.get('mode')
        host = proxy.get('host')
        port = proxy.get('port')
        self.proxy_str = (host + ':' + port) if mode else _('None')

    def choose_server_dialog(self, popup):
        from .uix.dialogs.choice_dialog import ChoiceDialog
        protocol = 's'
        def cb2(host):
            from electrum_ltc import constants
            pp = servers.get(host, constants.net.DEFAULT_PORTS)
            port = pp.get(protocol, '')
            popup.ids.host.text = host
            popup.ids.port.text = port
        servers = self.network.get_servers()
        ChoiceDialog(_('Choose a server'), sorted(servers), popup.ids.host.text, cb2).open()

    def choose_blockchain_dialog(self, dt):
        from .uix.dialogs.choice_dialog import ChoiceDialog
        chains = self.network.get_blockchains()
        def cb(name):
            with blockchain.blockchains_lock: blockchain_items = list(blockchain.blockchains.items())
            for chain_id, b in blockchain_items:
                if name == b.get_name():
                    self.network.run_from_another_thread(self.network.follow_chain_given_id(chain_id))
        chain_objects = [blockchain.blockchains.get(chain_id) for chain_id in chains]
        chain_objects = filter(lambda b: b is not None, chain_objects)
        names = [b.get_name() for b in chain_objects]
        if len(names) > 1:
            cur_chain = self.network.blockchain().get_name()
            ChoiceDialog(_('Choose your chain'), names, cur_chain, cb).open()

    use_rbf = BooleanProperty(False)
    def on_use_rbf(self, instance, x):
        self.electrum_config.set_key('use_rbf', self.use_rbf, True)

    use_change = BooleanProperty(False)
    def on_use_change(self, instance, x):
        if self.wallet:
            self.wallet.use_change = self.use_change
            self.wallet.storage.put('use_change', self.use_change)
            self.wallet.storage.write()

    use_unconfirmed = BooleanProperty(False)
    def on_use_unconfirmed(self, instance, x):
        self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True)

    def set_URI(self, uri):
        self.switch_to('send')
        self.send_screen.set_URI(uri)

    def set_ln_invoice(self, invoice):
        self.switch_to('send')
        self.send_screen.set_ln_invoice(invoice)

    def on_new_intent(self, intent):
        data = intent.getDataString()
        if intent.getScheme() == 'litecoin':
            self.set_URI(data)
        elif intent.getScheme() == 'lightning':
            self.set_ln_invoice(data)

    def on_language(self, instance, language):
        Logger.info('language: {}'.format(language))
        _.switch_lang(language)

    def update_history(self, *dt):
        if self.history_screen:
            self.history_screen.update()

    def on_quotes(self, d):
        Logger.info("on_quotes")
        self._trigger_update_status()
        self._trigger_update_history()

    def on_history(self, d):
        Logger.info("on_history")
        if self.wallet:
            self.wallet.clear_coin_price_cache()
        self._trigger_update_history()

    def on_fee_histogram(self, *args):
        self._trigger_update_history()

    def on_payment_received(self, event, wallet, key, status):
        if self.request_popup and self.request_popup.key == key:
            self.request_popup.set_status(status)
        if status == PR_PAID:
            self.show_info(_('Payment Received') + '\n' + key)

    def on_payment_status(self, event, key, status, *args):
        self.update_tab('send')
        if status == 'success':
            self.show_info(_('Payment was sent'))
            self._trigger_update_history()
        elif status == 'progress':
            pass
        elif status == 'failure':
            self.show_info(_('Payment failed'))
        elif status == 'error':
            e = args[0]
            self.show_error(_('Error') + '\n' + str(e))

    def _get_bu(self):
        decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
        try:
            return decimal_point_to_base_unit_name(decimal_point)
        except UnknownBaseUnit:
            return decimal_point_to_base_unit_name(DECIMAL_POINT_DEFAULT)

    def _set_bu(self, value):
        assert value in base_units.keys()
        decimal_point = base_unit_name_to_decimal_point(value)
        self.electrum_config.set_key('decimal_point', decimal_point, True)
        self._trigger_update_status()
        self._trigger_update_history()

    wallet_name = StringProperty(_('No Wallet'))
    base_unit = AliasProperty(_get_bu, _set_bu)
    fiat_unit = StringProperty('')

    def on_fiat_unit(self, a, b):
        self._trigger_update_history()

    def decimal_point(self):
        return base_units[self.base_unit]

    def btc_to_fiat(self, amount_str):
        if not amount_str:
            return ''
        if not self.fx.is_enabled():
            return ''
        rate = self.fx.exchange_rate()
        if rate.is_nan():
            return ''
        fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
        return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.')

    def fiat_to_btc(self, fiat_amount):
        if not fiat_amount:
            return ''
        rate = self.fx.exchange_rate()
        if rate.is_nan():
            return ''
        satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
        return format_satoshis_plain(satoshis, self.decimal_point())

    def get_amount(self, amount_str):
        a, u = amount_str.split()
        assert u == self.base_unit
        try:
            x = Decimal(a)
        except:
            return None
        p = pow(10, self.decimal_point())
        return int(p * x)


    _orientation = OptionProperty('landscape',
                                 options=('landscape', 'portrait'))

    def _get_orientation(self):
        return self._orientation

    orientation = AliasProperty(_get_orientation,
                                None,
                                bind=('_orientation',))
    '''Tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape'
    '''

    _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))

    def _get_ui_mode(self):
        return self._ui_mode

    ui_mode = AliasProperty(_get_ui_mode,
                            None,
                            bind=('_ui_mode',))
    '''Defines tries to ascertain the kind of device the app is running on.
    Cane be one of `tablet` or `phone`.

    :data:`ui_mode` is a read only `AliasProperty` Defaults to 'phone'
    '''

    def __init__(self, **kwargs):
        # initialize variables
        self._clipboard = Clipboard
        self.info_bubble = None
        self.nfcscanner = None
        self.tabs = None
        self.is_exit = False
        self.wallet = None  # type: Optional[Abstract_Wallet]
        self.pause_time = 0
        self.asyncio_loop = asyncio.get_event_loop()

        App.__init__(self)#, **kwargs)

        self.electrum_config = config = kwargs.get('config', None)  # type: SimpleConfig
        self.language = config.get('language', 'en')
        self.network = network = kwargs.get('network', None)  # type: Network
        if self.network:
            self.num_blocks = self.network.get_local_height()
            self.num_nodes = len(self.network.get_interfaces())
            net_params = self.network.get_parameters()
            self.server_host = net_params.host
            self.server_port = net_params.port
            self.auto_connect = net_params.auto_connect
            self.oneserver = net_params.oneserver
            self.proxy_config = net_params.proxy if net_params.proxy else {}
            self.update_proxy_str(self.proxy_config)

        self.plugins = kwargs.get('plugins', None)  # type: Plugins
        self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
        self.daemon = self.gui_object.daemon
        self.fx = self.daemon.fx

        self.is_lightning_enabled = bool(config.get('lightning'))

        self.use_rbf = config.get('use_rbf', True)
        self.use_unconfirmed = not config.get('confirmed_only', False)

        # create triggers so as to minimize updating a max of 2 times a sec
        self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
        self._trigger_update_status = Clock.create_trigger(self.update_status, .5)
        self._trigger_update_history = Clock.create_trigger(self.update_history, .5)
        self._trigger_update_interfaces = Clock.create_trigger(self.update_interfaces, .5)

        self._periodic_update_status_during_sync = Clock.schedule_interval(self.update_wallet_synchronizing_progress, .5)

        # cached dialogs
        self._settings_dialog = None
        self._password_dialog = None
        self._channels_dialog = None
        self._addresses_dialog = None
        self.fee_status = self.electrum_config.get_fee_status()
        self.request_popup = None

    def on_pr(self, pr):
        if not self.wallet:
            self.show_error(_('No wallet loaded.'))
            return
        if pr.verify(self.wallet.contacts):
            key = pr.get_id()
            invoice = self.wallet.get_invoice(key)
            if invoice and invoice['status'] == PR_PAID:
                self.show_error("invoice already paid")
                self.send_screen.do_clear()
            elif pr.has_expired():
                self.show_error(_('Payment request has expired'))
            else:
                self.switch_to('send')
                self.send_screen.set_request(pr)
        else:
            self.show_error("invoice error:" + pr.error)
            self.send_screen.do_clear()

    def on_qr(self, data):
        from electrum_ltc.bitcoin import base_decode, is_address
        data = data.strip()
        if is_address(data):
            self.set_URI(data)
            return
        if data.startswith('litecoin:'):
            self.set_URI(data)
            return
        if data.startswith('ln'):
            self.set_ln_invoice(data)
            return
        # try to decode transaction
        from electrum_ltc.transaction import Transaction
        from electrum_ltc.util import bh2u
        try:
            text = bh2u(base_decode(data, None, base=43))
            tx = Transaction(text)
            tx.deserialize()
        except:
            tx = None
        if tx:
            self.tx_dialog(tx)
            return
        # show error
        self.show_error("Unable to decode QR data")

    def update_tab(self, name):
        s = getattr(self, name + '_screen', None)
        if s:
            s.update()

    @profiler
    def update_tabs(self):
        for tab in ['invoices', 'send', 'history', 'receive', 'address']:
            self.update_tab(tab)

    def switch_to(self, name):
        s = getattr(self, name + '_screen', None)
        if s is None:
            s = self.tabs.ids[name + '_screen']
            s.load_screen()
        panel = self.tabs.ids.panel
        tab = self.tabs.ids[name + '_tab']
        panel.switch_to(tab)

    def show_request(self, is_lightning, key):
        from .uix.dialogs.request_dialog import RequestDialog
        request = self.wallet.get_request(key)
        status = request['status']
        data = request['invoice'] if is_lightning else request['URI']
        self.request_popup = RequestDialog('Request', data, key)
        self.request_popup.set_status(request['status'])
        self.request_popup.open()

    def show_invoice(self, is_lightning, key):
        from .uix.dialogs.invoice_dialog import InvoiceDialog
        invoice = self.wallet.get_invoice(key)
        if not invoice:
            return
        status = invoice['status']
        data = invoice['invoice'] if is_lightning else key
        self.invoice_popup = InvoiceDialog('Invoice', data, key)
        self.invoice_popup.open()

    def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None):
        from .uix.dialogs.qr_dialog import QRDialog
        def on_qr_failure():
            popup.dismiss()
            msg = _('Failed to display QR code.')
            if text_for_clipboard:
                msg += '\n' + _('Text copied to clipboard.')
                self._clipboard.copy(text_for_clipboard)
            Clock.schedule_once(lambda dt: self.show_info(msg))
        popup = QRDialog(title, data, show_text, failure_cb=on_qr_failure,
                         text_for_clipboard=text_for_clipboard)
        popup.open()

    def scan_qr(self, on_complete):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        from android import activity
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        SimpleScannerActivity = autoclass("org.electrum.qr.SimpleScannerActivity")
        Intent = autoclass('android.content.Intent')
        intent = Intent(PythonActivity.mActivity, SimpleScannerActivity)

        def on_qr_result(requestCode, resultCode, intent):
            try:
                if resultCode == -1:  # RESULT_OK:
                    #  this doesn't work due to some bug in jnius:
                    # contents = intent.getStringExtra("text")
                    String = autoclass("java.lang.String")
                    contents = intent.getStringExtra(String("text"))
                    on_complete(contents)
            except Exception as e:  # exc would otherwise get lost
                send_exception_to_crash_reporter(e)
            finally:
                activity.unbind(on_activity_result=on_qr_result)
        activity.bind(on_activity_result=on_qr_result)
        PythonActivity.mActivity.startActivityForResult(intent, 0)

    def do_share(self, data, title):
        if platform != 'android':
            return
        from jnius import autoclass, cast
        JS = autoclass('java.lang.String')
        Intent = autoclass('android.content.Intent')
        sendIntent = Intent()
        sendIntent.setAction(Intent.ACTION_SEND)
        sendIntent.setType("text/plain")
        sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data))
        PythonActivity = autoclass('org.kivy.android.PythonActivity')
        currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
        it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title)))
        currentActivity.startActivity(it)

    def build(self):
        return Builder.load_file('electrum_ltc/gui/kivy/main.kv')

    def _pause(self):
        if platform == 'android':
            # move activity to back
            from jnius import autoclass
            python_act = autoclass('org.kivy.android.PythonActivity')
            mActivity = python_act.mActivity
            mActivity.moveTaskToBack(True)

    def handle_crash_on_startup(func):
        def wrapper(self, *args, **kwargs):
            try:
                return func(self, *args, **kwargs)
            except Exception as e:
                from .uix.dialogs.crash_reporter import CrashReporter
                # show the crash reporter, and when it's closed, shutdown the app
                cr = CrashReporter(self, exctype=type(e), value=e, tb=e.__traceback__)
                cr.on_dismiss = lambda: self.stop()
                Clock.schedule_once(lambda _, cr=cr: cr.open(), 0)
        return wrapper

    @handle_crash_on_startup
    def on_start(self):
        ''' This is the start point of the kivy ui
        '''
        import time
        Logger.info('Time to on_start: {} <<<<<<<<'.format(time.process_time()))
        Window.bind(size=self.on_size, on_keyboard=self.on_keyboard)
        Window.bind(on_key_down=self.on_key_down)
        #Window.softinput_mode = 'below_target'
        self.on_size(Window, Window.size)
        self.init_ui()
        crash_reporter.ExceptionHook(self)
        # init plugins
        run_hook('init_kivy', self)
        # fiat currency
        self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
        # default tab
        self.switch_to('history')
        # bind intent for bitcoin: URI scheme
        if platform == 'android':
            from android import activity
            from jnius import autoclass
            PythonActivity = autoclass('org.kivy.android.PythonActivity')
            mactivity = PythonActivity.mActivity
            self.on_new_intent(mactivity.getIntent())
            activity.bind(on_new_intent=self.on_new_intent)
        # connect callbacks
        if self.network:
            interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
                         'status', 'new_transaction', 'verified']
            self.network.register_callback(self.on_network_event, interests)
            self.network.register_callback(self.on_fee, ['fee'])
            self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
            self.network.register_callback(self.on_quotes, ['on_quotes'])
            self.network.register_callback(self.on_history, ['on_history'])
            self.network.register_callback(self.on_payment_received, ['payment_received'])
            self.network.register_callback(self.on_channels, ['channels'])
            self.network.register_callback(self.on_channel, ['channel'])
            self.network.register_callback(self.on_payment_status, ['payment_status'])
        # load wallet
        self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
        # URI passed in config
        uri = self.electrum_config.get('url')
        if uri:
            self.set_URI(uri)


    def get_wallet_path(self):
        if self.wallet:
            return self.wallet.storage.path
        else:
            return ''

    def on_wizard_complete(self, wizard, storage):
        if storage:
            wallet = Wallet(storage, config=self.electrum_config)
            wallet.start_network(self.daemon.network)
            self.daemon.add_wallet(wallet)
            self.load_wallet(wallet)
        elif not self.wallet:
            # wizard did not return a wallet; and there is no wallet open atm
            # try to open last saved wallet (potentially start wizard again)
            self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True),
                                     ask_if_wizard=True)

    def load_wallet_by_name(self, path, ask_if_wizard=False):
        if not path:
            return
        if self.wallet and self.wallet.storage.path == path:
            return
        wallet = self.daemon.load_wallet(path, None)
        if wallet:
            if platform == 'android' and wallet.has_password():
                self.password_dialog(wallet, _('Enter PIN code'), lambda x: self.load_wallet(wallet), self.stop)
            else:
                self.load_wallet(wallet)
        else:
            def launch_wizard():
                wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
                wizard.path = path
                wizard.bind(on_wizard_complete=self.on_wizard_complete)
                storage = WalletStorage(path, manual_upgrades=True)
                if not storage.file_exists():
                    wizard.run('new')
                elif storage.is_encrypted():
                    raise Exception("Kivy GUI does not support encrypted wallet files.")
                elif storage.requires_upgrade():
                    wizard.upgrade_storage(storage)
                else:
                    raise Exception("unexpected storage file situation")
            if not ask_if_wizard:
                launch_wizard()
            else:
                from .uix.dialogs.question import Question
                def handle_answer(b: bool):
                    if b:
                        launch_wizard()
                    else:
                        try: os.unlink(path)
                        except FileNotFoundError: pass
                        self.stop()
                d = Question(_('Do you want to launch the wizard again?'), handle_answer)
                d.open()

    def on_stop(self):
        Logger.info('on_stop')
        if self.wallet:
            self.electrum_config.save_last_wallet(self.wallet)
        self.stop_wallet()

    def stop_wallet(self):
        if self.wallet:
            self.daemon.stop_wallet(self.wallet.storage.path)
            self.wallet = None

    def on_key_down(self, instance, key, keycode, codepoint, modifiers):
        if 'ctrl' in modifiers:
            # q=24 w=25
            if keycode in (24, 25):
                self.stop()
            elif keycode == 27:
                # r=27
                # force update wallet
                self.update_wallet()
            elif keycode == 112:
                # pageup
                #TODO move to next tab
                pass
            elif keycode == 117:
                # pagedown
                #TODO move to prev tab
                pass
        #TODO: alt+tab_number to activate the particular tab

    def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
        if key == 27 and self.is_exit is False:
            self.is_exit = True
            self.show_info(_('Press again to exit'))
            return True
        # override settings button
        if key in (319, 282): #f1/settings button on android
            #self.gui.main_gui.toggle_settings(self)
            return True

    def settings_dialog(self):
        from .uix.dialogs.settings import SettingsDialog
        if self._settings_dialog is None:
            self._settings_dialog = SettingsDialog(self)
        self._settings_dialog.update()
        self._settings_dialog.open()

    def lightning_open_channel_dialog(self):
        d = LightningOpenChannelDialog(self)
        d.open()

    def lightning_channels_dialog(self):
        if self._channels_dialog is None:
            self._channels_dialog = LightningChannelsDialog(self)
        self._channels_dialog.open()

    def on_channel(self, evt, chan):
        if self._channels_dialog:
            Clock.schedule_once(lambda dt: self._channels_dialog.update())

    def on_channels(self, evt):
        if self._channels_dialog:
            Clock.schedule_once(lambda dt: self._channels_dialog.update())

    def popup_dialog(self, name):
        if name == 'settings':
            self.settings_dialog()
        elif name == 'wallets':
            from .uix.dialogs.wallets import WalletDialog
            d = WalletDialog()
            d.open()
        elif name == 'status':
            popup = Builder.load_file('electrum_ltc/gui/kivy/uix/ui_screens/'+name+'.kv')
            master_public_keys_layout = popup.ids.master_public_keys
            for xpub in self.wallet.get_master_public_keys()[1:]:
                master_public_keys_layout.add_widget(TopLabel(text=_('Master Public Key')))
                ref = RefLabel()
                ref.name = _('Master Public Key')
                ref.data = xpub
                master_public_keys_layout.add_widget(ref)
            popup.open()
        elif name.endswith("_dialog"):
            getattr(self, name)()
        else:
            popup = Builder.load_file('electrum_ltc/gui/kivy/uix/ui_screens/'+name+'.kv')
            popup.open()

    @profiler
    def init_ui(self):
        ''' Initialize The Ux part of electrum. This function performs the basic
        tasks of setting up the ui.
        '''
        #from weakref import ref

        self.funds_error = False
        # setup UX
        self.screens = {}

        #setup lazy imports for mainscreen
        Factory.register('AnimatedPopup',
                         module='electrum_ltc.gui.kivy.uix.dialogs')
        Factory.register('QRCodeWidget',
                         module='electrum_ltc.gui.kivy.uix.qrcodewidget')

        # preload widgets. Remove this if you want to load the widgets on demand
        #Cache.append('electrum_ltc_widgets', 'AnimatedPopup', Factory.AnimatedPopup())
        #Cache.append('electrum_ltc_widgets', 'QRCodeWidget', Factory.QRCodeWidget())

        # load and focus the ui
        self.root.manager = self.root.ids['manager']

        self.history_screen = None
        self.contacts_screen = None
        self.send_screen = None
        self.invoices_screen = None
        self.receive_screen = None
        self.requests_screen = None
        self.address_screen = None
        self.icon = "electrum_ltc/gui/icons/electrum-ltc.png"
        self.tabs = self.root.ids['tabs']

    def update_interfaces(self, dt):
        net_params = self.network.get_parameters()
        self.num_nodes = len(self.network.get_interfaces())
        self.num_chains = len(self.network.get_blockchains())
        chain = self.network.blockchain()
        self.blockchain_forkpoint = chain.get_max_forkpoint()
        self.blockchain_name = chain.get_name()
        interface = self.network.interface
        if interface:
            self.server_host = interface.host
        else:
            self.server_host = str(net_params.host) + ' (connecting...)'
        self.proxy_config = net_params.proxy or {}
        self.update_proxy_str(self.proxy_config)

    def on_network_event(self, event, *args):
        Logger.info('network event: '+ event)
        if event == 'network_updated':
            self._trigger_update_interfaces()
            self._trigger_update_status()
        elif event == 'wallet_updated':
            self._trigger_update_wallet()
            self._trigger_update_status()
        elif event == 'blockchain_updated':
            # to update number of confirmations in history
            self._trigger_update_wallet()
        elif event == 'status':
            self._trigger_update_status()
        elif event == 'new_transaction':
            self._trigger_update_wallet()
        elif event == 'verified':
            self._trigger_update_wallet()

    @profiler
    def load_wallet(self, wallet: 'Abstract_Wallet'):
        if self.wallet:
            self.stop_wallet()
        self.wallet = wallet
        self.wallet_name = wallet.basename()
        self.update_wallet()
        # Once GUI has been initialized check if we want to announce something
        # since the callback has been called before the GUI was initialized
        if self.receive_screen:
            self.receive_screen.clear()
        self.update_tabs()
        run_hook('load_wallet', wallet, self)
        try:
            wallet.try_detecting_internal_addresses_corruption()
        except InternalAddressCorruption as e:
            self.show_error(str(e))
            send_exception_to_crash_reporter(e)
            return
        self.use_change = self.wallet.use_change

    def update_status(self, *dt):
        if not self.wallet:
            return
        if self.network is None or not self.network.is_connected():
            status = _("Offline")
        elif self.network.is_connected():
            self.num_blocks = self.network.get_local_height()
            server_height = self.network.get_server_height()
            server_lag = self.num_blocks - server_height
            if not self.wallet.up_to_date or server_height == 0:
                num_sent, num_answered = self.wallet.get_history_sync_state_details()
                status = ("{} [size=18dp]({}/{})[/size]"
                          .format(_("Synchronizing..."), num_answered, num_sent))
            elif server_lag > 1:
                status = _("Server is lagging ({} blocks)").format(server_lag)
            else:
                status = ''
        else:
            status = _("Disconnected")
        if status:
            self.balance = status
            self.fiat_balance = status
        else:
            c, u, x = self.wallet.get_balance()
            text = self.format_amount(c+x+u)
            self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit
            self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy

    def update_wallet_synchronizing_progress(self, *dt):
        if not self.wallet:
            return
        if not self.wallet.up_to_date:
            self._trigger_update_status()

    def get_max_amount(self):
        from electrum_ltc.transaction import TxOutput
        if run_hook('abort_send', self):
            return ''
        inputs = self.wallet.get_spendable_coins(None)
        if not inputs:
            return ''
        addr = None
        if self.send_screen:
            addr = str(self.send_screen.screen.address)
        if not addr:
            addr = self.wallet.dummy_address()
        outputs = [TxOutput(TYPE_ADDRESS, addr, '!')]
        try:
            tx = self.wallet.make_unsigned_transaction(inputs, outputs)
        except NoDynamicFeeEstimates as e:
            Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
            return ''
        except NotEnoughFunds:
            return ''
        except InternalAddressCorruption as e:
            self.show_error(str(e))
            send_exception_to_crash_reporter(e)
            return ''
        amount = tx.output_value()
        __, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
        amount_after_all_fees = amount - x_fee_amount
        return format_satoshis_plain(amount_after_all_fees, self.decimal_point())

    def format_amount(self, x, is_diff=False, whitespaces=False):
        return format_satoshis(x, 0, self.decimal_point(), is_diff=is_diff, whitespaces=whitespaces)

    def format_amount_and_units(self, x):
        return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit

    def format_fee_rate(self, fee_rate):
        # fee_rate is in sat/kB
        return format_fee_satoshis(fee_rate/1000) + ' sat/byte'

    #@profiler
    def update_wallet(self, *dt):
        self._trigger_update_status()
        if self.wallet and (self.wallet.up_to_date or not self.network or not self.network.is_connected()):
            self.update_tabs()

    def notify(self, message):
        try:
            global notification, os
            if not notification:
                from plyer import notification
            icon = (os.path.dirname(os.path.realpath(__file__))
                    + '/../../' + self.icon)
            notification.notify('Electrum-LTC', message,
                            app_icon=icon, app_name='Electrum-LTC')
        except ImportError:
            Logger.Error('Notification: needs plyer; `sudo python3 -m pip install plyer`')

    def on_pause(self):
        self.pause_time = time.time()
        # pause nfc
        if self.nfcscanner:
            self.nfcscanner.nfc_disable()
        return True

    def on_resume(self):
        now = time.time()
        if self.wallet and self.wallet.has_password() and now - self.pause_time > 60:
            self.password_dialog(self.wallet, _('Enter PIN'), None, self.stop)
        if self.nfcscanner:
            self.nfcscanner.nfc_enable()

    def on_size(self, instance, value):
        width, height = value
        self._orientation = 'landscape' if width > height else 'portrait'
        self._ui_mode = 'tablet' if min(width, height) > inch(3.51) else 'phone'

    def on_ref_label(self, label):
        if not label.data:
            return
        self.qr_dialog(label.name, label.data, True)

    def show_error(self, error, width='200dp', pos=None, arrow_pos=None,
        exit=False, icon='atlas://electrum_ltc/gui/kivy/theming/light/error', duration=0,
        modal=False):
        ''' Show an error Message Bubble.
        '''
        self.show_info_bubble( text=error, icon=icon, width=width,
            pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit,
            duration=duration, modal=modal)

    def show_info(self, error, width='200dp', pos=None, arrow_pos=None,
        exit=False, duration=0, modal=False):
        ''' Show an Info Message Bubble.
        '''
        self.show_error(error, icon='atlas://electrum_ltc/gui/kivy/theming/light/important',
            duration=duration, modal=modal, exit=exit, pos=pos,
            arrow_pos=arrow_pos)

    def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0,
        arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False):
        '''Method to show an Information Bubble

        .. parameters::
            text: Message to be displayed
            pos: position for the bubble
            duration: duration the bubble remains on screen. 0 = click to hide
            width: width of the Bubble
            arrow_pos: arrow position for the bubble
        '''
        info_bubble = self.info_bubble
        if not info_bubble:
            info_bubble = self.info_bubble = Factory.InfoBubble()

        win = Window
        if info_bubble.parent:
            win.remove_widget(info_bubble
                                 if not info_bubble.modal else
                                 info_bubble._modal_view)

        if not arrow_pos:
            info_bubble.show_arrow = False
        else:
            info_bubble.show_arrow = True
            info_bubble.arrow_pos = arrow_pos
        img = info_bubble.ids.img
        if text == 'texture':
            # icon holds a texture not a source image
            # display the texture in full screen
            text = ''
            img.texture = icon
            info_bubble.fs = True
            info_bubble.show_arrow = False
            img.allow_stretch = True
            info_bubble.dim_background = True
            info_bubble.background_image = 'atlas://electrum_ltc/gui/kivy/theming/light/card'
        else:
            info_bubble.fs = False
            info_bubble.icon = icon
            #if img.texture and img._coreimage:
            #    img.reload()
            img.allow_stretch = False
            info_bubble.dim_background = False
            info_bubble.background_image = 'atlas://data/images/defaulttheme/bubble'
        info_bubble.message = text
        if not pos:
            pos = (win.center[0], win.center[1] - (info_bubble.height/2))
        info_bubble.show(pos, duration, width, modal=modal, exit=exit)

    def tx_dialog(self, tx):
        from .uix.dialogs.tx_dialog import TxDialog
        d = TxDialog(self, tx)
        d.open()

    def sign_tx(self, *args):
        threading.Thread(target=self._sign_tx, args=args).start()

    def _sign_tx(self, tx, password, on_success, on_failure):
        try:
            self.wallet.sign_transaction(tx, password)
        except InvalidPassword:
            Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
            return
        on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
        Clock.schedule_once(lambda dt: on_success(tx))

    def _broadcast_thread(self, tx, on_complete):
        status = False
        try:
            self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
        except TxBroadcastError as e:
            msg = e.get_message_for_gui()
        except BestEffortRequestFailed as e:
            msg = repr(e)
        else:
            status, msg = True, tx.txid()
        Clock.schedule_once(lambda dt: on_complete(status, msg))

    def broadcast(self, tx, pr=None):
        def on_complete(ok, msg):
            if ok:
                self.show_info(_('Payment sent.'))
                if self.send_screen:
                    self.send_screen.do_clear()
                if pr:
                    self.wallet.invoices.set_paid(pr, tx.txid())
                    self.wallet.invoices.save()
                    self.update_tab('invoices')
            else:
                msg = msg or ''
                self.show_error(msg)

        if self.network and self.network.is_connected():
            self.show_info(_('Sending'))
            threading.Thread(target=self._broadcast_thread, args=(tx, on_complete)).start()
        else:
            self.show_info(_('Cannot broadcast transaction') + ':\n' + _('Not connected'))

    def description_dialog(self, screen):
        from .uix.dialogs.label_dialog import LabelDialog
        text = screen.message
        def callback(text):
            screen.message = text
        d = LabelDialog(_('Enter description'), text, callback)
        d.open()

    def amount_dialog(self, screen, show_max):
        from .uix.dialogs.amount_dialog import AmountDialog
        amount = screen.amount
        if amount:
            amount, u = str(amount).split()
            assert u == self.base_unit
        def cb(amount):
            screen.amount = amount
        popup = AmountDialog(show_max, amount, cb)
        popup.open()

    def addresses_dialog(self):
        from .uix.dialogs.addresses import AddressesDialog
        if self._addresses_dialog is None:
            self._addresses_dialog = AddressesDialog(self)
        self._addresses_dialog.update()
        self._addresses_dialog.open()

    def fee_dialog(self, label, dt):
        from .uix.dialogs.fee_dialog import FeeDialog
        def cb():
            self.fee_status = self.electrum_config.get_fee_status()
        fee_dialog = FeeDialog(self, self.electrum_config, cb)
        fee_dialog.open()

    def on_fee(self, event, *arg):
        self.fee_status = self.electrum_config.get_fee_status()

    def protected(self, msg, f, args):
        if self.wallet.has_password():
            on_success = lambda pw: f(*(args + (pw,)))
            self.password_dialog(self.wallet, msg, on_success, lambda: None)
        else:
            f(*(args + (None,)))

    def delete_wallet(self):
        from .uix.dialogs.question import Question
        basename = os.path.basename(self.wallet.storage.path)
        d = Question(_('Delete wallet?') + '\n' + basename, self._delete_wallet)
        d.open()

    def _delete_wallet(self, b):
        if b:
            basename = self.wallet.basename()
            self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ())

    def __delete_wallet(self, pw):
        wallet_path = self.get_wallet_path()
        basename = os.path.basename(wallet_path)
        if self.wallet.has_password():
            try:
                self.wallet.check_password(pw)
            except:
                self.show_error("Invalid PIN")
                return
        self.stop_wallet()
        os.unlink(wallet_path)
        self.show_error(_("Wallet removed: {}").format(basename))
        new_path = self.electrum_config.get_wallet_path(use_gui_last_wallet=True)
        self.load_wallet_by_name(new_path)

    def show_seed(self, label):
        self.protected(_("Enter your PIN code in order to decrypt your seed"), self._show_seed, (label,))

    def _show_seed(self, label, password):
        if self.wallet.has_password() and password is None:
            return
        keystore = self.wallet.keystore
        try:
            seed = keystore.get_seed(password)
            passphrase = keystore.get_passphrase(password)
        except:
            self.show_error("Invalid PIN")
            return
        label.data = seed
        if passphrase:
            label.data += '\n\n' + _('Passphrase') + ': ' + passphrase

    def password_dialog(self, wallet, msg, on_success, on_failure):
        from .uix.dialogs.password_dialog import PasswordDialog
        if self._password_dialog is None:
            self._password_dialog = PasswordDialog()
        self._password_dialog.init(self, wallet, msg, on_success, on_failure)
        self._password_dialog.open()

    def change_password(self, cb):
        from .uix.dialogs.password_dialog import PasswordDialog
        if self._password_dialog is None:
            self._password_dialog = PasswordDialog()
        message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:")
        def on_success(old_password, new_password):
            self.wallet.update_password(old_password, new_password)
            self.show_info(_("Your PIN code was updated"))
        on_failure = lambda: self.show_error(_("PIN codes do not match"))
        self._password_dialog.init(self, self.wallet, message, on_success, on_failure, is_change=1)
        self._password_dialog.open()

    def export_private_keys(self, pk_label, addr):
        if self.wallet.is_watching_only():
            self.show_info(_('This is a watching-only wallet. It does not contain private keys.'))
            return
        def show_private_key(addr, pk_label, password):
            if self.wallet.has_password() and password is None:
                return
            if not self.wallet.can_export():
                return
            try:
                key = str(self.wallet.export_private_key(addr, password)[0])
                pk_label.data = key
            except InvalidPassword:
                self.show_error("Invalid PIN")
                return
        self.protected(_("Enter your PIN code in order to decrypt your private key"), show_private_key, (addr, pk_label))
예제 #6
0
class AnimatedButton(Label):

    state = OptionProperty('normal', options=('normal', 'down'))

    allow_stretch = BooleanProperty(True)

    keep_ratio = BooleanProperty(False)

    border = ObjectProperty(None)

    anim_delay = ObjectProperty(None)

    background_normal = StringProperty(
        'atlas://data/images/defaulttheme/button')

    texture_background = ObjectProperty(None)

    background_down = StringProperty(
        'atlas://data/images/defaulttheme/button_pressed')

    def __init__(self, **kwargs):
        super(AnimatedButton, self).__init__(**kwargs)

        self.register_event_type('on_press')
        self.register_event_type('on_release')
        #borderImage.border by default is ...
        self.border = (16, 16, 16, 16)
        #Image to display depending on state
        self.img = Image(source=self.background_normal,
                         allow_stretch=self.allow_stretch,
                         keep_ratio=self.keep_ratio,
                         mipmap=True)

        #reset animation if anim_delay is changed
        def anim_reset(*l):
            self.img.anim_delay = self.anim_delay

        self.bind(anim_delay=anim_reset)
        self.anim_delay = .1
        #update self.texture when image.texture changes
        self.img.bind(texture=self.on_tex_changed)
        self.on_tex_changed()

        #update image source when background image is changed
        def background_changed(*l):
            self.img.source = self.background_normal
            self.anim_delay = .1

        self.bind(background_normal=background_changed)

    def on_tex_changed(self, *largs):
        self.texture_background = self.img.texture

    def _do_press(self):
        self.state = 'down'

    def _do_release(self):
        self.state = 'normal'

    def on_touch_down(self, touch):
        if not self.collide_point(touch.x, touch.y):
            return False
        if self in touch.ud:
            return False
        touch.grab(self)
        touch.ud[self] = True
        _animdelay = self.img.anim_delay
        self.img.source = self.background_down
        self.img.anim_delay = _animdelay
        self._do_press()
        self.dispatch('on_press')
        return True

    def on_touch_move(self, touch):
        return self in touch.ud

    def on_touch_up(self, touch):
        if touch.grab_current is not self:
            return
        assert (self in touch.ud)
        touch.ungrab(self)
        _animdelay = self.img._coreimage.anim_delay
        self.img.source = self.background_normal
        self.anim_delay = _animdelay
        self._do_release()
        self.dispatch('on_release')
        return True

    def on_press(self):
        pass

    def on_release(self):
        pass
예제 #7
0
파일: label.py 프로젝트: cowens1919/Nix-BTC
class MDLabel(ThemableBehavior, Label):
    font_style = OptionProperty("Body1", options=theme_font_styles)

    can_capitalize = BooleanProperty(True)
    _capitalizing = BooleanProperty(False)

    def _get_text(self):
        if self._capitalizing:
            return self._text.upper()
        return self._text

    def _set_text(self, value):
        self._text = value

    _text = StringProperty()
    text = AliasProperty(_get_text, _set_text, bind=["_text", "_capitalizing"])

    theme_text_color = OptionProperty(
        None,
        allownone=True,
        options=[
            "Primary",
            "Secondary",
            "Hint",
            "Error",
            "Custom",
            "ContrastParentBackground",
        ],
    )

    text_color = ListProperty(None, allownone=True)

    parent_background = ListProperty(None, allownone=True)

    _currently_bound_property = {}

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(
            font_style=self.update_font_style,
            can_capitalize=self.update_font_style,
        )
        self.on_theme_text_color(None, self.theme_text_color)
        self.update_font_style()
        self.on_opposite_colors(None, self.opposite_colors)

    def update_font_style(self, *args):
        font_info = self.theme_cls.font_styles[self.font_style]
        self.font_name = font_info[0]
        self.font_size = sp(font_info[1])
        if font_info[2] and self.can_capitalize:
            self._capitalizing = True
        else:
            self._capitalizing = False
        # TODO: Add letter spacing change
        # self.letter_spacing = font_info[3]

    def on_theme_text_color(self, instance, value):
        t = self.theme_cls
        op = self.opposite_colors
        setter = self.setter("color")
        t.unbind(**self._currently_bound_property)
        attr_name = {
            "Primary": "text_color" if not op else "opposite_text_color",
            "Secondary": "secondary_text_color"
            if not op else "opposite_secondary_text_color",
            "Hint": "disabled_hint_text_color"
            if not op else "opposite_disabled_hint_text_color",
            "Error": "error_color",
        }.get(value, None)
        if attr_name:
            c = {attr_name: setter}
            t.bind(**c)
            self._currently_bound_property = c
            self.color = getattr(t, attr_name)
        else:
            # 'Custom' and 'ContrastParentBackground' lead here, as well as the
            # generic None value it's not yet been set
            if value == "Custom" and self.text_color:
                self.color = self.text_color
            elif value == "ContrastParentBackground" and self.parent_background:
                self.color = get_contrast_text_color(self.parent_background)
            else:
                self.color = [0, 0, 0, 1]

    def on_text_color(self, *args):
        if self.theme_text_color == "Custom":
            self.color = self.text_color

    def on_opposite_colors(self, instance, value):
        self.on_theme_text_color(self, self.theme_text_color)
예제 #8
0
class ButtonBehavior(object):
    '''Button behavior.

    :Events:
        `on_press`
            Fired when the button is pressed.
        `on_release`
            Fired when the button is released (i.e. the touch/click that
            pressed the button goes away).
    '''

    state = OptionProperty('normal', options=('normal', 'down'))
    '''State of the button, must be one of 'normal' or 'down'.
    The state is 'down' only when the button is currently touched/clicked,
    otherwise 'normal'.

    :attr:`state` is an :class:`~kivy.properties.OptionProperty`.
    '''


    def __init__(self, **kwargs):
        self.register_event_type('on_press')
        self.register_event_type('on_release')
        super(ButtonBehavior, self).__init__(**kwargs)

    def _do_press(self):
        self.state = 'down'

    def _do_release(self):
        self.state = 'normal'

    def on_touch_down(self, touch):
        if self in touch.ud:
            if isinstance(self, LogBehavior):
                log_manager = self.log_manager
                if log_manager.do_logging:
                    if isinstance(self, CheckBox):
                        touch_id = touch.ud['log_id']
                        log_manager.log_interface.set_entry(
                            'touches', touch_id, 
                            'checkbox_pressed_down', self.state, 
                            do_timestamp=True)
                    else:
                        touch_id = touch.ud['log_id']
                        log_manager.log_interface.set_entry(
                            'touches', touch_id, 
                            'button_pressed', self.text, do_timestamp=True)
            self._do_press()
            self.dispatch('on_press')
        return super(ButtonBehavior, self).on_touch_down(touch)

    def on_touch_move(self, touch):
        return super(ButtonBehavior, self).on_touch_move(touch)

    def on_touch_up(self, touch):
        if self in touch.ud:  
            if isinstance(self, LogBehavior):
                log_manager = self.log_manager
                if log_manager.do_logging:
                    if isinstance(self, CheckBox):
                        touch_id = touch.ud['log_id']
                        log_manager.log_interface.set_entry(
                            'touches', touch_id, 
                            'checkbox_released', self.state, 
                            do_timestamp=True)
                    else:
                        touch_id = touch.ud['log_id']
                        log_manager.log_interface.set_entry(
                            'touches', touch_id, 'button_released', 
                            self.text, do_timestamp=True)
            self._do_release()
            self.dispatch('on_release')
        return super(ButtonBehavior, self).on_touch_up(touch)

    def on_press(self):
        pass

    def on_release(self):
        pass

    def trigger_action(self, duration=0.1):
        '''Trigger whatever action(s) have been bound to the button by calling
        both the on_press and on_release callbacks.

        This simulates a quick button press without using any touch events.

        Duration is the length of the press in seconds. Pass 0 if you want
        the action to happen instantly.

        .. versionadded:: 1.8.0
        '''
        self._do_press()
        self.dispatch('on_press')

        def trigger_release(dt):
            self._do_release()
            self.dispatch('on_release')
        if not duration:
            trigger_release(0)
        else:
            Clock.schedule_once(trigger_release, duration)
예제 #9
0
class MDDropdownMenu(ThemableBehavior, FloatLayout):
    """
    :Events:
        :attr:`on_enter`
            Call when mouse enter the bbox of item menu.
        :attr:`on_leave`
            Call when the mouse exit the item menu.
        :attr:`on_dismiss`
            Call when closes menu.
        :attr:`on_release`
            The method that will be called when you click menu items.
    """

    selected_color = ListProperty()
    """Custom color (``rgba`` format) for list item when hover behavior occurs.

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

    items = ListProperty()
    """
    See :attr:`~kivy.uix.recycleview.RecycleView.data`.

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

    width_mult = NumericProperty(1)
    """
    This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.

    :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `1`.
    """

    max_height = NumericProperty()
    """
    The menu will grow no bigger than this number. Set to 0 for no limit.

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

    border_margin = NumericProperty("4dp")
    """
    Margin between Window border and menu.

    :attr:`border_margin` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `4dp`.
    """

    ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
    """
    Where the menu will grow vertically to when opening. Set to None to let
    the widget pick for you. Available options are: `'up'`, `'down'`.

    :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    hor_growth = OptionProperty(None, allownone=True, options=["left", "right"])
    """
    Where the menu will grow horizontally to when opening. Set to None to let
    the widget pick for you. Available options are: `'left'`, `'right'`.

    :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    background_color = ListProperty()
    """
    Color of the background of the menu.

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

    opening_transition = StringProperty("out_cubic")
    """
    Type of animation for opening a menu window.

    :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'out_cubic'`.
    """

    opening_time = NumericProperty(0.2)
    """
    Menu window opening animation time.

    :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    caller = ObjectProperty()
    """
    The widget object that caller the menu window.

    :attr:`caller` is a :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    position = OptionProperty("auto", options=["auto", "center", "bottom"])
    """
    Menu window position relative to parent element.
    Available options are: `'auto'`, `'center'`, `'bottom'`.

    :attr:`position` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'auto'`.
    """

    radius = NumericProperty(7)
    """
    Menu radius.

    :attr:`radius` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `'7'`.
    """

    _start_coords = []
    _calculate_complete = False
    _calculate_process = False

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Window.bind(on_resize=self.check_position_caller)
        Window.bind(on_maximize=self.set_menu_properties)
        Window.bind(on_restore=self.set_menu_properties)
        self.register_event_type("on_dismiss")
        self.register_event_type("on_enter")
        self.register_event_type("on_leave")
        self.register_event_type("on_release")
        self.menu = self.ids.md_menu
        self.target_height = 0

    def check_position_caller(self, instance, width, height):
        self.set_menu_properties(0)

    def set_bg_color_items(self, instance_selected_item):
        """Called when a Hover Behavior event occurs for a list item.

        :type instance_selected_item: <kivymd.uix.menu.MDMenuItemIcon object>
        """

        if self.selected_color:
            for item in self.menu.ids.box.children:
                if item is not instance_selected_item:
                    item.bg_color = (0, 0, 0, 0)
                else:
                    instance_selected_item.bg_color = self.selected_color

    def create_menu_items(self):
        """Creates menu items."""

        for data in self.items:
            item = MDMenuItemIcon(
                text=data.get("text", ""),
                divider=data.get("divider", "Full"),
                _txt_top_pad=data.get("top_pad", "20dp"),
                _txt_bot_pad=data.get("bot_pad", "20dp"),
            )
            # Set height item.
            if data.get("height", ""):
                item.height = data.get("height")
            # Remove left icon.
            if not data.get("icon"):
                item.remove_widget(item.ids._left_container)
                item._txt_left_pad = data.get("left_pad", "32dp")
            # Set left icon.
            else:
                item.icon = data.get("icon", "")
            item.bind(on_release=lambda x=item: self.dispatch("on_release", x))
            right_content_cls = data.get("right_content_cls", None)
            # Set right content.
            if isinstance(right_content_cls, RightContent):
                item.ids._right_container.width = right_content_cls.width + dp(
                    20
                )
                item.ids._right_container.padding = ("10dp", 0, 0, 0)
                item.add_widget(right_content_cls)
            else:
                if "_right_container" in item.ids:
                    item.ids._right_container.width = 0
            self.menu.ids.box.add_widget(item)

    def set_menu_properties(self, interval=0):
        """Sets the size and position for the menu window."""

        if self.caller:
            if not self.menu.ids.box.children:
                self.create_menu_items()
            # We need to pick a starting point, see how big we need to be,
            # and where to grow to.
            self._start_coords = self.caller.to_window(
                self.caller.center_x, self.caller.center_y
            )
            self.target_width = self.width_mult * m_res.STANDARD_INCREMENT

            # If we're wider than the Window...
            if self.target_width > Window.width:
                # ...reduce our multiplier to max allowed.
                self.target_width = (
                    int(Window.width / m_res.STANDARD_INCREMENT)
                    * m_res.STANDARD_INCREMENT
                )

            # Set the target_height of the menu depending on the size of
            # each MDMenuItem or MDMenuItemIcon
            self.target_height = 0
            for item in self.menu.ids.box.children:
                self.target_height += item.height

            # If we're over max_height...
            if 0 < self.max_height < self.target_height:
                self.target_height = self.max_height

            # Establish vertical growth direction.
            if self.ver_growth is not None:
                ver_growth = self.ver_growth
            else:
                # If there's enough space below us:
                if (
                    self.target_height
                    <= self._start_coords[1] - self.border_margin
                ):
                    ver_growth = "down"
                # if there's enough space above us:
                elif (
                    self.target_height
                    < Window.height - self._start_coords[1] - self.border_margin
                ):
                    ver_growth = "up"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # If there"s more space below us:
                    if (
                        self._start_coords[1]
                        >= Window.height - self._start_coords[1]
                    ):
                        ver_growth = "down"
                        self.target_height = (
                            self._start_coords[1] - self.border_margin
                        )
                    # If there's more space above us:
                    else:
                        ver_growth = "up"
                        self.target_height = (
                            Window.height
                            - self._start_coords[1]
                            - self.border_margin
                        )

            if self.hor_growth is not None:
                hor_growth = self.hor_growth
            else:
                # If there's enough space to the right:
                if (
                    self.target_width
                    <= Window.width - self._start_coords[0] - self.border_margin
                ):
                    hor_growth = "right"
                # if there's enough space to the left:
                elif (
                    self.target_width
                    < self._start_coords[0] - self.border_margin
                ):
                    hor_growth = "left"
                # Otherwise, let's pick the one with more space and adjust ourselves.
                else:
                    # if there"s more space to the right:
                    if (
                        Window.width - self._start_coords[0]
                        >= self._start_coords[0]
                    ):
                        hor_growth = "right"
                        self.target_width = (
                            Window.width
                            - self._start_coords[0]
                            - self.border_margin
                        )
                    # if there"s more space to the left:
                    else:
                        hor_growth = "left"
                        self.target_width = (
                            self._start_coords[0] - self.border_margin
                        )

            if ver_growth == "down":
                self.tar_y = self._start_coords[1] - self.target_height
            else:  # should always be "up"
                self.tar_y = self._start_coords[1]

            if hor_growth == "right":
                self.tar_x = self._start_coords[0]
            else:  # should always be "left"
                self.tar_x = self._start_coords[0] - self.target_width
            self._calculate_complete = True

    def open(self):
        """Animate the opening of a menu window."""

        def open(interval):
            if not self._calculate_complete:
                return
            if self.position == "auto":
                self.menu.pos = self._start_coords
                anim = Animation(
                    x=self.tar_x,
                    y=self.tar_y,
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
                anim.start(self.menu)
            else:
                if self.position == "center":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self._start_coords[1] - self.target_height / 2,
                    )
                elif self.position == "bottom":
                    self.menu.pos = (
                        self._start_coords[0] - self.target_width / 2,
                        self.caller.pos[1] - self.target_height,
                    )
                anim = Animation(
                    width=self.target_width,
                    height=self.target_height,
                    duration=self.opening_time,
                    opacity=1,
                    transition=self.opening_transition,
                )
            anim.start(self.menu)
            Window.add_widget(self)
            Clock.unschedule(open)
            self._calculate_process = False

        self.set_menu_properties()
        if not self._calculate_process:
            self._calculate_process = True
            Clock.schedule_interval(open, 0)

    def on_touch_down(self, touch):
        if not self.menu.collide_point(*touch.pos):
            self.dispatch("on_dismiss")
            return True
        super().on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super().on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        return True

    def on_enter(self, instance):
        """Call when mouse enter the bbox of the item of menu."""

    def on_leave(self, instance):
        """Call when the mouse exit the item of menu."""

    def on_release(self, *args):
        """The method that will be called when you click menu items."""

    def on_dismiss(self):
        """Called when the menu is closed."""

        Window.remove_widget(self)
        self.menu.width = 0
        self.menu.height = 0
        self.menu.opacity = 0

    def dismiss(self):
        """Closes the menu."""

        self.on_dismiss()
예제 #10
0
파일: slider.py 프로젝트: DGEDGE21/Marco
class Slider(Widget):
    """Class for creating a Slider widget.

    Check module documentation for more details.
    """

    value = NumericProperty(0.)
    '''Current value used for the slider.

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

    min = NumericProperty(0.)
    '''Minimum value allowed for :attr:`value`.

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

    max = NumericProperty(100.)
    '''Maximum value allowed for :attr:`value`.

    :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to
    100.'''

    padding = NumericProperty('16sp')
    '''Padding of the slider. The padding is used for graphical representation
    and interaction. It prevents the cursor from going out of the bounds of the
    slider bounding box.

    By default, padding is 16sp. The range of the slider is reduced from
    padding \*2 on the screen. It allows drawing the default cursor of 32sp
    width without having the cursor go out of the widget.

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

    orientation = OptionProperty('horizontal', options=(
        'vertical', 'horizontal'))
    '''Orientation of the slider.

    :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'.
    '''

    range = ReferenceListProperty(min, max)
    '''Range of the slider in the format (minimum value, maximum value)::

        >>> slider = Slider(min=10, max=80)
        >>> slider.range
        [10, 80]
        >>> slider.range = (20, 100)
        >>> slider.min
        20
        >>> slider.max
        100

    :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`min`, :attr:`max`) properties.
    '''

    step = BoundedNumericProperty(0, min=0)
    '''Step size of the slider.

    .. versionadded:: 1.4.0

    Determines the size of each interval or step the slider takes between
    min and max. If the value range can't be evenly divisible by step the
    last step will be capped by slider.max

    :attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 1.'''

    background_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background')
    """Background of the slider used in the horizontal orientation.

    .. versionadded:: 1.10.0

    :attr:`background_horizontal` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/sliderh_background`.
    """

    background_disabled_horizontal = StringProperty(
        'atlas://data/images/defaulttheme/sliderh_background_disabled')
    """Background of the disabled slider used in the horizontal orientation.

    .. versionadded:: 1.10.0

    :attr:`background_disabled_horizontal` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    `atlas://data/images/defaulttheme/sliderh_background_disabled`.
    """

    background_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background')
    """Background of the slider used in the vertical orientation.

    .. versionadded:: 1.10.0

    :attr:`background_vertical` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/sliderv_background`.
    """

    background_disabled_vertical = StringProperty(
        'atlas://data/images/defaulttheme/sliderv_background_disabled')
    """Background of the disabled slider used in the vertical orientation.

    .. versionadded:: 1.10.0

    :attr:`background_disabled_vertical` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    `atlas://data/images/defaulttheme/sliderv_background_disabled`.
    """

    background_width = NumericProperty('36sp')
    """Slider's background's width (thickness), used in both horizontal
    and vertical orientations.

    .. versionadded 1.10.0

    :attr:`background_width` is a
    :class:`~kivy.properties.NumericProperty` and defaults to 36sp.
    """

    cursor_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor')
    """Path of the image used to draw the slider cursor.

    .. versionadded 1.10.0

    :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/slider_cursor`.
    """

    cursor_disabled_image = StringProperty(
        'atlas://data/images/defaulttheme/slider_cursor_disabled')
    """Path of the image used to draw the disabled slider cursor.

    .. versionadded 1.10.0

    :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to `atlas://data/images/defaulttheme/slider_cursor_disabled`.
    """

    cursor_width = NumericProperty('32sp')
    """Width of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 32sp.
    """

    cursor_height = NumericProperty('32sp')
    """Height of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_height` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 32sp.
    """

    cursor_size = ReferenceListProperty(cursor_width, cursor_height)
    """Size of the cursor image.

    .. versionadded 1.10.0

    :attr:`cursor_size` is a :class:`~kivy.properties.ReferenceListProperty`
    of (:attr:`cursor_width`, :attr:`cursor_height`) properties.
    """

    border_horizontal = ListProperty([0, 18, 0, 18])
    """Border used to draw the slider background in horizontal orientation.

    .. versionadded 1.10.0

    :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
    and defaults to [0, 18, 0, 18].
    """

    border_vertical = ListProperty([18, 0, 18, 0])
    """Border used to draw the slider background in vertical orientation.

    .. versionadded 1.10.0

    :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty`
    and defaults to [18, 0, 18, 0].
    """

    value_track = BooleanProperty(False)
    """Decides if slider should draw the line indicating the
    space between :attr:`min` and :attr:`value` properties values.

    .. versionadded 1.10.0

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

    value_track_color = ListProperty([1, 1, 1, 1])
    """Color of the :attr:`value_line` in rgba format.

    .. versionadded 1.10.0

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

    value_track_width = NumericProperty('3dp')
    """Width of the track line.

    .. versionadded 1.10.0

    :attr:`value_track_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to 3dp.
    """

    sensitivity = OptionProperty('all', options=('all', 'handle'))
    """Whether the touch collides with the whole body of the widget
    or with the slider handle part only.

    .. versionadded:: 1.10.1

    :attr:`sensitivity` is a :class:`~kivy.properties.OptionProperty`
    and defaults to 'all'. Can take a value of 'all' or 'handle'.
    """

    # The following two methods constrain the slider's value
    # to range(min,max). Otherwise it may happen that self.value < self.min
    # at init.

    def on_min(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def on_max(self, *largs):
        self.value = min(self.max, max(self.min, self.value))

    def get_norm_value(self):
        vmin = self.min
        d = self.max - vmin
        if d == 0:
            return 0
        return (self.value - vmin) / float(d)

    def set_norm_value(self, value):
        vmin = self.min
        vmax = self.max
        step = self.step
        val = min(value * (vmax - vmin) + vmin, vmax)
        if step == 0:
            self.value = val
        else:
            self.value = min(round((val - vmin) / step) * step + vmin,
                             vmax)
    value_normalized = AliasProperty(get_norm_value, set_norm_value,
                                     bind=('value', 'min', 'max', 'step'))
    '''Normalized value inside the :attr:`range` (min/max) to 0-1 range::

        >>> slider = Slider(value=50, min=0, max=100)
        >>> slider.value
        50
        >>> slider.value_normalized
        0.5
        >>> slider.value = 0
        >>> slider.value_normalized
        0
        >>> slider.value = 100
        >>> slider.value_normalized
        1

    You can also use it for setting the real value without knowing the minimum
    and maximum::

        >>> slider = Slider(min=0, max=200)
        >>> slider.value_normalized = .5
        >>> slider.value
        100
        >>> slider.value_normalized = 1.
        >>> slider.value
        200

    :attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def get_value_pos(self):
        padding = self.padding
        x = self.x
        y = self.y
        nval = self.value_normalized
        if self.orientation == 'horizontal':
            return (x + padding + nval * (self.width - 2 * padding), y)
        else:
            return (x, y + padding + nval * (self.height - 2 * padding))

    def set_value_pos(self, pos):
        padding = self.padding
        x = min(self.right - padding, max(pos[0], self.x + padding))
        y = min(self.top - padding, max(pos[1], self.y + padding))
        if self.orientation == 'horizontal':
            if self.width == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (x - self.x - padding
                                         ) / float(self.width - 2 * padding)
        else:
            if self.height == 0:
                self.value_normalized = 0
            else:
                self.value_normalized = (y - self.y - padding
                                         ) / float(self.height - 2 * padding)
    value_pos = AliasProperty(get_value_pos, set_value_pos,
                              bind=('x', 'y', 'width', 'height', 'min',
                                    'max', 'value_normalized', 'orientation'))
    '''Position of the internal cursor, based on the normalized value.

    :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def on_touch_down(self, touch):
        if self.disabled or not self.collide_point(*touch.pos):
            return
        if touch.is_mouse_scrolling:
            if 'down' in touch.button or 'left' in touch.button:
                if self.step:
                    self.value = min(self.max, self.value + self.step)
                else:
                    self.value = min(
                        self.max,
                        self.value + (self.max - self.min) / 20)
            if 'up' in touch.button or 'right' in touch.button:
                if self.step:
                    self.value = max(self.min, self.value - self.step)
                else:
                    self.value = max(
                        self.min,
                        self.value - (self.max - self.min) / 20)
        elif self.sensitivity == 'handle':
            if self.children[0].collide_point(*touch.pos):
                touch.grab(self)
        else:
            touch.grab(self)
            self.value_pos = touch.pos
        return True

    def on_touch_move(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True

    def on_touch_up(self, touch):
        if touch.grab_current == self:
            self.value_pos = touch.pos
            return True
예제 #11
0
class Bubble(GridLayout):
    '''Bubble class. See module documentation for more information.
    '''

    background_color = ListProperty([1, 1, 1, 1])
    '''Background color, in the format (r, g, b, a). To use it you have to set
    either :attr:`background_image` or :attr:`arrow_image` first.

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

    border = ListProperty([16, 16, 16, 16])
    '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
    graphics instruction. Used with the :attr:`background_image`.
    It should be used when using custom backgrounds.

    It must be a list of 4 values: (bottom, right, top, left). Read the
    BorderImage instructions for more information about how to use it.

    :attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults to
    (16, 16, 16, 16)
    '''

    background_image = StringProperty(
        'atlas://data/images/defaulttheme/bubble')
    '''Background image of the bubble.

    :attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/bubble'.
    '''

    arrow_image = StringProperty(
        'atlas://data/images/defaulttheme/bubble_arrow')
    ''' Image of the arrow pointing to the bubble.

    :attr:`arrow_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/bubble_arrow'.
    '''

    show_arrow = BooleanProperty(True)
    ''' Indicates whether to show arrow.

    .. versionadded:: 1.8.0

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

    arrow_pos = OptionProperty('bottom_mid', options=(
        'left_top', 'left_mid', 'left_bottom', 'top_left', 'top_mid',
        'top_right', 'right_top', 'right_mid', 'right_bottom',
        'bottom_left', 'bottom_mid', 'bottom_right'))
    '''Specifies the position of the arrow relative to the bubble.
    Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right
    right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right.

    :attr:`arrow_pos` is a :class:`~kivy.properties.OptionProperty` and
    defaults to 'bottom_mid'.
    '''

    content = ObjectProperty(None)
    '''This is the object where the main content of the bubble is held.

    :attr:`content` is a :class:`~kivy.properties.ObjectProperty` and
    defaults to 'None'.
    '''

    orientation = OptionProperty('horizontal',
                                 options=('horizontal', 'vertical'))
    '''This specifies the manner in which the children inside bubble
    are arranged. Can be one of 'vertical' or 'horizontal'.

    :attr:`orientation` is a :class:`~kivy.properties.OptionProperty` and
    defaults to 'horizontal'.
    '''

    limit_to = ObjectProperty(None, allownone=True)
    '''Specifies the widget to which the bubbles position is restricted.

    .. versionadded:: 1.6.0

    :attr:`limit_to` is a :class:`~kivy.properties.ObjectProperty` and
    defaults to 'None'.
    '''

    border_auto_scale = OptionProperty(
        'both_lower',
        options=[
            'off', 'both', 'x_only', 'y_only', 'y_full_x_lower',
            'x_full_y_lower', 'both_lower'
        ]
    )
    '''Specifies the :attr:`kivy.graphics.BorderImage.auto_scale`
    value on the background BorderImage.

    .. versionadded:: 1.11.0

    :attr:`border_auto_scale` is a
    :class:`~kivy.properties.OptionProperty` and defaults to
    'both_lower'.
    '''

    def __init__(self, **kwargs):
        self._prev_arrow_pos = None
        self._arrow_layout = BoxLayout()
        self._bk_img = Image(
            source=self.background_image, allow_stretch=True,
            keep_ratio=False, color=self.background_color)
        self.background_texture = self._bk_img.texture
        self._arrow_img = Image(source=self.arrow_image,
                                allow_stretch=True,
                                color=self.background_color)
        self.content = content = BubbleContent(parent=self)
        super(Bubble, self).__init__(**kwargs)
        content.parent = None
        self.add_widget(content)
        self.on_arrow_pos()

    def add_widget(self, *l):
        content = self.content
        if content is None:
            return
        if l[0] == content or l[0] == self._arrow_img \
                or l[0] == self._arrow_layout:
            super(Bubble, self).add_widget(*l)
        else:
            content.add_widget(*l)

    def remove_widget(self, *l):
        content = self.content
        if not content:
            return
        if l[0] == content or l[0] == self._arrow_img \
                or l[0] == self._arrow_layout:
            super(Bubble, self).remove_widget(*l)
        else:
            content.remove_widget(l[0])

    def clear_widgets(self, **kwargs):
        content = self.content
        if not content:
            return
        if kwargs.get('do_super', False):
            super(Bubble, self).clear_widgets()
        else:
            content.clear_widgets()

    def on_show_arrow(self, instance, value):
        self._arrow_img.opacity = int(value)

    def on_parent(self, instance, value):
        Clock.schedule_once(self._update_arrow)

    def on_pos(self, instance, pos):
        lt = self.limit_to

        if lt:
            self.limit_to = None
            if lt is EventLoop.window:
                x = y = 0
                top = lt.height
                right = lt.width
            else:
                x, y = lt.x, lt.y
                top, right = lt.top, lt.right

            self.x = max(self.x, x)
            self.right = min(self.right, right)
            self.top = min(self.top, top)
            self.y = max(self.y, y)
            self.limit_to = lt

    def on_background_image(self, *l):
        self._bk_img.source = self.background_image

    def on_background_color(self, *l):
        if self.content is None:
            return
        self._arrow_img.color = self._bk_img.color = self.background_color

    def on_orientation(self, *l):
        content = self.content
        if not content:
            return
        if self.orientation[0] == 'v':
            content.cols = 1
            content.rows = 99
        else:
            content.cols = 99
            content.rows = 1

    def on_arrow_image(self, *l):
        self._arrow_img.source = self.arrow_image

    def on_arrow_pos(self, *l):
        self_content = self.content
        if not self_content:
            Clock.schedule_once(self.on_arrow_pos)
            return
        if self_content not in self.children:
            Clock.schedule_once(self.on_arrow_pos)
            return
        self_arrow_pos = self.arrow_pos
        if self._prev_arrow_pos == self_arrow_pos:
            return
        self._prev_arrow_pos = self_arrow_pos

        self_arrow_layout = self._arrow_layout
        self_arrow_layout.clear_widgets()
        self_arrow_img = self._arrow_img
        self._sctr = self._arrow_img
        self.clear_widgets(do_super=True)
        self_content.parent = None

        self_arrow_img.size_hint = (1, None)
        self_arrow_img.height = dp(self_arrow_img.texture_size[1])
        self_arrow_img.pos = 0, 0
        widget_list = []
        arrow_list = []
        parent = self_arrow_img.parent
        if parent:
            parent.remove_widget(self_arrow_img)

        if self_arrow_pos[0] == 'b' or self_arrow_pos[0] == 't':
            self.cols = 1
            self.rows = 3
            self_arrow_layout.orientation = 'horizontal'
            self_arrow_img.width = self.width / 3
            self_arrow_layout.size_hint = (1, None)
            self_arrow_layout.height = self_arrow_img.height
            if self_arrow_pos[0] == 'b':
                if self_arrow_pos == 'bottom_mid':
                    widget_list = (self_content, self_arrow_img)
                else:
                    if self_arrow_pos == 'bottom_left':
                        arrow_list = (self_arrow_img, Widget(), Widget())
                    elif self_arrow_pos == 'bottom_right':
                        # add two dummy widgets
                        arrow_list = (Widget(), Widget(), self_arrow_img)
                    widget_list = (self_content, self_arrow_layout)
            else:
                sctr = Scatter(do_translation=False,
                               rotation=180,
                               do_rotation=False,
                               do_scale=False,
                               size_hint=(None, None),
                               size=self_arrow_img.size)
                sctr.add_widget(self_arrow_img)
                if self_arrow_pos == 'top_mid':
                    # add two dummy widgets
                    arrow_list = (Widget(), sctr, Widget())
                elif self_arrow_pos == 'top_left':
                    arrow_list = (sctr, Widget(), Widget())
                elif self_arrow_pos == 'top_right':
                    arrow_list = (Widget(), Widget(), sctr)
                widget_list = (self_arrow_layout, self_content)
        elif self_arrow_pos[0] == 'l' or self_arrow_pos[0] == 'r':
            self.cols = 3
            self.rows = 1
            self_arrow_img.width = self.height / 3
            self_arrow_layout.orientation = 'vertical'
            self_arrow_layout.cols = 1
            self_arrow_layout.size_hint = (None, 1)
            self_arrow_layout.width = self_arrow_img.height

            rotation = -90 if self_arrow_pos[0] == 'l' else 90
            self._sctr = sctr = Scatter(do_translation=False,
                                        rotation=rotation,
                                        do_rotation=False,
                                        do_scale=False,
                                        size_hint=(None, None),
                                        size=(self_arrow_img.size))
            sctr.add_widget(self_arrow_img)

            if self_arrow_pos[-4:] == '_top':
                arrow_list = (Widget(size_hint=(1, .07)),
                              sctr, Widget(size_hint=(1, .3)))
            elif self_arrow_pos[-4:] == '_mid':
                arrow_list = (Widget(), sctr, Widget())
                Clock.schedule_once(self._update_arrow)
            elif self_arrow_pos[-7:] == '_bottom':
                arrow_list = (Widget(), Widget(), sctr)

            if self_arrow_pos[0] == 'l':
                widget_list = (self_arrow_layout, self_content)
            else:
                widget_list = (self_content, self_arrow_layout)

        # add widgets to arrow_layout
        add = self_arrow_layout.add_widget
        for widg in arrow_list:
            add(widg)

        # add widgets to self
        add = self.add_widget
        for widg in widget_list:
            add(widg)

    def _update_arrow(self, *dt):
        if self.arrow_pos in ('left_mid', 'right_mid'):
            self._sctr.center_y = self._arrow_layout.center_y
예제 #12
0
class ActionGroup(ActionItem, Button):
    '''
    ActionGroup class, see module documentation for more information.
    '''

    use_separator = BooleanProperty(False)
    '''
    Specifies whether to use a separator after/before this group or not.

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

    separator_image = StringProperty(
        'atlas://data/images/defaulttheme/separator')
    '''
    Background Image for an ActionSeparator in an ActionView.

    :attr:`separator_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/separator'.
    '''

    separator_width = NumericProperty(0)
    '''
    Width of the ActionSeparator in an ActionView.

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

    mode = OptionProperty('normal', options=('normal', 'spinner'))
    '''
    Sets the current mode of an ActionGroup. If mode is 'normal', the
    ActionGroups children will be displayed normally if there is enough
    space, otherwise they will be displayed in a spinner. If mode is
    'spinner', then the children will always be displayed in a spinner.

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

    dropdown_width = NumericProperty(0)
    '''
    If non zero, provides the width for the associated DropDown. This is
    useful when some items in the ActionGroup's DropDown are wider than usual
    and you don't want to make the ActionGroup widget itself wider.

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

    .. versionadded:: 1.10.0
    '''

    is_open = BooleanProperty(False)
    '''By default, the DropDown is not open. Set to True to open it.

    :attr:`is_open` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''
    def __init__(self, **kwargs):
        self.list_action_item = []
        self._list_overflow_items = []
        super(ActionGroup, self).__init__(**kwargs)

        # real is_open independent on public event
        self._is_open = False

        # create DropDown for the group and save its state to _is_open
        self._dropdown = ActionDropDown()
        self._dropdown.bind(attach_to=lambda ins, value: setattr(
            self, '_is_open', True if value else False))

        # put open/close responsibility to the event
        # - trigger dropdown opening when clicked
        self.bind(on_release=lambda *args: setattr(self, 'is_open', True))

        # - trigger dropdown closing when an item
        #   in the dropdown is clicked
        self._dropdown.bind(
            on_dismiss=lambda *args: setattr(self, 'is_open', False))

    def on_is_open(self, instance, value):
        # opening only if the DropDown is closed
        if value and not self._is_open:
            self._toggle_dropdown()
            self._dropdown.open(self)
            return

        # closing is_open manually, dismiss manually
        if not value and self._is_open:
            self._dropdown.dismiss()

    def _toggle_dropdown(self, *largs):
        ddn = self._dropdown
        ddn.size_hint_x = None

        # if container was set incorrectly and/or is missing
        if not ddn.container:
            return
        children = ddn.container.children

        # set DropDown width manually or if not set, then widen
        # the ActionGroup + DropDown until the widest child fits
        if children:
            ddn.width = self.dropdown_width or max(
                self.width, max(c.pack_width for c in children))
        else:
            ddn.width = self.width

        # set the DropDown children's height
        for item in children:
            item.size_hint_y = None
            item.height = max([self.height, sp(48)])

            # dismiss DropDown manually
            # auto_dismiss applies to touching outside of the DropDown
            item.bind(on_release=ddn.dismiss)

    def add_widget(self, widget, *args, **kwargs):
        '''
        .. versionchanged:: 2.1.0
            Renamed argument `item` to `widget`.
        '''
        # if adding ActionSeparator ('normal' mode,
        # everything visible), add it to the parent
        if isinstance(widget, ActionSeparator):
            super(ActionGroup, self).add_widget(widget, *args, **kwargs)
            return

        if not isinstance(widget, ActionItem):
            raise ActionBarException('ActionGroup only accepts ActionItem')

        self.list_action_item.append(widget)

    def show_group(self):
        # 'normal' mode, items can fit to the view
        self.clear_widgets()
        for item in self._list_overflow_items + self.list_action_item:
            item.inside_group = True
            self._dropdown.add_widget(item)

    def clear_widgets(self, *args, **kwargs):
        self._dropdown.clear_widgets(*args, **kwargs)
예제 #13
0
class Video(Image):
    '''Video class. See module documentation for more information.
    '''

    state = OptionProperty('stop', options=('play', 'pause', 'stop'))
    '''String, indicates whether to play, pause, or stop the video::

        # start playing the video at creation
        video = Video(source='movie.mkv', state='play')

        # create the video, and start later
        video = Video(source='movie.mkv')
        # and later
        video.state = 'play'

    :data:`state` is a :class:`~kivy.properties.OptionProperty`, default to
    'play'.
    '''

    play = BooleanProperty(False)
    '''
    .. deprecated:: 1.4.0
        Use :data:`state` instead.

    Boolean, indicates if the video is playing.
    You can start/stop the video by setting this property::

        # start playing the video at creation
        video = Video(source='movie.mkv', play=True)

        # create the video, and start later
        video = Video(source='movie.mkv')
        # and later
        video.play = True

    :data:`play` is a :class:`~kivy.properties.BooleanProperty`, default to
    False.

    .. deprecated:: 1.4.0
        Use :data:`state` instead.
    '''

    eos = BooleanProperty(False)
    '''Boolean, indicates if the video is done playing (reached end of stream).

    :data:`eos` is a :class:`~kivy.properties.BooleanProperty`, default to
    False.
    '''

    loaded = BooleanProperty(False)
    '''Boolean, indicates if the video is loaded and ready for playback.

    .. versionadded:: 1.6.0

    :data:`loaded` is a :class:`~kivy.properties.BooleanProperty`, default to
    False.
    '''

    position = NumericProperty(-1)
    '''Position of the video between 0 and :data:`duration`. The position
    defaults to -1, and is set to a real position when the video is loaded.

    :data:`position` is a :class:`~kivy.properties.NumericProperty`, default to
    -1.
    '''

    duration = NumericProperty(-1)
    '''Duration of the video. The duration defaults to -1, and is set to a real
    duration when the video is loaded.

    :data:`duration` is a :class:`~kivy.properties.NumericProperty`, default to
    -1.
    '''

    volume = NumericProperty(1.)
    '''Volume of the video, in the range 0-1. 1 means full volume, 0 means mute.

    :data:`volume` is a :class:`~kivy.properties.NumericProperty`, default to
    1.
    '''

    options = ObjectProperty({})
    '''Options to pass at Video core object creation.

    .. versionadded:: 1.0.4

    :data:`options` is a :class:`kivy.properties.ObjectProperty`, default to {}.
    '''
    def __init__(self, **kwargs):
        self._video = None
        super(Image, self).__init__(**kwargs)
        self.bind(source=self._trigger_video_load)

        if self.source:
            self._trigger_video_load()

    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 has no impact.

        .. versionadded:: 1.2.0
        '''
        if self._video is None:
            raise Exception('Video not loaded.')
        self._video.seek(percent)

    def _trigger_video_load(self, *largs):
        Clock.unschedule(self._do_video_load)
        Clock.schedule_once(self._do_video_load, -1)

    def _do_video_load(self, *largs):
        if CoreVideo is None:
            return
        if self._video:
            self._video.stop()
        if not self.source:
            self._video = None
            self.texture = None
        else:
            filename = self.source
            # FIXME make it extensible.
            if filename.split(':')[0] not in ('http', 'https', 'file', 'udp',
                                              'rtp', 'rtsp'):
                filename = resource_find(filename)
            self._video = CoreVideo(filename=filename, **self.options)
            self._video.volume = self.volume
            self._video.bind(on_load=self._on_load,
                             on_frame=self._on_video_frame,
                             on_eos=self._on_eos)
            if self.state == 'play' or self.play:
                self._video.play()
            self.duration = 1.
            self.position = 0.

    def on_play(self, instance, value):
        value = 'play' if value else 'stop'
        return self.on_state(instance, value)

    def on_state(self, instance, value):
        if not self._video:
            return
        if value == 'play':
            if self.eos:
                self._video.stop()
                self._video.position = 0.
                self._video.eos = False
            self.eos = False
            self._video.play()
        elif value == 'pause':
            self._video.pause()
        else:
            self._video.stop()
            self._video.position = 0
            self._video.eos = False

    def _on_video_frame(self, *largs):
        self.duration = self._video.duration
        self.position = self._video.position
        self.texture = self._video.texture
        self.canvas.ask_update()

    def _on_eos(self, *largs):
        if self._video.eos != 'loop':
            self.state = 'stop'
            self.eos = True

    def _on_load(self, *largs):
        self.loaded = True
        self._on_video_frame(largs)

    def on_volume(self, instance, value):
        if self._video:
            self._video.volume = value
예제 #14
0
class BaseListItem(ThemableBehavior, RectangularRippleBehavior, ButtonBehavior,
                   FloatLayout):
    '''Base class to all ListItems. Not supposed to be instantiated on its own.
    '''

    text = StringProperty()
    '''Text shown in the first line.

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

    text_color = ListProperty(None)
    ''' Text color used if theme_text_color is set to 'Custom' '''

    font_style = OptionProperty('Subhead',
                                options=[
                                    'Body1', 'Body2', 'Caption', 'Subhead',
                                    'Title', 'Headline', 'Display1',
                                    'Display2', 'Display3', 'Display4',
                                    'Button', 'Icon'
                                ])

    theme_text_color = StringProperty('Primary', allownone=True)
    ''' Theme text color for primary text '''

    secondary_text = StringProperty()
    '''Text shown in the second and potentially third line.

    The text will wrap into the third line if the ListItem's type is set to
    \'one-line\'. It can be forced into the third line by adding a \\n
    escape sequence.

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

    secondary_text_color = ListProperty(None)
    ''' Text color used for secondary text if secondary_theme_text_color 
    is set to 'Custom' '''

    secondary_theme_text_color = StringProperty('Secondary', allownone=True)
    ''' Theme text color for secondary primary text '''

    secondary_font_style = OptionProperty('Body1',
                                          options=[
                                              'Body1', 'Body2', 'Caption',
                                              'Subhead', 'Title', 'Headline',
                                              'Display1', 'Display2',
                                              'Display3', 'Display4', 'Button',
                                              'Icon'
                                          ])

    divider = OptionProperty('Full',
                             options=['Full', 'Inset', None],
                             allownone=True)

    _txt_left_pad = NumericProperty(dp(16))
    _txt_top_pad = NumericProperty()
    _txt_bot_pad = NumericProperty()
    _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS)
    _num_lines = 2
예제 #15
0
class MDTabbedPanel(TabbedPanelBase):
    """ A tab panel that is implemented by delegating all tabs
        to a ScreenManager.
    """
    # If tabs should fill space
    tab_width_mode = OptionProperty('stacked', options=['stacked', 'fixed'])

    # Where the tabs go
    tab_orientation = OptionProperty('top',
                                     options=['top'
                                              ])  # ,'left','bottom','right'])

    # How tabs are displayed
    tab_display_mode = OptionProperty('text', options=['text',
                                                       'icons'])  # ,'both'])
    _tab_display_height = DictProperty({
        'text': dp(46),
        'icons': dp(46),
        'both': dp(72)
    })

    # Tab background color (leave empty for theme color)
    tab_color = ListProperty([])

    # Tab text color in normal state (leave empty for theme color)
    tab_text_color = ListProperty([])

    # Tab text color in active state (leave empty for theme color)
    tab_text_color_active = ListProperty([])

    # Tab indicator color  (leave empty for theme color)
    tab_indicator_color = ListProperty([])

    # Tab bar bottom border color (leave empty for theme color)
    tab_border_color = ListProperty([])

    def __init__(self, **kwargs):
        super(MDTabbedPanel, self).__init__(**kwargs)
        self.index = 0
        self._refresh_tabs()

    def on_tab_width_mode(self, *args):
        self._refresh_tabs()

    def on_tab_display_mode(self, *args):
        self._refresh_tabs()

    def _refresh_tabs(self):
        """ Refresh all tabs """
        # if fixed width, use a box layout
        if not self.ids:
            return
        tab_bar = self.ids.tab_bar
        tab_bar.clear_widgets()
        tab_manager = self.ids.tab_manager
        for tab in tab_manager.screens:
            tab_header = MDTabHeader(tab=tab,
                                     panel=self,
                                     height=tab_bar.height)
            tab_bar.add_widget(tab_header)

    def add_widget(self, widget, **kwargs):
        """ Add tabs to the screen or the layout.
        :param widget: The widget to add.
        """
        if isinstance(widget, MDTab):
            self.index += 1
            if self.index == 1:
                self.previous_tab = widget
            widget.index = self.index
            widget.parent_widget = self
            self.ids.tab_manager.add_widget(widget)
            self._refresh_tabs()
        else:
            super(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)
예제 #16
0
class PopupBox(ModalView):
    '''
    :Events:
        `on_open`:
            Fired when the Popup is opened.
        `on_dismiss`:
            Fired when the Popup is closed. If the callback returns True, the
            dismiss will be canceled.
    '''

    title = StringProperty('No title')
    '''String that represents the title of the popup.

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

    title_size = NumericProperty('14sp')
    '''Represents the font size of the popup title.

    .. versionadded:: 1.6.0

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

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

    .. versionadded:: 1.9.0

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

    title_font = StringProperty('Roboto')
    '''Font used to render the title text.

    .. versionadded:: 1.9.0

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

    content = ObjectProperty(None)
    '''Content of the popup that is displayed just under the title.

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

    title_color = ListProperty([1, 1, 1, 1])
    '''Color used by the Title.

    .. versionadded:: 1.8.0

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

    separator_color = ListProperty([47 / 255., 167 / 255., 212 / 255., 1.])
    '''Color used by the separator between title and content.

    .. versionadded:: 1.1.0

    :attr:`separator_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [47 / 255., 167 / 255., 212 / 255., 1.]
    '''

    separator_height = NumericProperty('2dp')
    '''Height of the separator.

    .. versionadded:: 1.1.0

    :attr:`separator_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 2dp.
    '''

    # Internal properties used for graphical representation.

    _container = ObjectProperty(None)
예제 #17
0
class MDDropdownMenu(ThemableBehavior, BoxLayout):
    items = ListProperty()
    """See :attr:`~kivy.uix.recycleview.RecycleView.data`
    """

    width_mult = NumericProperty(1)
    """This number multiplied by the standard increment (56dp on mobile,
    64dp on desktop, determines the width of the menu items.

    If the resulting number were to be too big for the application Window,
    the multiplier will be adjusted for the biggest possible one.
    """

    max_height = NumericProperty()
    """The menu will grow no bigger than this number.

    Set to 0 for no limit. Defaults to 0.
    """

    border_margin = NumericProperty(dp(4))
    """Margin between Window border and menu
    """

    ver_growth = OptionProperty(None, allownone=True, options=["up", "down"])
    """Where the menu will grow vertically to when opening

    Set to None to let the widget pick for you. Defaults to None.
    """

    hor_growth = OptionProperty(None,
                                allownone=True,
                                options=["left", "right"])
    """Where the menu will grow horizontally to when opening

    Set to None to let the widget pick for you. Defaults to None.
    """

    background_color = ListProperty()
    """Color of the background of the menu
    """

    color_rectangle = ListProperty()
    """Color of the rectangle of the menu
    """

    width_rectangle = NumericProperty(2)
    """Width of the rectangle of the menu
    """

    _center = BooleanProperty(False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_dismiss")

        if not len(self.background_color):
            self.background_color = self.theme_cls.primary_color
        if not len(self.color_rectangle):
            self.color_rectangle = self.theme_cls.divider_color

    def open(self, *args):
        if self.parent:
            self.parent.remove_widget(self)
        Window.add_widget(self)
        Clock.schedule_once(lambda x: self.display_menu(args[0]), -1)

    def display_menu(self, caller):
        # We need to pick a starting point, see how big we need to be,
        # and where to grow to.
        c = caller.to_window(caller.center_x,
                             caller.center_y)  # Starting coords

        # TODO: ESTABLISH INITIAL TARGET SIZE ESTIMATE
        target_width = self.width_mult * m_res.STANDARD_INCREMENT
        # md_menu = self.ids.md_menu
        # opts = md_menu.layout_manager.view_opts
        # md_item = md_menu.view_adapter.get_view(1, md_menu.data[1],
        #                                         opts[1]['viewclass'])

        # If we're wider than the Window...
        if target_width > Window.width:
            # ...reduce our multiplier to max allowed.
            target_width = (int(Window.width / m_res.STANDARD_INCREMENT) *
                            m_res.STANDARD_INCREMENT)

        target_height = sum([dp(48) for i in self.items])
        # If we're over max_height...
        if 0 < self.max_height < target_height:
            target_height = self.max_height

        # ---ESTABLISH VERTICAL GROWTH DIRECTION---
        if self.ver_growth is not None:
            ver_growth = self.ver_growth
        else:
            # If there's enough space below us:
            if target_height <= c[1] - self.border_margin:
                ver_growth = "down"
            # if there's enough space above us:
            elif target_height < Window.height - c[1] - self.border_margin:
                ver_growth = "up"
            # otherwise, let's pick the one with more space and adjust ourselves
            else:
                # if there's more space below us:
                if c[1] >= Window.height - c[1]:
                    ver_growth = "down"
                    target_height = c[1] - self.border_margin
                # if there's more space above us:
                else:
                    ver_growth = "up"
                    target_height = Window.height - c[1] - self.border_margin

        if self.hor_growth is not None:
            hor_growth = self.hor_growth
        else:
            # If there's enough space to the right:
            if target_width <= Window.width - c[0] - self.border_margin:
                hor_growth = "right"
            # if there's enough space to the left:
            elif target_width < c[0] - self.border_margin:
                hor_growth = "left"
            # otherwise, let's pick the one with more space and adjust ourselves
            else:
                # if there's more space to the right:
                if Window.width - c[0] >= c[0]:
                    hor_growth = "right"
                    target_width = Window.width - c[0] - self.border_margin
                # if there's more space to the left:
                else:
                    hor_growth = "left"
                    target_width = c[0] - self.border_margin

        if ver_growth == "down":
            tar_y = c[1] - target_height
        else:  # should always be 'up'
            tar_y = c[1]

        if hor_growth == "right":
            tar_x = c[0]
        else:  # should always be 'left'
            tar_x = c[0] - target_width

        menu = self.ids.md_menu
        if not self._center:
            anim = Animation(
                x=tar_x,
                y=tar_y,
                width=target_width,
                height=target_height,
                duration=0.3,
                transition="out_quint",
            )
            menu.pos = c
            anim.start(menu)
        else:
            menu.width = target_width
            menu.height = target_height
            menu.x = caller.x - dp(15)
            menu.y = caller.y - menu.height / 2

            # TODO: Add the ability to set the list to the current user selection.
            """
            for data in menu.data:
                if data["text"] == caller.ids.label_item.text:
                    opts = menu.layout_manager.view_opts
                    item = menu.view_adapter.get_view(1, data, opts[1]["viewclass"])
                    # AttributeError: 'function' object has no attribute 'is_triggered'
                    # https://github.com/kivy/kivy/issues/5014
                    # Attempt to fix - https://github.com/Bakterija/log_fruit/blob/dev/src/app_modules/widgets/app_recycleview/recycleview.py#L25-L34
                    menu.scroll_to(item)
                    break
            """

    def on_touch_down(self, touch):
        if not self.ids.md_menu.collide_point(*touch.pos):
            self.dispatch("on_dismiss")
            return True
        super().on_touch_down(touch)
        return True

    def on_touch_move(self, touch):
        super().on_touch_move(touch)
        return True

    def on_touch_up(self, touch):
        super().on_touch_up(touch)
        return True

    def on_dismiss(self):
        Window.remove_widget(self)

    def dismiss(self):
        self.on_dismiss()
예제 #18
0
파일: list.py 프로젝트: RideTheSkyP/Notify
class BaseListItem(ThemableBehavior, RectangularRippleBehavior, ButtonBehavior,
                   FloatLayout):
    """
    Base class to all ListItems. Not supposed to be instantiated on its own.
    """

    text = StringProperty()
    """
    Text shown in the first line.

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

    text_color = ListProperty(None)
    """
    Text color in ``rgba`` format used if :attr:`~theme_text_color` is set
    to `'Custom'`.

    :attr:`text_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to `None`.
    """

    font_style = StringProperty("Subtitle1")
    """
    Text font style. See ``kivymd.font_definitions.py``.

    :attr:`font_style` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Subtitle1'`.
    """

    theme_text_color = StringProperty("Primary", allownone=True)
    """
    Theme text color in ``rgba`` format for primary text.

    :attr:`theme_text_color` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Primary'`.
    """

    secondary_text = StringProperty()
    """
    Text shown in the second line.

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

    tertiary_text = StringProperty()
    """
    The text is displayed on the third line.

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

    secondary_text_color = ListProperty(None)
    """
    Text color in ``rgba`` format used for secondary text
    if :attr:`~secondary_theme_text_color` is set to `'Custom'`.

    :attr:`secondary_text_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to `None`.
    """

    tertiary_text_color = ListProperty(None)
    """
    Text color in ``rgba`` format used for tertiary text
    if :attr:`~tertiary_theme_text_color` is set to 'Custom'.

    :attr:`tertiary_text_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to `None`.
    """

    secondary_theme_text_color = StringProperty("Secondary", allownone=True)
    """
    Theme text color for secondary text.

    :attr:`secondary_theme_text_color` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Secondary'`.
    """

    tertiary_theme_text_color = StringProperty("Secondary", allownone=True)
    """
    Theme text color for tertiary text.

    :attr:`tertiary_theme_text_color` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Secondary'`.
    """

    secondary_font_style = StringProperty("Body1")
    """
    Font style for secondary line. See ``kivymd.font_definitions.py``.

    :attr:`secondary_font_style` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Body1'`.
    """

    tertiary_font_style = StringProperty("Body1")
    """
    Font style for tertiary line. See ``kivymd.font_definitions.py``.

    :attr:`tertiary_font_style` is a :class:`~kivy.properties.StringProperty`
    and defaults to `'Body1'`.
    """

    divider = OptionProperty("Full",
                             options=["Full", "Inset", None],
                             allownone=True)
    """
    Divider mode. Available options are: `'Full'`, `'Inset'`
    and default to `'Full'`.

    :attr:`divider` is a :class:`~kivy.properties.OptionProperty`
    and defaults to `'Full'`.
    """

    bg_color = ListProperty()
    """
    Background color for menu item.

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

    _txt_left_pad = NumericProperty("16dp")
    _txt_top_pad = NumericProperty()
    _txt_bot_pad = NumericProperty()
    _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS)
    _num_lines = 3
    _no_ripple_effect = BooleanProperty(False)
예제 #19
0
class TabbedPanel(GridLayout):
    '''The TabbedPanel class. See module documentation for more information.
    '''

    background_color = ColorProperty([1, 1, 1, 1])
    '''Background color, in the format (r, g, b, a).

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

    .. versionchanged:: 2.0.0
        Changed from :class:`~kivy.properties.ListProperty` to
        :class:`~kivy.properties.ColorProperty`.
    '''

    border = ListProperty([16, 16, 16, 16])
    '''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
    graphics instruction, used itself for :attr:`background_image`.
    Can be changed for a custom background.

    It must be a list of four values: (bottom, right, top, left). Read the
    BorderImage instructions for more information.

    :attr:`border` is a :class:`~kivy.properties.ListProperty` and
    defaults to (16, 16, 16, 16)
    '''

    background_image = StringProperty('atlas://data/images/defaulttheme/tab')
    '''Background image of the main shared content object.

    :attr:`background_image` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/tab'.
    '''

    background_disabled_image = StringProperty(
        'atlas://data/images/defaulttheme/tab_disabled')
    '''Background image of the main shared content object when disabled.

    .. versionadded:: 1.8.0

    :attr:`background_disabled_image` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    'atlas://data/images/defaulttheme/tab'.
    '''

    strip_image = StringProperty(
        'atlas://data/images/defaulttheme/action_view')
    '''Background image of the tabbed strip.

    .. versionadded:: 1.8.0

    :attr:`strip_image` is a :class:`~kivy.properties.StringProperty`
    and defaults to a empty image.
    '''

    strip_border = ListProperty([4, 4, 4, 4])
    '''Border to be used on :attr:`strip_image`.

    .. versionadded:: 1.8.0

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

    _current_tab = ObjectProperty(None)

    def get_current_tab(self):
        return self._current_tab

    current_tab = AliasProperty(get_current_tab, None, bind=('_current_tab', ))
    '''Links to the currently selected or active tab.

    .. versionadded:: 1.4.0

    :attr:`current_tab` is an :class:`~kivy.AliasProperty`, read-only.
    '''

    tab_pos = OptionProperty(
        'top_left',
        options=('left_top', 'left_mid', 'left_bottom', 'top_left', 'top_mid',
                 'top_right', 'right_top', 'right_mid', 'right_bottom',
                 'bottom_left', 'bottom_mid', 'bottom_right'))
    '''Specifies the position of the tabs relative to the content.
    Can be one of: `left_top`, `left_mid`, `left_bottom`, `top_left`,
    `top_mid`, `top_right`, `right_top`, `right_mid`, `right_bottom`,
    `bottom_left`, `bottom_mid`, `bottom_right`.

    :attr:`tab_pos` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'top_left'.
    '''

    tab_height = NumericProperty('40dp')
    '''Specifies the height of the tab header.

    :attr:`tab_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 40.
    '''

    tab_width = NumericProperty('100dp', allownone=True)
    '''Specifies the width of the tab header.

    :attr:`tab_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 100.
    '''

    do_default_tab = BooleanProperty(True)
    '''Specifies whether a default_tab head is provided.

    .. versionadded:: 1.5.0

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

    default_tab_text = StringProperty('Default tab')
    '''Specifies the text displayed on the default tab header.

    :attr:`default_tab_text` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'default tab'.
    '''

    default_tab_cls = ObjectProperty(TabbedPanelHeader)
    '''Specifies the class to use for the styling of the default tab.

    .. versionadded:: 1.4.0

    .. warning::
        `default_tab_cls` should be subclassed from `TabbedPanelHeader`

    :attr:`default_tab_cls` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `TabbedPanelHeader`. If you set a string, the
    :class:`~kivy.factory.Factory` will be used to resolve the class.

    .. versionchanged:: 1.8.0
        The :class:`~kivy.factory.Factory` will resolve the class if a string
        is set.
    '''

    def get_tab_list(self):
        if self._tab_strip:
            return self._tab_strip.children
        return 1.

    tab_list = AliasProperty(get_tab_list, None)
    '''List of all the tab headers.

    :attr:`tab_list` is an :class:`~kivy.properties.AliasProperty` and is
    read-only.
    '''

    content = ObjectProperty(None)
    '''This is the object holding (current_tab's content is added to this)
    the content of the current tab. To Listen to the changes in the content
    of the current tab, you should bind to current_tabs `content` property.

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

    _default_tab = ObjectProperty(None, allow_none=True)

    def get_def_tab(self):
        return self._default_tab

    def set_def_tab(self, new_tab):
        if not issubclass(new_tab.__class__, TabbedPanelHeader):
            raise TabbedPanelException('`default_tab_class` should be\
                subclassed from `TabbedPanelHeader`')
        if self._default_tab == new_tab:
            return
        oltab = self._default_tab
        self._default_tab = new_tab
        self.remove_widget(oltab)
        self._original_tab = None
        self.switch_to(new_tab)
        new_tab.state = 'down'

    default_tab = AliasProperty(get_def_tab,
                                set_def_tab,
                                bind=('_default_tab', ))
    '''Holds the default tab.

    .. Note:: For convenience, the automatically provided default tab is
              deleted when you change default_tab to something else.
              As of 1.5.0, this behaviour has been extended to every
              `default_tab` for consistency and not just the automatically
              provided one.

    :attr:`default_tab` is an :class:`~kivy.properties.AliasProperty`.
    '''

    def get_def_tab_content(self):
        return self.default_tab.content

    def set_def_tab_content(self, *l):
        self.default_tab.content = l[0]

    default_tab_content = AliasProperty(get_def_tab_content,
                                        set_def_tab_content)
    '''Holds the default tab content.

    :attr:`default_tab_content` is an :class:`~kivy.properties.AliasProperty`.
    '''

    _update_top_ev = _update_tab_ev = _update_tabs_ev = None

    def __init__(self, **kwargs):
        # these variables need to be initialized before the kv lang is
        # processed setup the base layout for the tabbed panel
        self._childrens = []
        self._tab_layout = StripLayout(rows=1)
        self.rows = 1
        self._tab_strip = TabbedPanelStrip(tabbed_panel=self,
                                           rows=1,
                                           size_hint=(None, None),
                                           height=self.tab_height,
                                           width=self.tab_width)

        self._partial_update_scrollview = None
        self.content = TabbedPanelContent()
        self._current_tab = self._original_tab \
            = self._default_tab = TabbedPanelHeader()

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

        self.fbind('size', self._reposition_tabs)
        if not self.do_default_tab:
            Clock.schedule_once(self._switch_to_first_tab)
            return
        self._setup_default_tab()
        self.switch_to(self.default_tab)

    def switch_to(self, header, do_scroll=False):
        '''Switch to a specific panel header.

        .. versionchanged:: 1.10.0

        If used with `do_scroll=True`, it scrolls
        to the header's tab too.

        :meth:`switch_to` cannot be called from within the
        :class:`TabbedPanel` or its subclass' ``__init__`` method.
        If that is required, use the ``Clock`` to schedule it. See `discussion
        <https://github.com/kivy/kivy/issues/3493#issuecomment-121567969>`_
        for full example.
        '''
        header_content = header.content
        self._current_tab.state = 'normal'
        header.state = 'down'
        self._current_tab = header
        self.clear_widgets()
        if header_content is None:
            return
        # if content has a previous parent remove it from that parent
        parent = header_content.parent
        if parent:
            parent.remove_widget(header_content)
        self.add_widget(header_content)

        if do_scroll:
            tabs = self._tab_strip
            tabs.parent.scroll_to(header)

    def clear_tabs(self, *l):
        self_tabs = self._tab_strip
        self_tabs.clear_widgets()
        if self.do_default_tab:
            self_default_tab = self._default_tab
            self_tabs.add_widget(self_default_tab)
            self_tabs.width = self_default_tab.width
        self._reposition_tabs()

    def add_widget(self, widget, *args, **kwargs):
        content = self.content
        if content is None:
            return
        parent = widget.parent
        if parent:
            parent.remove_widget(widget)
        if widget in (content, self._tab_layout):
            super(TabbedPanel, self).add_widget(widget, *args, **kwargs)
        elif isinstance(widget, TabbedPanelHeader):
            self_tabs = self._tab_strip
            self_tabs.add_widget(widget, *args, **kwargs)
            widget.group = '__tab%r__' % self_tabs.uid
            self.on_tab_width()
        else:
            widget.pos_hint = {'x': 0, 'top': 1}
            self._childrens.append(widget)
            content.disabled = self.current_tab.disabled
            content.add_widget(widget, *args, **kwargs)

    def remove_widget(self, widget, *args, **kwargs):
        content = self.content
        if content is None:
            return
        if widget in (content, self._tab_layout):
            super(TabbedPanel, self).remove_widget(widget, *args, **kwargs)
        elif isinstance(widget, TabbedPanelHeader):
            if not (self.do_default_tab and widget is self._default_tab):
                self_tabs = self._tab_strip
                self_tabs.width -= widget.width
                self_tabs.remove_widget(widget)
                if widget.state == 'down' and self.do_default_tab:
                    self._default_tab.on_release()
                self._reposition_tabs()
            else:
                Logger.info('TabbedPanel: default tab! can\'t be removed.\n' +
                            'Change `default_tab` to a different tab.')
        else:
            if widget in self._childrens:
                self._childrens.remove(widget)
            if widget in content.children:
                content.remove_widget(widget, *args, **kwargs)

    def clear_widgets(self, *args, **kwargs):
        if self.content:
            self.content.clear_widgets(*args, **kwargs)

    def on_strip_image(self, instance, value):
        if not self._tab_layout:
            return
        self._tab_layout.background_image = value

    def on_strip_border(self, instance, value):
        if not self._tab_layout:
            return
        self._tab_layout.border = value

    def on_do_default_tab(self, instance, value):
        if not value:
            dft = self.default_tab
            if dft in self.tab_list:
                self.remove_widget(dft)
                self._switch_to_first_tab()
                self._default_tab = self._current_tab
        else:
            self._current_tab.state = 'normal'
            self._setup_default_tab()

    def on_default_tab_text(self, *args):
        self._default_tab.text = self.default_tab_text

    def on_tab_width(self, *l):
        ev = self._update_tab_ev
        if ev is None:
            ev = self._update_tab_ev = Clock.create_trigger(
                self._update_tab_width, 0)
        ev()

    def on_tab_height(self, *l):
        self._tab_layout.height = self._tab_strip.height = self.tab_height
        self._reposition_tabs()

    def on_tab_pos(self, *l):
        # ensure canvas
        self._reposition_tabs()

    def _setup_default_tab(self):
        if self._default_tab in self.tab_list:
            return
        content = self._default_tab.content
        _tabs = self._tab_strip
        cls = self.default_tab_cls

        if isinstance(cls, string_types):
            cls = Factory.get(cls)

        if not issubclass(cls, TabbedPanelHeader):
            raise TabbedPanelException('`default_tab_class` should be\
                subclassed from `TabbedPanelHeader`')

        # no need to instantiate if class is TabbedPanelHeader
        if cls != TabbedPanelHeader:
            self._current_tab = self._original_tab = self._default_tab = cls()

        default_tab = self.default_tab
        if self._original_tab == self.default_tab:
            default_tab.text = self.default_tab_text

        default_tab.height = self.tab_height
        default_tab.group = '__tab%r__' % _tabs.uid
        default_tab.state = 'down'
        default_tab.width = self.tab_width if self.tab_width else 100
        default_tab.content = content

        tl = self.tab_list
        if default_tab not in tl:
            _tabs.add_widget(default_tab, len(tl))

        if default_tab.content:
            self.clear_widgets()
            self.add_widget(self.default_tab.content)
        else:
            Clock.schedule_once(self._load_default_tab_content)
        self._current_tab = default_tab

    def _switch_to_first_tab(self, *l):
        ltl = len(self.tab_list) - 1
        if ltl > -1:
            self._current_tab = dt = self._original_tab \
                = self.tab_list[ltl]
            self.switch_to(dt)

    def _load_default_tab_content(self, dt):
        if self.default_tab:
            self.switch_to(self.default_tab)

    def _reposition_tabs(self, *l):
        ev = self._update_tabs_ev
        if ev is None:
            ev = self._update_tabs_ev = Clock.create_trigger(
                self._update_tabs, 0)
        ev()

    def _update_tabs(self, *l):
        self_content = self.content
        if not self_content:
            return
        # cache variables for faster access
        tab_pos = self.tab_pos
        tab_layout = self._tab_layout
        tab_layout.clear_widgets()
        scrl_v = ScrollView(size_hint=(None, 1), always_overscroll=False)
        tabs = self._tab_strip
        parent = tabs.parent
        if parent:
            parent.remove_widget(tabs)
        scrl_v.add_widget(tabs)
        scrl_v.pos = (0, 0)
        self_update_scrollview = self._update_scrollview

        # update scrlv width when tab width changes depends on tab_pos
        if self._partial_update_scrollview is not None:
            tabs.unbind(width=self._partial_update_scrollview)
        self._partial_update_scrollview = partial(self_update_scrollview,
                                                  scrl_v)
        tabs.bind(width=self._partial_update_scrollview)

        # remove all widgets from the tab_strip
        super(TabbedPanel, self).clear_widgets()
        tab_height = self.tab_height

        widget_list = []
        tab_list = []
        pos_letter = tab_pos[0]
        if pos_letter == 'b' or pos_letter == 't':
            # bottom or top positions
            # one col containing the tab_strip and the content
            self.cols = 1
            self.rows = 2
            # tab_layout contains the scrollview containing tabs and two blank
            # dummy widgets for spacing
            tab_layout.rows = 1
            tab_layout.cols = 3
            tab_layout.size_hint = (1, None)
            tab_layout.height = (tab_height + tab_layout.padding[1] +
                                 tab_layout.padding[3] + dp(2))
            self_update_scrollview(scrl_v)

            if pos_letter == 'b':
                # bottom
                if tab_pos == 'bottom_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                    widget_list = (self_content, tab_layout)
                else:
                    if tab_pos == 'bottom_left':
                        tab_list = (scrl_v, Widget(), Widget())
                    elif tab_pos == 'bottom_right':
                        # add two dummy widgets
                        tab_list = (Widget(), Widget(), scrl_v)
                    widget_list = (self_content, tab_layout)
            else:
                # top
                if tab_pos == 'top_mid':
                    tab_list = (Widget(), scrl_v, Widget())
                elif tab_pos == 'top_left':
                    tab_list = (scrl_v, Widget(), Widget())
                elif tab_pos == 'top_right':
                    tab_list = (Widget(), Widget(), scrl_v)
                widget_list = (tab_layout, self_content)
        elif pos_letter == 'l' or pos_letter == 'r':
            # left or right positions
            # one row containing the tab_strip and the content
            self.cols = 2
            self.rows = 1
            # tab_layout contains two blank dummy widgets for spacing
            # "vertically" and the scatter containing scrollview
            # containing tabs
            tab_layout.rows = 3
            tab_layout.cols = 1
            tab_layout.size_hint = (None, 1)
            tab_layout.width = tab_height
            scrl_v.height = tab_height
            self_update_scrollview(scrl_v)

            # rotate the scatter for vertical positions
            rotation = 90 if tab_pos[0] == 'l' else -90
            sctr = Scatter(do_translation=False,
                           rotation=rotation,
                           do_rotation=False,
                           do_scale=False,
                           size_hint=(None, None),
                           auto_bring_to_front=False,
                           size=scrl_v.size)
            sctr.add_widget(scrl_v)

            lentab_pos = len(tab_pos)

            # Update scatter's top when its pos changes.
            # Needed for repositioning scatter to the correct place after its
            # added to the parent. Use clock_schedule_once to ensure top is
            # calculated after the parent's pos on canvas has been calculated.
            # This is needed for when tab_pos changes to correctly position
            # scatter. Without clock.schedule_once the positions would look
            # fine but touch won't translate to the correct position

            if tab_pos[lentab_pos - 4:] == '_top':
                # on positions 'left_top' and 'right_top'
                sctr.bind(pos=partial(self._update_top, sctr, 'top', None))
                tab_list = (sctr, )
            elif tab_pos[lentab_pos - 4:] == '_mid':
                # calculate top of scatter
                sctr.bind(
                    pos=partial(self._update_top, sctr, 'mid', scrl_v.width))
                tab_list = (Widget(), sctr, Widget())
            elif tab_pos[lentab_pos - 7:] == '_bottom':
                tab_list = (Widget(), Widget(), sctr)

            if pos_letter == 'l':
                widget_list = (tab_layout, self_content)
            else:
                widget_list = (self_content, tab_layout)

        # add widgets to tab_layout
        add = tab_layout.add_widget
        for widg in tab_list:
            add(widg)

        # add widgets to self
        add = self.add_widget
        for widg in widget_list:
            add(widg)

    def _update_tab_width(self, *l):
        if self.tab_width:
            for tab in self.tab_list:
                tab.size_hint_x = 1
            tsw = self.tab_width * len(self._tab_strip.children)
        else:
            # tab_width = None
            tsw = 0
            for tab in self.tab_list:
                if tab.size_hint_x:
                    # size_hint_x: x/.xyz
                    tab.size_hint_x = 1
                    # drop to default tab_width
                    tsw += 100
                else:
                    # size_hint_x: None
                    tsw += tab.width
        self._tab_strip.width = tsw
        self._reposition_tabs()

    def _update_top(self, *args):
        sctr, top, scrl_v_width, x, y = args
        ev = self._update_top_ev
        if ev is not None:
            ev.cancel()

        ev = self._update_top_ev = Clock.schedule_once(
            partial(self._updt_top, sctr, top, scrl_v_width), 0)

    def _updt_top(self, sctr, top, scrl_v_width, *args):
        if top[0] == 't':
            sctr.top = self.top
        else:
            sctr.top = self.top - (self.height - scrl_v_width) / 2

    def _update_scrollview(self, scrl_v, *l):
        self_tab_pos = self.tab_pos
        self_tabs = self._tab_strip
        if self_tab_pos[0] == 'b' or self_tab_pos[0] == 't':
            # bottom or top
            scrl_v.width = min(self.width, self_tabs.width)
            # required for situations when scrl_v's pos is calculated
            # when it has no parent
            scrl_v.top += 1
            scrl_v.top -= 1
        else:
            # left or right
            scrl_v.width = min(self.height, self_tabs.width)
            self_tabs.pos = (0, 0)
예제 #20
0
class Tester(App):
    state = OptionProperty('OBJECT', options=['OBJECT', 'SCREEN'])
    axis = OptionProperty('XY', options=['XY', 'XZ', 'YZ'])

    def __init__(self, **kwargs):
        super(Tester, self).__init__(**kwargs)
        # This is the magic bit that connects kivy to the
        # blender socket (Blender should already have binded to the port)
        # and sends the info. Port 6682 = ord(B) + ord(R)
        host = ''
        port = 6682

        self.address = (host, port)
        self.s = socket.socket()  # socket.AF_INET, socket.SOCK_STREAM)

        self.slider = None

    def build(self):
        # Should have done it in kv. Oops
        box = BoxLayout(orientation='vertical')
        self.slider = Slider(size_hint=(1, .1))
        self.slider.value = 1
        self.slider.min = 1
        self.slider.max = 100

        self.btn1 = ToggleButton(group='selection',
                                 text='Move object',
                                 state='down',
                                 on_touch_down=self.update_state)

        self.btn2 = ToggleButton(group='selection',
                                 text='Move screen',
                                 on_touch_down=self.update_state)

        b = BoxLayout(size_hint_y=.2, padding=10, spacing=10)
        self.opt1 = CheckBox(text='xy',
                             group='axis',
                             active=True,
                             on_touch_down=self.update_axis)
        self.opt2 = CheckBox(text='xz',
                             group='axis',
                             active=False,
                             on_touch_down=self.update_axis)
        self.opt3 = CheckBox(text='yz',
                             group='axis',
                             active=False,
                             on_touch_down=self.update_axis)

        b.add_widget(self.btn1)
        b.add_widget(self.btn2)
        b1 = BoxLayout(size_hint_x=.2, padding=10, spacing=10)
        b1.add_widget(self.opt1)
        b1.add_widget(self.opt2)
        b1.add_widget(self.opt3)
        b.add_widget(b1)

        box.add_widget(CanvasWidget())
        box.add_widget(b)
        box.add_widget(self.slider)
        return box

    def update_state(self, *args):
        """
        The state tells it to update the view or object.
        So Object would make the Cube move.
        Screen would make the screen pan and rotate (Rotate still broken)
        """
        if args[0].state == 'down':
            self.state = str(args[0].text.replace('Move ', '')).upper()
        print(self.state)

    def update_axis(self, *args):
        """
        Only used in the Object mode. Makes the object move / rotate
        on the XY, XZ or YZ axis.
        """
        if self.opt1.active:
            self.axis = 'XY'
        if self.opt2.active:
            self.axis = 'XZ'
        if self.opt3.active:
            self.axis = 'YZ'
        print(self.axis)

    def send(self, action, unit):
        """
        This actually sends the info to Blender
        Called from within the Canvas Widget
        """
        s = socket.socket()
        s.connect(self.address)
        s.send('{}: {}\n'.format(self.state + '_' + action, unit))
        s.close()
예제 #21
0
class SpecificBackgroundColorBehavior(BackgroundColorBehavior):
    background_palette = OptionProperty(
        "Primary", options=["Primary", "Accent", *palette])
    """
    See :attr:`kivymd.color_definitions.palette`.

    :attr:`background_palette` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Primary'`.
    """

    background_hue = OptionProperty("500", options=hue)
    """
    See :attr:`kivymd.color_definitions.hue`.

    :attr:`background_hue` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'500'`.
    """

    specific_text_color = ColorProperty([0, 0, 0, 0.87])
    """
    :attr:`specific_text_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0.87]`.
    """

    specific_secondary_text_color = ColorProperty([0, 0, 0, 0.87])
    """
    :attr:`specific_secondary_text_color`is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0.87]`.
    """
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if hasattr(self, "theme_cls"):
            self.theme_cls.bind(
                primary_palette=self._update_specific_text_color)
            self.theme_cls.bind(
                accent_palette=self._update_specific_text_color)
            self.theme_cls.bind(theme_style=self._update_specific_text_color)
        self.bind(background_hue=self._update_specific_text_color)
        self.bind(background_palette=self._update_specific_text_color)
        self._update_specific_text_color(None, None)

    def _update_specific_text_color(self, instance, value):
        if hasattr(self, "theme_cls"):
            palette = {
                "Primary": self.theme_cls.primary_palette,
                "Accent": self.theme_cls.accent_palette,
            }.get(self.background_palette, self.background_palette)
        else:
            palette = {
                "Primary": "Blue",
                "Accent": "Amber"
            }.get(self.background_palette, self.background_palette)
        color = get_color_from_hex(text_colors[palette][self.background_hue])
        secondary_color = color[:]
        # Check for black text (need to adjust opacity).
        if (color[0] + color[1] + color[2]) == 0:
            color[3] = 0.87
            secondary_color[3] = 0.54
        else:
            secondary_color[3] = 0.7
        self.specific_text_color = color
        self.specific_secondary_text_color = secondary_color
예제 #22
0
class XFilePopup(XBase):
    """XFilePopup class. See module documentation for more information.
    """

    size_hint_x = NumericProperty(1., allownone=True)
    size_hint_y = NumericProperty(1., allownone=True)
    '''Default size properties for the popup
    '''

    browser = ObjectProperty(None)
    '''This property represents the FileChooser object. The property contains
    an object after creation :class:`xpopup.XFilePopup` object.
    '''

    path = StringProperty(u'/')
    '''Initial path for the browser.

    Binded to :attr:`~kivy.uix.filechooser.FileChooser.path`
    '''

    selection = ListProperty()
    '''Contains the selection in the browser.

    Binded to :attr:`~kivy.uix.filechooser.FileChooser.selection`
    '''

    multiselect = BooleanProperty(False)
    '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.multiselect`
    '''

    dirselect = BooleanProperty(False)
    '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.dirselect`
    '''

    filters = ListProperty()
    '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.filters`
    '''

    CTRL_VIEW_ICON = 'icon'
    CTRL_VIEW_LIST = 'list'
    CTRL_NEW_FOLDER = 'new_folder'

    view_mode = OptionProperty(CTRL_VIEW_ICON,
                               options=(CTRL_VIEW_ICON, CTRL_VIEW_LIST))
    '''Binded to :attr:`~kivy.uix.filechooser.FileChooser.view_mode`
    '''
    def _get_body(self):
        from kivy.lang import Builder
        import textwrap
        self.browser = Builder.load_string(
            textwrap.dedent('''\
        FileChooser:
            FileChooserIconLayout
            FileChooserListLayout
        '''))

        self.browser.path = self.path
        self.browser.multiselect = self.multiselect
        self.browser.dirselect = self.dirselect
        self.browser.filters = self.filters
        self.browser.bind(path=self.setter('path'),
                          selection=self.setter('selection'))
        self.bind(view_mode=self.browser.setter('view_mode'),
                  multiselect=self.browser.setter('multiselect'),
                  dirselect=self.browser.setter('dirselect'),
                  filters=self.browser.setter('filters'))

        lbl_path = Factory.XLabel(text=self.browser.path,
                                  valign='top',
                                  halign='left',
                                  size_hint_y=None,
                                  height=metrics.dp(25))
        self.browser.bind(path=lbl_path.setter('text'))

        layout = BoxLayout(orientation='vertical')
        layout.add_widget(self._ctrls_init())
        layout.add_widget(lbl_path)
        layout.add_widget(self.browser)
        return layout

    def _ctrls_init(self):
        pnl_controls = BoxLayout(size_hint_y=None, height=metrics.dp(25))
        pnl_controls.add_widget(
            Factory.XButton(text=_('Icons'),
                            id=self.CTRL_VIEW_ICON,
                            on_release=self._ctrls_click))
        pnl_controls.add_widget(
            Factory.XButton(text=_('List'),
                            id=self.CTRL_VIEW_LIST,
                            on_release=self._ctrls_click))
        pnl_controls.add_widget(
            Factory.XButton(text=_('New folder'),
                            id=self.CTRL_NEW_FOLDER,
                            on_release=self._ctrls_click))
        return pnl_controls

    def _ctrls_click(self, instance):
        if instance.id in self.property('view_mode').options:
            self.view_mode = instance.id
        elif instance.id == self.CTRL_NEW_FOLDER:
            XTextInput(title=_('Input folder name'),
                       text=_('New folder'),
                       on_dismiss=self._create_dir)

    def _create_dir(self, instance):
        """Callback for create a new folder.
        """
        if instance.is_canceled():
            return
        new_folder = self.path + path.sep + instance.get_value()
        if path.exists(new_folder):
            XError(text=_('Folder "%s" is already exist. Maybe you should '
                          'enter another name?') % instance.get_value())
            return True
        makedirs(new_folder)
        self.browser.property('path').dispatch(self.browser)

    def _filter_selection(self, folders=True, files=True):
        """Filter the list of selected objects

        :param folders: if True - folders will be included in selection
        :param files: if True - files will be included in selection
        """
        if folders and files:
            return

        t = []
        for entry in self.selection:
            if entry == '..' + path.sep:
                pass
            elif folders and self.browser.file_system.is_dir(entry):
                t.append(entry)
            elif files and not self.browser.file_system.is_dir(entry):
                t.append(entry)
        self.selection = t
예제 #23
0
class LinearRecycleLayoutManager(RecycleLayoutManager):
    """Implementation of a `RecycleLayoutManager` for a horizontal or vertical
    arrangment
    """
    orientation = OptionProperty("vertical",
                                 options=["horizontal", "vertical"])

    # internal
    computed_sizes = []
    computed_positions = []

    def compute_positions_and_sizes(self, append):
        recycleview = self.recycleview
        height = 0
        key_size = self.key_size
        default_size = self.default_size
        data = recycleview.adapter.data
        if append:
            sizes = self.computed_sizes
            pos = self.computed_positions
            n = len(sizes)

            sizes.extend(
                [item.get(key_size, default_size) for item in data[n:]])
            self.computed_size += sum(sizes[n:])
            pos.extend(
                self._compute_positions(sizes[n:], pos[-1] + sizes[n - 1]))
        else:
            self.computed_sizes = [
                item.get(key_size, default_size) for item in data
            ]
            self.computed_size = sum(self.computed_sizes)
            self.computed_positions = list(
                self._compute_positions(self.computed_sizes))

        if self.orientation == "horizontal":
            recycleview.container.size = self.computed_size, recycleview.height
        else:
            recycleview.container.size = recycleview.width, self.computed_size

    def _compute_positions(self, sizes, pos=0):
        for size in sizes:
            yield pos
            pos += size

    def recycleview_setup(self):
        """(internal) Prepare the scrollview and container to receive widgets
        from this layout manager. Means the size of the container, as well as
        the allowed axis of the scrollview need to be set
        """
        recycleview = self.recycleview
        if self.orientation == "horizontal":
            recycleview.do_scroll_x = True
            recycleview.do_scroll_y = False
        else:
            recycleview.do_scroll_x = False
            recycleview.do_scroll_y = True

    def compute_visible_views(self):
        """(internal) Determine the views that need to be showed in the current
        scrollview. All the hidden views will be flagged as dirty, and might
        be resued for others views.
        """
        # determine the view to create for the scrollview y / height
        recycleview = self.recycleview
        container = recycleview.container
        if self.orientation == "vertical":
            h = container.height
            scroll_y = min(1, max(recycleview.scroll_y, 0))
            px_end = 0, max(0, (h - recycleview.height) * scroll_y)
            px_start = 0, px_end[1] + min(recycleview.height, h)
            viewport = 0, px_end[1], container.width, px_start[1]
        else:
            w = container.width
            scroll_x = min(1, max(recycleview.scroll_x, 0))
            px_start = max(0, (w - recycleview.width) * scroll_x), 0
            px_end = px_start[0] + min(recycleview.width, w), 0
            viewport = px_end[0], 0, px_start[0], container.height

        # now calculate the view indices we must show
        at_idx = self.get_view_index_at
        s, e, = at_idx(px_start), at_idx(px_end)
        data = recycleview.data
        if s is None:
            s = len(data) - 1
        if e is None:
            e = len(data) - 1
        new, old = recycleview.get_views(s, e)

        rm = container.remove_widget
        for widget in old:
            rm(widget)

        refresh_view_layout = self.refresh_view_layout
        add = container.add_widget
        for widget, index in new:
            # add to the container if it's not already done
            refresh_view_layout(index, widget, viewport)
            if widget.parent is None:
                add(widget)

    def refresh_view_layout(self, index, view, viewport):
        """(internal) Refresh the layout of a view. Size and pos are determine
        by the `RecycleView` according to the view `index` informations
        """
        rv = self.recycleview
        container = rv.container
        view.size_hint = None, None
        if view.__class__ not in _view_base_cache:
            _view_base_cache[view.__class__] = isinstance(
                view, RecycleViewMixin)

        if self.orientation == "vertical":
            w = container.width
            h = self.computed_sizes[index]
            y = self.computed_size - self.computed_positions[index] - h
            x = 0
        else:
            h = container.height
            w = self.computed_sizes[index]
            x = self.computed_size - self.computed_positions[index] - w
            y = 0

        if _view_base_cache[view.__class__]:
            view.refresh_view_layout(rv, index, (x, y), (w, h), viewport)
        else:
            view.size = w, h
            view.pos = x, y

    def get_view_position(self, index):
        return self.computed_positions[index]

    def get_view_size(self, index):
        return self.computed_sizes[index]

    def get_view_index_at(self, pos):
        if self.orientation == 'vertical':
            pos = self.recycleview.container.height - pos[1]
        else:
            pos = pos[0]
        for index, c_pos in enumerate(self.computed_positions):
            if c_pos > pos:
                return max(index - 1, 0)
        if pos >= self.computed_positions[-1] + self.computed_sizes[-1]:
            return None
        return index

    def show_index_view(self, index):
        rv = self.recycleview
        if self.orientation == "vertical":
            h = rv.container.height
            if h <= rv.height:  # all views are visible
                return

            # convert everything to container coordinates
            top = h - self.computed_positions[index]
            bottom = top - self.computed_sizes[index]
            view_h = h - rv.height
            view_bot = view_h * min(1, max(rv.scroll_y, 0))
            view_top = view_bot + rv.height

            if top <= view_top:
                if bottom >= view_bot:  # it's fully in view
                    return
                rv.scroll_y = bottom / float(view_h)
            else:
                rv.scroll_y = (top - rv.height) / float(view_h)
        else:
            w = rv.container.width
            if w <= rv.width:  # all views are visible
                return

            # convert everything to container coordinates
            left = self.computed_positions[index]
            right = left + self.computed_sizes[index]
            view_w = w - rv.width
            view_left = view_w * min(1, max(rv.scroll_x, 0))
            view_right = view_left + rv.width

            if left >= view_left:
                if right <= view_right:  # it's fully in view
                    return
                rv.scroll_x = (right - rv.width) / float(view_w)
            else:
                rv.scroll_x = left / float(view_w)
예제 #24
0
class MDToolbar(
        ThemableBehavior,
        RectangularElevationBehavior,
        SpecificBackgroundColorBehavior,
        BoxLayout,
):
    """
    :Events:
        `on_action_button`
            Method for the button used for the :class:`~MDBottomAppBar` class.
    """
    left_action_items = ListProperty()
    """The icons on the left of the toolbar.
    To add one, append a list like the following:

    .. code-block:: kv

        left_action_items: [`'icon_name'`, callback]

    where `'icon_name'` is a string that corresponds to an icon definition and
    ``callback`` is the function called on a touch release event.

    :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    right_action_items = ListProperty()
    """The icons on the left of the toolbar.
    Works the same way as :attr:`left_action_items`.

    :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    title = StringProperty()
    """Text toolbar.

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    md_bg_color = ListProperty()
    """Color toolbar.
    
    :attr:`md_bg_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    anchor_title = StringProperty("left")

    mode = OptionProperty("center",
                          options=["free-end", "free-center", "end", "center"])
    """Floating button position. Onle for :class:`~MDBottomAppBar` class.
    Available options are: `'free-end'`, `'free-center'`, `'end'`, `'center'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'center'`.
    """

    round = NumericProperty("10dp")
    """
    Rounding the corners at the notch for a button.
    Onle for :class:`~MDBottomAppBar` class.

    :attr:`round` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'10dp'`.
    """

    icon = StringProperty("android")
    """
    Floating button. Onle for :class:`~MDBottomAppBar` class.

    :attr:`icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'android'`.
    """

    icon_color = ListProperty()
    """
    Color action button. Only for :class:`~MDBottomAppBar` class.

    :attr:`icon_color` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    type = OptionProperty("top", options=["top", "bottom"])
    """
    When using the :class:`~MDBottomAppBar` class, the parameter ``type``
    must be set to `'bottom'`:

    .. code-block:: kv

        MDBottomAppBar:

            MDToolbar:
                type: "bottom"
    
    Available options are: `'top'`, `'bottom'`.

    :attr:`type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'top'`.
    """

    _shift = NumericProperty("3.5dp")
    _angle_start = NumericProperty(90)
    _angle_end = NumericProperty(270)

    def __init__(self, **kwargs):
        self.action_button = MDActionBottomAppBarButton()
        super().__init__(**kwargs)
        self.register_event_type("on_action_button")
        self.action_button.bind(
            on_release=lambda x: self.dispatch("on_action_button"))
        self.action_button.x = Window.width / 2 - self.action_button.width / 2
        self.action_button.y = ((self.center[1] - self.height / 2) +
                                self.theme_cls.standard_increment / 2 +
                                self._shift)
        if not self.icon_color:
            self.icon_color = self.theme_cls.primary_color
        Window.bind(on_resize=self._on_resize)
        self.bind(specific_text_color=self.update_action_bar_text_colors)
        Clock.schedule_once(
            lambda x: self.on_left_action_items(0, self.left_action_items))
        Clock.schedule_once(
            lambda x: self.on_right_action_items(0, self.right_action_items))

    def on_action_button(self, *args):
        pass

    def on_md_bg_color(self, instance, value):
        if self.type == "bottom":
            self.md_bg_color = [0, 0, 0, 0]

    def on_left_action_items(self, instance, value):
        self.update_action_bar(self.ids["left_actions"], value)

    def on_right_action_items(self, instance, value):
        self.update_action_bar(self.ids["right_actions"], value)

    def update_action_bar(self, action_bar, action_bar_items):
        action_bar.clear_widgets()
        new_width = 0
        for item in action_bar_items:
            new_width += dp(48)
            action_bar.add_widget(
                MDIconButton(
                    icon=item[0],
                    on_release=item[1],
                    opposite_colors=True,
                    text_color=self.specific_text_color,
                    theme_text_color="Custom",
                ))
        action_bar.width = new_width

    def update_action_bar_text_colors(self, instance, value):
        for child in self.ids["left_actions"].children:
            child.text_color = self.specific_text_color
        for child in self.ids["right_actions"].children:
            child.text_color = self.specific_text_color

    def _on_resize(self, instance, width, height):
        if self.mode == "center":
            self.action_button.x = width / 2 - self.action_button.width / 2
        else:
            self.action_button.x = width - self.action_button.width * 2

    def on_icon(self, instance, value):
        self.action_button.icon = value

    def on_icon_color(self, instance, value):
        self.action_button.md_bg_color = value

    def on_mode(self, instance, value):
        def set_button_pos(*args):
            self.action_button.x = x
            self.action_button.y = y
            self.action_button._hard_shadow_size = (0, 0)
            self.action_button._soft_shadow_size = (0, 0)
            anim = Animation(_scale_x=1, _scale_y=1, d=0.05)
            anim.bind(on_complete=self.set_shadow)
            anim.start(self.action_button)

        if value == "center":
            self.set_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = ((self.center[1] - self.height / 2) +
                 self.theme_cls.standard_increment / 2 + self._shift)
        elif value == "end":

            self.set_notch()
            x = Window.width - self.action_button.width * 2
            y = ((self.center[1] - self.height / 2) +
                 self.theme_cls.standard_increment / 2 + self._shift)
            self.right_action_items = []
        elif value == "free-end":
            self.remove_notch()
            x = Window.width - self.action_button.width - dp(10)
            y = self.action_button.height + self.action_button.height / 2
        elif value == "free-center":
            self.remove_notch()
            x = Window.width / 2 - self.action_button.width / 2
            y = self.action_button.height + self.action_button.height / 2
        self.remove_shadow()
        anim = Animation(_scale_x=0, _scale_y=0, d=0.05)
        anim.bind(on_complete=set_button_pos)
        anim.start(self.action_button)

    def remove_notch(self):
        self._angle_start = 0
        self._angle_end = 0
        self.round = 0
        self._shift = 0

    def set_notch(self):
        self._angle_start = 90
        self._angle_end = 270
        self.round = dp(10)
        self._shift = dp(3.5)

    def remove_shadow(self):
        self.action_button._hard_shadow_size = (0, 0)
        self.action_button._soft_shadow_size = (0, 0)

    def set_shadow(self, *args):
        self.action_button._hard_shadow_size = (dp(112), dp(112))
        self.action_button._soft_shadow_size = (dp(112), dp(112))
예제 #25
0
class NavigationDrawerIconButton(OneLineIconListItem):
    """An item in the :class:`MDNavigationDrawer`."""

    _active = BooleanProperty(False)
    _active_color = ListProperty()
    _icon = ObjectProperty()
    divider = None

    icon_color = ListProperty()
    """Custom icon color.

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

    active_color = ListProperty()
    """Custom active color.
    This option only takes effect when :attr:`active_color_type` = 'custom'.

    :attr:`active_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to None.
    """

    active_color_type = OptionProperty("primary",
                                       options=["primary", "accent", "custom"])
    """Decides which color should be used for the active color.
    This option only takes effect when :attr:`use_active` = True.

    Options:
        primary: Active color will be the primary theme color.

        accent: Active color will be the theme's accent color.

        custom: Active color will be taken from the :attr:`active_color` attribute.

    :attr:`active_color_type` is a :class:`~kivy.properties.OptionProperty`
    and defaults to 'primary'.
    """

    icon = StringProperty("checkbox-blank-circle")
    """Icon that appears to the left of the widget.

    :attr:`icon` is a :class:`~kivy.properties.StringProperty` and defaults
    to 'checkbox-blank-circle'.
    """
    badge_text = StringProperty("")
    """
    Text that appears on the right side of the item, usually
    for displaying a count of sorts.


    :attr:`badge_text` is a :class:`~kivy.properties.StringProperty`
    and defaults to ''.
    """
    use_active = BooleanProperty(True)
    """If the button should change to the active color when selected.

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

    See also:
        :attr:`active_color`

        :attr:`active_color_type`
    """

    # active_color = get_color_from_hex(colors['Red']['500'])
    # active_color_type = 'custom'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._set_active_color()
        self.theme_cls.bind(
            primary_color=self._set_active_color_primary,
            accent_color=self._set_active_color_accent,
        )
        Clock.schedule_once(lambda x: self.on_icon(self, self.icon))

    def _set_active(self, active, nav_drawer):
        if self.use_active:
            self._active = active
            if nav_drawer.active_item != self:
                if nav_drawer.active_item is not None:
                    nav_drawer.active_item._active = False
            nav_drawer.active_item = self

    def _set_active_color(self, *args):
        if self.active_color_type == "primary":
            self._set_active_color_primary()
        elif self.active_color_type == "accent":
            self._set_active_color_accent()

    # Note to future developers/myself: These must be separate functions
    def _set_active_color_primary(self, *args):
        if self.active_color_type == "primary":
            self._active_color = self.theme_cls.primary_color

    def _set_active_color_accent(self, *args):
        if self.active_color_type == "accent":
            self._active_color = self.theme_cls.accent_color

    def on_icon(self, instance, value):
        super().__init__()
        self.ids._icon.text = "{}".format(md_icons[value])

    def on_active_color_type(self, *args):
        self._set_active_color(args)
class CustomCarousel(StencilView):
    slides = ListProperty([])
    direction = OptionProperty('right',
                               options=('right', 'left', 'top', 'bottom'))
    min_move = NumericProperty(0.05)
    anim_move_duration = NumericProperty(0.2)
    anim_cancel_duration = NumericProperty(0.2)
    loop = BooleanProperty(False)
    scroll_timeout = NumericProperty(200)
    scroll_distance = NumericProperty('1dp')
    anim_type = StringProperty('out_quad')
    ignore_perpendicular_swipes = BooleanProperty(False)

    _index = NumericProperty(0, allownone=True)
    _prev = ObjectProperty(None, allownone=True)
    _current = ObjectProperty(None, allownone=True)
    _next = ObjectProperty(None, allownone=True)
    _offset = NumericProperty(0)
    _touch = ObjectProperty(None, allownone=True)
    _change_touch_mode_ev = None

    def _get_slides_container(self):
        return [x.parent for x in self.slides]

    def _get_index(self):
        if self.slides:
            return self._index % len(self.slides)
        return None

    def _set_index(self, value):
        if self.slides:
            self._index = value % len(self.slides)
        else:
            self._index = None

    def _prev_slide(self):
        slides = self.slides
        len_slides = len(slides)
        index = self.index
        if len_slides < 2:  # None, or 1 slide
            return None
        if len_slides == 2:
            if index == 0:
                return None
            if index == 1:
                return slides[0]
        if self.loop and index == 0:
            return slides[-1]
        if index > 0:
            return slides[index - 1]

    def _prev_prev_slide(self):
        if self.index > 1:
            return self.slides[self.index - 2]

    def _curr_slide(self):
        if len(self.slides):
            return self.slides[self.index or 0]

    def _next_slide(self):
        if len(self.slides) < 2:  # None, or 1 slide
            return None
        if len(self.slides) == 2:
            if self.index == 0:
                return self.slides[1]
            if self.index == 1:
                return None
        if self.loop and self.index == len(self.slides) - 1:
            return self.slides[0]
        if self.index < len(self.slides) - 1:
            return self.slides[self.index + 1]

    def _next_next_slide(self):
        if self.index < len(self.slides) - 2:
            return self.slides[self.index + 2]

    slides_container = AliasProperty(_get_slides_container, bind=('slides', ))
    index = AliasProperty(_get_index,
                          _set_index,
                          bind=('_index', 'slides'),
                          cache=True)
    previous_slide = AliasProperty(_prev_slide,
                                   bind=('slides', 'index', 'loop'),
                                   cache=True)
    previous_previous_slide = AliasProperty(_prev_prev_slide,
                                            bind=('slides', 'index', 'loop'),
                                            cache=True)
    current_slide = AliasProperty(_curr_slide,
                                  bind=('slides', 'index'),
                                  cache=True)
    next_slide = AliasProperty(_next_slide,
                               bind=('slides', 'index', 'loop'),
                               cache=True)
    next_next_slide = AliasProperty(_next_next_slide,
                                    bind=('slides', 'index', 'loop'),
                                    cache=True)

    def __init__(self, **kwargs):
        self._trigger_position_visible_slides = Clock.create_trigger(
            self._position_visible_slides, -1)
        super(CustomCarousel, self).__init__(**kwargs)
        self._skip_slide = None
        self.touch_mode_change = False

    def load_slide(self, slide):
        slides = self.slides
        start, stop = slides.index(self.current_slide), slides.index(slide)
        if start == stop:
            return

        self._skip_slide = stop
        if stop > start:
            self._insert_visible_slides(_next_slide=slide)
            self.load_next()
        else:
            self._insert_visible_slides(_prev_slide=slide)
            self.load_previous()

    def load_previous(self):
        self.load_next(mode='prev')

    def load_next(self, mode='next'):
        if self.index is not None:
            w, h = (dp(275), dp(200))
            _direction = {
                'top': -h / 2,
                'bottom': h / 2,
                'left': w / 2,
                'right': -w / 2
            }
            _offset = _direction[self.direction]
            if mode == 'prev':
                _offset = -_offset

            self._start_animation(min_move=0, offset=_offset)

    def get_slide_container(self, slide):
        return slide.parent

    def _insert_visible_slides(self,
                               _next_slide=None,
                               _prev_slide=None,
                               _next_next_slide=None,
                               _prev_prev_slide=None):
        get_slide_container = self.get_slide_container

        previous_slide = _prev_slide if _prev_slide else self.previous_slide
        if previous_slide:
            self._prev = get_slide_container(previous_slide)
        else:
            self._prev = None

        current_slide = self.current_slide
        if current_slide:
            self._current = get_slide_container(current_slide)
        else:
            self._current = None

        next_slide = _next_slide if _next_slide else self.next_slide
        if next_slide:
            self._next = get_slide_container(next_slide)
        else:
            self._next = None

        next_next_slide = _next_next_slide if _next_next_slide else self.next_next_slide
        if next_next_slide:
            self._next_next = get_slide_container(next_next_slide)
        else:
            self._next_next = None

        previous_previous_slide = _prev_prev_slide if _prev_prev_slide else self.previous_previous_slide
        if previous_previous_slide:
            self._prev_prev = get_slide_container(previous_previous_slide)
        else:
            self._prev_prev = None

        super_remove = super(CustomCarousel, self).remove_widget
        for container in self.slides_container:
            super_remove(container)

        if self._prev and self._prev.parent is not self:
            super(CustomCarousel, self).add_widget(self._prev)
        if self._prev_prev and self._prev_prev.parent is not self:
            super(CustomCarousel, self).add_widget(self._prev_prev)
        if self._next and self._next.parent is not self:
            super(CustomCarousel, self).add_widget(self._next)
        if self._next_next and self._next_next.parent is not self:
            super(CustomCarousel, self).add_widget(self._next_next)
        if self._current:
            super(CustomCarousel, self).add_widget(self._current)

    def _position_visible_slides(self, *args):
        slides, index = self.slides, self.index
        no_of_slides = len(slides) - 1
        if not slides:
            return
        x, y, width, height = self.x, self.y, dp(275), self.height
        _offset, direction = self._offset, self.direction
        _prev, _prev_prev, _next, _next_next, _current = self._prev, self._prev_prev, self._next, self._next_next, self._current
        get_slide_container = self.get_slide_container
        last_slide = get_slide_container(slides[-1])
        first_slide = get_slide_container(slides[0])
        skip_next = False
        _loop = self.loop

        if direction[0] in ['r', 'l']:
            xoff = x + _offset + dp((self.width - dp(250)) / dp(2))
            x_prev = {'l': xoff + width, 'r': xoff - width}
            x_next = {'l': xoff - width, 'r': xoff + width}

            x_prev_prev = {
                'l': xoff + width + width,
                'r': xoff - width - width
            }
            x_next_next = {
                'l': xoff - width - width,
                'r': xoff + width + width
            }
            if _prev:
                _prev.pos = (x_prev[direction[0]], y)
            if _prev_prev:
                _prev_prev.pos = (x_prev_prev[direction[0]], y)
            elif _loop and _next and index == 0:
                # if first slide is moving to right with direction set to right
                # or toward left with direction set to left
                if ((_offset > 0 and direction[0] == 'r')
                        or (_offset < 0 and direction[0] == 'l')):
                    # put last_slide before first slide
                    last_slide.pos = (x_prev[direction[0]], y)
                    skip_next = True
            if _current:
                _current.pos = (xoff, y)
            if skip_next:
                return
            if _next:
                _next.pos = (x_next[direction[0]], y)
            if _next_next:
                _next_next.pos = (x_next_next[direction[0]], y)
            elif _loop and _prev and index == no_of_slides:
                if ((_offset < 0 and direction[0] == 'r')
                        or (_offset > 0 and direction[0] == 'l')):
                    first_slide.pos = (x_next[direction[0]], y)
        if direction[0] in ['t', 'b']:
            yoff = y + _offset
            y_prev = {'t': yoff - height, 'b': yoff + height}
            y_next = {'t': yoff + height, 'b': yoff - height}
            if _prev:
                _prev.pos = (x, y_prev[direction[0]])
            elif _loop and _next and index == 0:
                if ((_offset > 0 and direction[0] == 't')
                        or (_offset < 0 and direction[0] == 'b')):
                    last_slide.pos = (x, y_prev[direction[0]])
                    skip_next = True
            if _current:
                _current.pos = (x, yoff)
            if skip_next:
                return
            if _next:
                _next.pos = (x, y_next[direction[0]])
            elif _loop and _prev and index == no_of_slides:
                if ((_offset < 0 and direction[0] == 't')
                        or (_offset > 0 and direction[0] == 'b')):
                    first_slide.pos = (x, y_next[direction[0]])

    def on_size(self, *args):
        size = (dp(275), dp(200))
        for slide in self.slides_container:
            slide.size = size
        self._trigger_position_visible_slides()

    def on_pos(self, *args):
        self._trigger_position_visible_slides()

    def on_index(self, *args):
        self._insert_visible_slides()
        self._trigger_position_visible_slides()
        self._offset = 0

    def on_slides(self, *args):
        if self.slides:
            self.index = self.index % len(self.slides)
        self._insert_visible_slides()
        self._trigger_position_visible_slides()

    def on__offset(self, *args):
        self._trigger_position_visible_slides()
        # if reached full offset, switch index to next or prev
        direction = self.direction
        _offset = self._offset
        width = dp(275)
        height = self.height
        index = self.index
        if self._skip_slide is not None or index is None:
            return

        # Move to next slide?
        if (direction[0] == 'r' and _offset <= -width) or \
                (direction[0] == 'l' and _offset >= width) or \
                (direction[0] == 't' and _offset <= - height) or \
                (direction[0] == 'b' and _offset >= height):
            if self.next_slide:
                self.index += 1

        # Move to previous slide?
        if (direction[0] == 'r' and _offset >= width) or \
                (direction[0] == 'l' and _offset <= -width) or \
                (direction[0] == 't' and _offset >= height) or \
                (direction[0] == 'b' and _offset <= -height):
            if self.previous_slide:
                self.index -= 1

    def _start_animation(self, *args, **kwargs):
        # compute target offset for ease back, next or prev
        new_offset = 0
        direction = kwargs.get('direction', self.direction)
        is_horizontal = direction[0] in ['r', 'l']
        extent = dp(275) if is_horizontal else self.height
        min_move = kwargs.get('min_move', self.min_move)
        _offset = kwargs.get('offset', self._offset)

        if _offset < min_move * -extent:
            new_offset = -extent
        elif _offset > min_move * extent:
            new_offset = extent

        # if new_offset is 0, it wasnt enough to go next/prev
        dur = self.anim_move_duration
        if new_offset == 0:
            dur = self.anim_cancel_duration

        # detect edge cases if not looping
        len_slides = len(self.slides)
        index = self.index
        if not self.loop or len_slides == 1:
            is_first = (index == 0)
            is_last = (index == len_slides - 1)
            if direction[0] in ['r', 't']:
                towards_prev = (new_offset > 0)
                towards_next = (new_offset < 0)
            else:
                towards_prev = (new_offset < 0)
                towards_next = (new_offset > 0)
            if (is_first and towards_prev) or (is_last and towards_next):
                new_offset = 0

        anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
        anim.cancel_all(self)

        def _cmp(*l):
            if self._skip_slide is not None:
                self.index = self._skip_slide
                self._skip_slide = None

        anim.bind(on_complete=_cmp)
        anim.start(self)

    def _get_uid(self, prefix='sv'):
        return '{0}.{1}'.format(prefix, self.uid)

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            touch.ud[self._get_uid('cavoid')] = True
            return
        if self.disabled:
            return True
        if self._touch:
            return super(CustomCarousel, self).on_touch_down(touch)
        Animation.cancel_all(self)
        self._touch = touch
        uid = self._get_uid()
        touch.grab(self)
        touch.ud[uid] = {'mode': 'unknown', 'time': touch.time_start}
        self._change_touch_mode_ev = Clock.schedule_once(
            self._change_touch_mode, self.scroll_timeout / 1000.)
        self.touch_mode_change = False
        return True

    def on_touch_move(self, touch):
        if not self.touch_mode_change:
            if self.ignore_perpendicular_swipes and \
                    self.direction in ('top', 'bottom'):
                if abs(touch.oy - touch.y) < self.scroll_distance:
                    if abs(touch.ox - touch.x) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touch_mode_change = True
            elif self.ignore_perpendicular_swipes and \
                    self.direction in ('right', 'left'):
                if abs(touch.ox - touch.x) < self.scroll_distance:
                    if abs(touch.oy - touch.y) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touch_mode_change = True

        if self._get_uid('cavoid') in touch.ud:
            return
        if self._touch is not touch:
            super(CustomCarousel, self).on_touch_move(touch)
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True
        ud = touch.ud[self._get_uid()]
        direction = self.direction
        if ud['mode'] == 'unknown':
            if direction[0] in ('r', 'l'):
                distance = abs(touch.ox - touch.x)
            else:
                distance = abs(touch.oy - touch.y)
            if distance > self.scroll_distance:
                ev = self._change_touch_mode_ev
                if ev is not None:
                    ev.cancel()
                ud['mode'] = 'scroll'
        else:
            if direction[0] in ('r', 'l'):
                self._offset += touch.dx
            if direction[0] in ('t', 'b'):
                self._offset += touch.dy
        return True

    def on_touch_up(self, touch):
        if self._get_uid('cavoid') in touch.ud:
            return
        if self in [x() for x in touch.grab_list]:
            touch.ungrab(self)
            self._touch = None
            ud = touch.ud[self._get_uid()]
            if ud['mode'] == 'unknown':
                ev = self._change_touch_mode_ev
                if ev is not None:
                    ev.cancel()
                super(CustomCarousel, self).on_touch_down(touch)
                Clock.schedule_once(partial(self._do_touch_up, touch), .1)
            else:
                self._start_animation()

        else:
            if self._touch is not touch and self.uid not in touch.ud:
                super(CustomCarousel, self).on_touch_up(touch)
        return self._get_uid() in touch.ud

    def _do_touch_up(self, touch, *largs):
        super(CustomCarousel, self).on_touch_up(touch)
        # don't forget about grab event!
        for x in touch.grab_list[:]:
            touch.grab_list.remove(x)
            x = x()
            if not x:
                continue
            touch.grab_current = x
            super(CustomCarousel, self).on_touch_up(touch)
        touch.grab_current = None

    def _change_touch_mode(self, *largs):
        if not self._touch:
            return
        self._start_animation()
        uid = self._get_uid()
        touch = self._touch
        ud = touch.ud[uid]
        if ud['mode'] == 'unknown':
            touch.ungrab(self)
            self._touch = None
            super(CustomCarousel, self).on_touch_down(touch)
            return

    def add_widget(self, widget, index=0, canvas=None):
        slide = RelativeLayout(size=(dp(275), dp(200)),
                               x=self.x - dp(275),
                               y=self.y)
        slide.add_widget(widget)
        super(CustomCarousel, self).add_widget(slide, index, canvas)
        if index != 0:
            self.slides.insert(index - len(self.slides), widget)
        else:
            self.slides.append(widget)

    def remove_widget(self, widget, *args, **kwargs):
        # XXX be careful, the widget.parent refer to the RelativeLayout
        # added in add_widget(). But it will break if RelativeLayout
        # implementation change.
        # if we passed the real widget
        if widget in self.slides:
            slide = widget.parent
            self.slides.remove(widget)
            return slide.remove_widget(widget, *args, **kwargs)
        return super(CustomCarousel,
                     self).remove_widget(widget, *args, **kwargs)

    def clear_widgets(self):
        for slide in self.slides[:]:
            self.remove_widget(slide)
        super(CustomCarousel, self).clear_widgets()
예제 #27
0
class MDColorPicker(BaseDialog):
    adjacent_color_constants = ListProperty([0.299, 0.887, 0.411])
    """
    A list of values that are used to create the gradient. These values are
    selected empirically. Each of these values will be added to the selected
    ``RGB`` value, thus creating colors that are close in value.

    :attr:`adjacent_color_constants` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0.299, 0.887, 0.411]`.
    """

    default_color = ColorProperty(None, allownone=True)
    """
    Default color value The set color value will be used when you open the
    dialog.

    :attr:`default_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    type_color = OptionProperty("RGB", options=["RGBA", "HEX", "RGB"])
    """
    Type of color.
    Available options are: `'RGBA'`, `'HEX'`, `'RGB'`.

    :attr:`type_color` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'RGB'`.
    """

    background_down_button_selected_type_color = ColorProperty([1, 1, 1, 0.3])
    """
    Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB').

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-background-down-button-selected-type-color.png
        :align: center

    :attr:`background_down_button_selected_type_color` is an
    :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 0.3]`.
    """

    radius_color_scale = VariableListProperty([8])
    """
    The radius value for the color scale.

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-gradient-scale-radius.png
        :align: center

    :attr:`radius` is an :class:`~kivy.properties.VariableListProperty`
    and defaults to `[8, 8, 8, 8]`.
    """

    text_button_ok = StringProperty("SELECT")
    """
    Color selection button text.

    :attr:`text_button_ok` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'SELECT'`.
    """

    text_button_cancel = StringProperty("CANCEL")
    """
    Cancel button text.

    :attr:`text_button_cancel` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'CANCEL'`.
    """

    selected_color = None
    # One of the objects of classes:
    # :class:`~GradientTab`, :class:`~ColorListTab`, :class:`~SliderTab`.
    _current_tab = ObjectProperty()
    # The `RGB` value for the transparency preview widget of the selected
    # color.
    _rgb = ListProperty()
    # The opacity value for the transparency preview widget of the selected
    # color.
    _opacity_value_selected_color = NumericProperty(1)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.gradient_tab = None
        self.register_event_type("on_select_color")
        self.register_event_type("on_switch_tabs")
        self.register_event_type("on_release")
        self.on_background_down_button_selected_type_color(
            None, self.background_down_button_selected_type_color)

        self.on_background_down_button_selected_type_color(
            None, self.background_down_button_selected_type_color)
        Clock.schedule_once(lambda x: self.on_type_color(self), 1)

    def update_color_slider_item_bottom_navigation(self,
                                                   color: list) -> NoReturn:
        """
        Updates the color of the slider that sets the transparency value of the
        selected color and the color of bottom navigation items.
        """

        if "select_alpha_channel_widget" in self._current_tab.ids:
            self._current_tab.ids.select_alpha_channel_widget.ids.slider.color = (
                color)
        self.ids.bottom_navigation.text_color_active = color

    def update_color_type_buttons(self, color: list) -> NoReturn:
        """
        Updating button colors (display buttons of type of color) to match the
        selected color.
        """

        for instance_toggle_button in self.ids.type_color_button_box.children:
            if instance_toggle_button.state != "down":
                instance_toggle_button.md_bg_color = color
            instance_toggle_button.background_normal = color

    def get_rgb(self, color: list) -> list:
        """Returns an ``RGB`` list of values from 0 to 255."""

        return [
            int(value * 255)
            for value in (color[:-1] if len(color) == 4 else color)
        ]

    def on_background_down_button_selected_type_color(self,
                                                      instance_color_picker,
                                                      color: list) -> NoReturn:
        def set_background_down(interval: Union[float, int]) -> NoReturn:
            for (instance_toggle_button
                 ) in self.ids.type_color_button_box.children:
                instance_toggle_button.background_down = color
                if self.type_color == instance_toggle_button.text:
                    instance_toggle_button.state = "down"

        Clock.schedule_once(set_background_down)

    def on_type_color(
        self,
        instance_color_picker,
        type_color: str = "",
        interval: Union[float, int] = 0,
    ) -> NoReturn:
        """Called when buttons are clicked to set the color type."""

        if not type_color:
            type_color = self.type_color

        if self._rgb:
            rgb = self._rgb if self._rgb[0] > 1 else self.get_rgb(self._rgb)
            opacity = self._opacity_value_selected_color
            color = ""

            if type_color == "RGB":
                self.selected_color = [value for value in rgb]
                color = f"RGB({', '.join([str(value) for value in self.selected_color])})"
            elif type_color == "RGBA":
                self.selected_color = [x / 255.0 for x in rgb] + [opacity]
                color = f"RGBA({', '.join([str(x / 255.0) for x in rgb])}, {opacity})"
            elif type_color == "HEX":
                self.selected_color = get_hex_from_color(
                    [x / 255.0 for x in rgb] + [opacity])
                color = f"HEX({self.selected_color})"

            self.ids.lbl_color_value.text = color

    def on_open(self) -> NoReturn:
        """Default open event handler."""

        if not self.ids.bottom_navigation_gradient.children:
            self.gradient_tab = GradientTab(color_picker=self)
            self._current_tab = self.gradient_tab
            self.ids.bottom_navigation_gradient.add_widget(self.gradient_tab)

    def on_select_color(self, color: list) -> NoReturn:
        """Called when a gradient image is clicked."""

        if len(color) == 3:
            color += [self._opacity_value_selected_color]

        self.ids.header.md_bg_color = color
        self._rgb = color[:-1]
        self.on_type_color(self, self.type_color)
        self.update_color_type_buttons(color)
        self.update_color_slider_item_bottom_navigation(color)

    def on_switch_tabs(
        self,
        bottom_navigation_instance,
        bottom_navigation_item_instance,
        name_tab,
    ) -> NoReturn:
        """Called when switching tabs of bottom navigation."""

        if name_tab == "bottom navigation gradient":
            self._current_tab = self.gradient_tab
            bottom_navigation_item_instance.children[0].updated_canvas(
                None,
                None,
                self._rgb if self._rgb[0] > 1 else self.get_rgb(self._rgb),
            )
            instance_slider_tab = (bottom_navigation_instance.ids.tab_manager.
                                   get_screen("tune").children[0])
            select_alpha_channel_widget = (
                self.gradient_tab.ids.select_alpha_channel_widget)
            select_alpha_channel_widget.ids.slider.value = (
                instance_slider_tab.ids.select_alpha_channel_widget.ids.slider.
                value)
            select_alpha_channel_widget.ids.slider.color = [
                x / 255.0 for x in self._rgb
            ] + [1]
        elif name_tab == "tune":
            if self._rgb[0] <= 1:
                color = self.get_rgb(self._rgb)
            else:
                color = self._rgb
            instance_slider_tab = self.ids.tune.children[0]
            self._current_tab = instance_slider_tab
            instance_slider_tab.ids.slider_red.ids.slider.value = color[0]
            instance_slider_tab.ids.slider_green.ids.slider.value = color[1]
            instance_slider_tab.ids.slider_blue.ids.slider.value = color[2]
            instance_slider_tab.ids.select_alpha_channel_widget.ids.slider.value = (
                self._opacity_value_selected_color)
        elif name_tab == "view headline":
            color = self._rgb + [1]
            color_list_tabs = self.ids.view_headline.children[0]
            self._current_tab = color_list_tabs
            try:
                color_list_tabs.background_color = color
            except ValueError:
                color_list_tabs.background_color = [x / 255.0
                                                    for x in color][:-1] + [1]
            if not color_list_tabs.get_tab_list():
                for color in _colors.keys():
                    tab_widget = TabColorList(title=str(color))
                    color_list_tabs.add_widget(tab_widget)

    def on_release(self, *args):
        """Called when the `SELECT` button is pressed"""

    def _get_selected_color(self, selected_color: Union[list, str]) -> list:
        """
        Convert [0-255, 0-255, 0-255] and '#rrggbb' to kivy color format.
        Return kivy color format.
        """

        rgba = [0, 0, 0, 0]
        if isinstance(selected_color, list):
            if selected_color[0] > 1:
                rgba = [x / 255.0 for x in selected_color
                        ] + [self._opacity_value_selected_color]
            else:
                rgba = selected_color
        elif isinstance(selected_color, str):
            rgba = get_color_from_hex(selected_color)[:-1] + [
                self._opacity_value_selected_color
            ]
        return rgba
예제 #28
0
class MergeGUI(BetterLogger, FloatLayout):
    active: str = BooleanProperty(None)
    mode: str = OptionProperty(None, options=["merge", "recipes"])

    other_merge_gui: MergeGUI = ObjectProperty(None)

    def __init__(self, **kwargs):
        BetterLogger.__init__(self)
        FloatLayout.__init__(self, **kwargs)

        self.size_hint = None, None

        self.register_event_type("on_items")

    def on_items(self, *args):
        pass

    def on_touch_down(self, touch, is_second: bool = False):
        if self.active:
            for child in self.children[:]:
                child.dispatch('on_touch_down', touch)

        if not is_second:
            self.other_merge_gui.dispatch('on_touch_down', touch, True)

    def set_all(self, items: dict):
        self.log_deep_debug("Set all to", items)

        self.clear_widgets()

        for item, amount in items.items():
            button = TextBetterButton(button_id=str(item) + "_item",
                                      size_type="big",
                                      show_amount_text=True,
                                      amount=amount,
                                      bg_visible=False)
            button.bind(on_release=ignore_args(self.item_pressed, button))
            button.button_storage = str(item)
            self.add_widget(button)

            self.log_debug("Added button -", button)

        self._trigger_layout()

    def add(self, item: str, amount: int = 1):
        button: TextBetterButton
        for button in self.children:
            if button.button_storage == str(item):
                button.amount += amount

                self.dispatch("on_items")
                return

        button = TextBetterButton(button_id=str(item) + "_item",
                                  size_type="big",
                                  show_amount_text=True,
                                  amount=1,
                                  bg_visible=False)
        button.bind(on_release=ignore_args(self.item_pressed, button))
        button.button_storage = str(item)
        self.add_widget(button)

        self.log_debug("Added button -", button)

        self.dispatch("on_items")

    def remove(self, item: str, amount: int = 1):
        button: TextBetterButton
        for button in self.children:
            if button.button_storage == str(item):
                button.amount -= amount
                if button.amount <= 0:
                    self.remove_widget(button)
                    self.log_debug("Removed", amount, "of", button)

                break

        self.dispatch("on_items")

    def get_moved_amount(self, item: str):
        button: TextBetterButton
        for button in self.children:
            if button.button_storage == str(item):
                return button.amount

        self.log_deep_debug(
            "Ran get_moved_amount but no button had that item - this is probably not a bug"
        )
        return 0

    def get_all(self):
        button: TextBetterButton
        for button in self.children:
            yield button.button_storage, button.amount

    def item_pressed(self, button: TextBetterButton):
        item = str(button.button_storage)
        touch: MotionEvent = button.last_touch
        item_large_move_amount = graphicsConfig.getint(
            "InventoryScreen", "item_large_move_amount")

        if self.mode == "merge":  # Move back
            if touch.is_double_tap or touch.is_triple_tap:
                self.remove(item, item_large_move_amount)

            else:
                self.remove(item, 1)

        elif self.mode == "recipes":
            item = str(button.button_storage)

            if item in GameConfig.get("Items", "recipes"):
                inventory_screen = get_screen("InventoryScreen")

                recipe = GameConfig.get("Items", "recipes", item)
                self.log_deep_debug("Creating GUI for recipe of item", item,
                                    "| Recipe is", recipe)

                self.set_all(recipe)
                inventory_screen.current_recipe_button_id = item + "_item"
                inventory_screen.ids[
                    "merge_output_button"].button_id = item + "_item"

            else:
                self.log_deep_debug(
                    "Item", item,
                    "was clicked on but is doesnt have a merge recipe")

    def on_active(self, _instance, value: bool):
        if bool(value):
            self.opacity = 1

        else:
            self.opacity = 0

        self.log_deep_debug("self.active set to", value,
                            "and self.opacity set to", self.opacity, "for",
                            self)

    def do_layout(self, *args, **kwargs):
        w, h = self.size
        x, y = kwargs.get('pos', self.pos)

        pos_hints: dict[int, dict[int, float]] = graphicsConfig.getdict(
            "InventoryScreen", "merge_gui_pos_hints")

        c: TextBetterButton
        for i, c in enumerate(self.children):
            # size
            shw, shh = c.size_hint
            shw_min, shh_min = c.size_hint_min
            shw_max, shh_max = c.size_hint_max

            if shw is not None and shh is not None:  # Stolen from actual FloatLayout
                c_w = shw * w
                c_h = shh * h

                if shw_min is not None and c_w < shw_min:
                    c_w = shw_min
                elif shw_max is not None and c_w > shw_max:
                    c_w = shw_max

                if shh_min is not None and c_h < shh_min:
                    c_h = shh_min
                elif shh_max is not None and c_h > shh_max:
                    c_h = shh_max
                c.size = c_w, c_h
            elif shw is not None:
                c_w = shw * w

                if shw_min is not None and c_w < shw_min:
                    c_w = shw_min
                elif shw_max is not None and c_w > shw_max:
                    c_w = shw_max
                c.width = c_w
            elif shh is not None:
                c_h = shh * h

                if shh_min is not None and c_h < shh_min:
                    c_h = shh_min
                elif shh_max is not None and c_h > shh_max:
                    c_h = shh_max
                c.height = c_h

            # Pos
            pos_hint = pos_hints[i]
            c.center_x = x + (w * pos_hint[0])
            c.center_y = y + (h * pos_hint[1])

    def do_size(self, instance: Image, _value):
        self.x = instance.center_x - instance.norm_image_size[0] / 2.
        self.y = instance.center_y - instance.norm_image_size[1] / 2.
        self.size = instance.get_norm_image_size()
예제 #29
0
class NetGame(Widget):
    """Main widget

    :ivar kivy.properties.ObjectProperty main_window: reference to main widget (without background)
    :ivar kivy.properties.ObjectProperty grid: reference to the game board widget (`RelativeLayout`)
    :ivar kivy.properties.ObjectProperty timer: reference to the timer label
    :ivar kivy.properties.OptionProperty state: game state (possible states are waiting', 'running', 'paused' and
        'solved')
    :ivar kivy.properties.NumericProperty moves: number of moves played
    :ivar kivy.properties.NumericProperty expected_moves: the expected (close to minimal) number of moves required to
        solve the puzzle

    :ivar builder.Puzzle puzzle: the puzzle currently displayed
    :ivar int columns: number of columns of the game board
    :ivar int rows: number of rows of the game board
    :ivar grid.Grid board: the game board, a :class:`grid.Grid` of :class:`TileWidget` objects
    """
    main_window = ObjectProperty()
    grid = ObjectProperty()
    timer = ObjectProperty()
    state = OptionProperty('waiting',
                           options=['waiting', 'running', 'paused', 'solved'])
    moves = NumericProperty(0)
    expected_moves = NumericProperty(10)

    def __init__(self, puzzle, **kwargs):
        """Create a new widget displaying the given puzzle. Adjusts window size."""
        super(NetGame, self).__init__(**kwargs)
        self.puzzle = puzzle
        self.columns = puzzle.grid.columns
        self.rows = puzzle.grid.rows
        self.grid.width = self.columns * tilesize
        self.grid.height = self.rows * tilesize
        self.expected_moves = self.puzzle.expected_moves
        self.board = None
        self._last_changed = None  # last changed tile
        self.ids['pause_button'].bind(on_press=self.on_pause)
        # Set window size to at least size of game dialog
        Window.size = (max(self.main_window.width,
                           300), max(self.main_window.height, 350))
        Window.bind(on_key_down=self.on_key_down)
        self._setup_board()
        self._setup_walls()
        self.check_power()

    def _setup_board(self):
        self.board = GridContainer(Grid(self.columns,
                                        self.rows))  # for storing widgets
        self.grid.clear_widgets()  # RelativeLayout
        for x, y in itertools.product(range(self.columns), range(self.rows)):
            if self.puzzle.grid[x, y].entity == EntityType.source:
                t = SourceWidget(self.puzzle.grid[x, y],
                                 pos=(x * tilesize, y * tilesize))
            elif self.puzzle.grid[x, y].entity == EntityType.drain:
                t = DrainWidget(self.puzzle.grid[x, y],
                                pos=(x * tilesize, y * tilesize))
            else:
                t = TileWidget(self.puzzle.grid[x, y],
                               pos=(x * tilesize, y * tilesize))
            t.bind(on_change=self.on_tile_change)
            self.board[x, y] = t
            self.grid.add_widget(t)

    def _setup_walls(self):
        for w in self.puzzle.walls:
            self.grid.add_widget(WallWidget(w))
            # Double the walls on the board's borders
            if w.position.x == 0 and w.orientation == Wall.Orientation.vertical:
                w2 = Wall(Vector2d(w.position.x + self.columns, w.position.y),
                          w.orientation)
                # noinspection PyTypeChecker
                self.grid.add_widget(WallWidget(w2))
            if w.position.y == 0 and w.orientation == Wall.Orientation.horizontal:
                w2 = Wall(Vector2d(w.position.x, w.position.y + self.rows),
                          w.orientation)
                # noinspection PyTypeChecker
                self.grid.add_widget(WallWidget(w2))

    def on_pause(self, instance):
        """Handle pressing the pause button"""
        del instance  # unused parameter
        self.state = 'paused'
        self.timer.stop()

    def proceed(self):
        """Unpause the game"""
        if self._last_changed is None:
            self.state = 'waiting'
        else:
            self.state = 'running'
            self.timer.start()

    def reset(self):
        """Reset the puzzle to its original state"""
        # Unlock all tiles and set their rotational angle back to the original state
        for x, y in itertools.product(range(self.columns), range(self.rows)):
            self.board[x, y].powered = False
            self.board[x, y].locked = False
            self.board[x, y].angle = self.puzzle.grid[x, y].orientation.angle
            self.board[x,
                       y].orientation = self.puzzle.grid[x,
                                                         y].orientation.angle
        self.check_power()
        self.moves = 0
        self.timer.stop()
        self.timer.reset()
        self._last_changed = None
        self.state = 'waiting'

    def on_key_down(self, window, key, scancode, codepoint, modifiers):
        del window, scancode, codepoint, modifiers  # unused parameters
        if self.state in ('waiting',
                          'running') and key == Keyboard.keycodes['escape']:
            self.on_pause(self)
            return True

    def on_tile_change(self, tile):
        """Slot called when a tile is rotated"""
        if self.state == 'waiting':
            self.timer.start()
            self.state = 'running'
        if self._last_changed is not tile:
            self._last_changed = tile
            self.moves += 1
        self.check_power()
        solved = self._check_solved()
        if solved:
            self.timer.stop()
            self.state = 'solved'

    def check_power(self):
        """Check which tile is connected to the power source"""
        power = GridContainer(Grid(self.columns, self.rows), False)
        work = {self.puzzle.source}
        while work:
            parent = work.pop()
            power[parent] = True
            for direction in Direction:
                proto_child = parent + direction.vector
                if not self.puzzle.wrap and not self.board.grid.valid(
                        proto_child):
                    continue
                child = Vector2d(proto_child.x % self.columns,
                                 proto_child.y % self.rows)
                # noinspection PyTypeChecker
                if (not power[child]
                        and self._connected(parent, child, direction)
                        and not self._blocking_wall(parent, proto_child)):
                    work.add(child)
        for x, y in itertools.product(range(self.columns), range(self.rows)):
            self.board[x, y].powered = power[x, y]

    def _connected(self, parent, child, direction):
        """Check if both parent and child tile have arms in opposite directions"""
        parent_angle = direction.angle
        child_angle = (parent_angle + 180) % 360
        return self.board[parent].has_arm(
            parent_angle) and self.board[child].has_arm(child_angle)

    def _blocking_wall(self, parent, child):
        """Check if parent and child are separated by a wall"""
        orientation = Wall.Orientation.horizontal if parent.x == child.x else Wall.Orientation.vertical
        position = Vector2d(max(parent.x, child.x), max(parent.y, child.y))
        position = Vector2d(position.x % self.columns, position.y % self.rows)
        return Wall(position, orientation) in self.puzzle.walls

    def _check_solved(self):
        """Check if all tiles are connected to the power source"""
        return all(tile.powered for tile in self.board.items())

    def score(self) -> int:
        """Calculate the score"""
        if not self.state == 'solved':
            return 0
        weights = {
            LinkType.empty: 0,
            LinkType.dead_end: 4,
            LinkType.corner: 4,
            LinkType.straight: 2,
            LinkType.t_intersection: 4,
            LinkType.cross_intersection: 0
        }
        score = 0
        for x, y in itertools.product(range(self.columns), range(self.rows)):
            score += weights[self.puzzle.grid[x, y].link]
        score -= 2 * len(
            self.puzzle.walls)  # wall: -1 point for left and right square
        if not self.puzzle.wrap:  # implicit walls
            score -= self.puzzle.grid.columns + self.puzzle.grid.rows
        score *= self.puzzle.expected_moves / (self.puzzle.grid.columns *
                                               self.puzzle.grid.rows)
        score = score**2 / self.timer.seconds
        return round(score)
예제 #30
0
class ScrollView_Modified(StencilView):
    '''ScrollView class. See module documentation for more information.

    :Events:
        `on_scroll_start`
            Generic event fired when scrolling starts from touch.
        `on_scroll_move`
            Generic event fired when scrolling move from touch.
        `on_scroll_stop`
            Generic event fired when scrolling stops from touch.

    .. versionchanged:: 1.9.0
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
        now dispatched when scrolling to handle nested ScrollViews.

    .. versionchanged:: 1.7.0
        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
        been deprecated, use :attr:`effect_cls` instead.
    '''

    scroll_distance = NumericProperty(_scroll_distance)
    '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
    soon as the distance has been traveled, the :class:`ScrollView` will start
    to scroll, and no touch event will go to children.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 20 (pixels), according to the default value in user
    configuration.
    '''

    scroll_wheel_distance = NumericProperty('20sp')
    '''Distance to move when scrolling with a mouse wheel.
    It is advisable that you base this value on the dpi of your target device's
    screen.

    .. versionadded:: 1.8.0

    :attr:`scroll_wheel_distance` is a
    :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
    '''

    scroll_timeout = NumericProperty(_scroll_timeout)
    '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
    If the user has not moved :attr:`scroll_distance` within the timeout,
    the scrolling will be disabled, and the touch event will go to the
    children.

    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 55 (milliseconds) according to the default value in user
    configuration.

    .. versionchanged:: 1.5.0
        Default value changed from 250 to 55.
    '''

    scroll_x = NumericProperty(0.)
    '''X scrolling value, between 0 and 1. If 0, the content's left side will
    touch the left side of the ScrollView. If 1, the content's right side will
    touch the right side.

    This property is controled by :class:`ScrollView` only if
    :attr:`do_scroll_x` is True.

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

    scroll_y = NumericProperty(1.)
    '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
    touch the bottom side of the ScrollView. If 1, the content's top side will
    touch the top side.

    This property is controled by :class:`ScrollView` only if
    :attr:`do_scroll_y` is True.

    :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.
    '''

    do_scroll_x = BooleanProperty(True)
    '''Allow scroll on X axis.

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

    do_scroll_y = BooleanProperty(True)
    '''Allow scroll on Y axis.

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

    def _get_do_scroll(self):
        return (self.do_scroll_x, self.do_scroll_y)

    def _set_do_scroll(self, value):
        if type(value) in (list, tuple):
            self.do_scroll_x, self.do_scroll_y = value
        else:
            self.do_scroll_x = self.do_scroll_y = bool(value)
    do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll,
                              bind=('do_scroll_x', 'do_scroll_y'))
    '''Allow scroll on X or Y axis.

    :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
    (:attr:`do_scroll_x` + :attr:`do_scroll_y`)
    '''

    def _get_vbar(self):
        # must return (y, height) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vh = self._viewport.height
        h = self.height
        if vh < h or vh == 0:
            return 0, 1.
        ph = max(0.01, h / float(vh))
        sy = min(1.0, max(0.0, self.scroll_y))
        py = (1. - ph) * sy
        return (py, ph)

    vbar = AliasProperty(_get_vbar, None, bind=(
        'scroll_y', '_viewport', 'viewport_size'))
    '''Return a tuple of (position, size) of the vertical scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    percentage of the current scrollview height. This property is used
    internally for drawing the little vertical bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    def _get_hbar(self):
        # must return (x, width) in %
        # calculate the viewport size / scrollview size %
        if self._viewport is None:
            return 0, 1.
        vw = self._viewport.width
        w = self.width
        if vw < w or vw == 0:
            return 0, 1.
        pw = max(0.01, w / float(vw))
        sx = min(1.0, max(0.0, self.scroll_x))
        px = (1. - pw) * sx
        return (px, pw)

    hbar = AliasProperty(_get_hbar, None, bind=(
        'scroll_x', '_viewport', 'viewport_size'))
    '''Return a tuple of (position, size) of the horizontal scrolling bar.

    .. versionadded:: 1.2.0

    The position and size are normalized between 0-1, and represent a
    percentage of the current scrollview height. This property is used
    internally for drawing the little horizontal bar when you're scrolling.

    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
    '''

    bar_color = ListProperty([.7, .7, .7, .9])
    '''Color of horizontal / vertical scroll bar, in RGBA format.

    .. versionadded:: 1.2.0

    :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults
    to [.7, .7, .7, .9].
    '''

    bar_inactive_color = ListProperty([.7, .7, .7, .2])
    '''Color of horizontal / vertical scroll bar (in RGBA format), when no
    scroll is happening.

    .. versionadded:: 1.9.0

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

    bar_width = NumericProperty('2dp')
    '''Width of the horizontal / vertical scroll bar. The width is interpreted
    as a height for the horizontal bar.

    .. versionadded:: 1.2.0

    :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 2.
    '''

    bar_pos_x = OptionProperty('bottom', options=('top', 'bottom'))
    '''Which side of the ScrollView the horizontal scroll bar should go
    on. Possible values are 'top' and 'bottom'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
    defaults to 'bottom'.

    '''

    bar_pos_y = OptionProperty('right', options=('left', 'right'))
    '''Which side of the ScrollView the vertical scroll bar should go
    on. Possible values are 'left' and 'right'.

    .. versionadded:: 1.8.0

    :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'right'.

    '''

    bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
    '''Which side of the scroll view to place each of the bars on.

    :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`bar_pos_x`, :attr:`bar_pos_y`)
    '''

    bar_margin = NumericProperty(0)
    '''Margin between the bottom / right side of the scrollview when drawing
    the horizontal / vertical scroll bar.

    .. versionadded:: 1.2.0

    :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
    to 0
    '''

    effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
    '''Class effect to instanciate for X and Y axis.

    .. versionadded:: 1.7.0

    :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to :class:`DampedScrollEffect`.

    .. versionchanged:: 1.8.0
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
        resolve the class.

    '''

    effect_x = ObjectProperty(None, allownone=True)
    '''Effect to apply for the X axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

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

    effect_y = ObjectProperty(None, allownone=True)
    '''Effect to apply for the Y axis. If None is set, an instance of
    :attr:`effect_cls` will be created.

    .. versionadded:: 1.7.0

    :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None, read-only.
    '''

    viewport_size = ListProperty([0, 0])
    '''(internal) Size of the internal viewport. This is the size of your only
    child in the scrollview.
    '''

    scroll_type = OptionProperty(['content'], options=(['content'], ['bars'],
                                 ['bars', 'content'], ['content', 'bars']))
    '''Sets the type of scrolling to use for the content of the scrollview.
    Available options are: ['content'], ['bars'], ['bars', 'content'].

    .. versionadded:: 1.8.0

    :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults
    to ['content'].
    '''

    # private, for internal use only

    _viewport = ObjectProperty(None, allownone=True)
    _bar_color = ListProperty([0, 0, 0, 0])
    _effect_x_start_width = None
    _effect_y_start_height = None
    _update_effect_bounds_ev = None
    _bind_inactive_bar_color_ev = None

    def _set_viewport_size(self, instance, value):
        self.viewport_size = value

    def on__viewport(self, instance, value):
        if value:
            value.bind(size=self._set_viewport_size)
            self.viewport_size = value.size

    __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop')

    def __init__(self, **kwargs):
        self._touch = None
        self._trigger_update_from_scroll = Clock.create_trigger(
            self.update_from_scroll, -1)
        # create a specific canvas for the viewport
        from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
        self.canvas_viewport = Canvas()
        self.canvas = Canvas()
        with self.canvas_viewport.before:
            PushMatrix()
            self.g_translate = Translate(0, 0)
        with self.canvas_viewport.after:
            PopMatrix()

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

        self.register_event_type('on_scroll_start')
        self.register_event_type('on_scroll_move')
        self.register_event_type('on_scroll_stop')

        # now add the viewport canvas to our canvas
        self.canvas.add(self.canvas_viewport)

        effect_cls = self.effect_cls
        if isinstance(effect_cls, string_types):
            effect_cls = Factory.get(effect_cls)
        if self.effect_x is None and effect_cls is not None:
            self.effect_x = effect_cls(target_widget=self._viewport)
        if self.effect_y is None and effect_cls is not None:
            self.effect_y = effect_cls(target_widget=self._viewport)

        trigger_update_from_scroll = self._trigger_update_from_scroll
        update_effect_widget = self._update_effect_widget
        update_effect_x_bounds = self._update_effect_x_bounds
        update_effect_y_bounds = self._update_effect_y_bounds
        fbind = self.fbind
        fbind('width', update_effect_x_bounds)
        fbind('height', update_effect_y_bounds)
        fbind('viewport_size', self._update_effect_bounds)
        fbind('_viewport', update_effect_widget)
        fbind('scroll_x', trigger_update_from_scroll)
        fbind('scroll_y', trigger_update_from_scroll)
        fbind('pos', trigger_update_from_scroll)
        fbind('size', trigger_update_from_scroll)

        update_effect_widget()
        update_effect_x_bounds()
        update_effect_y_bounds()

    def on_effect_x(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_x)
            value.target_widget = self._viewport

    def on_effect_y(self, instance, value):
        if value:
            value.bind(scroll=self._update_effect_y)
            value.target_widget = self._viewport

    def on_effect_cls(self, instance, cls):
        if isinstance(cls, string_types):
            cls = Factory.get(cls)
        self.effect_x = cls(target_widget=self._viewport)
        self.effect_x.bind(scroll=self._update_effect_x)
        self.effect_y = cls(target_widget=self._viewport)
        self.effect_y.bind(scroll=self._update_effect_y)

    def _update_effect_widget(self, *args):
        if self.effect_x:
            self.effect_x.target_widget = self._viewport
        if self.effect_y:
            self.effect_y.target_widget = self._viewport

    def _update_effect_x_bounds(self, *args):
        if not self._viewport or not self.effect_x:
            return
        self.effect_x.min = -(self.viewport_size[0] - self.width)
        self.effect_x.max = 0
        self.effect_x.value = self.effect_x.min * self.scroll_x

    def _update_effect_y_bounds(self, *args):
        if not self._viewport or not self.effect_y:
            return
        self.effect_y.min = -(self.viewport_size[1] - self.height)
        self.effect_y.max = 0
        self.effect_y.value = self.effect_y.min * self.scroll_y

    def _update_effect_bounds(self, *args):
        if not self._viewport:
            return
        if self.effect_x:
            self._update_effect_x_bounds()
        if self.effect_y:
            self._update_effect_y_bounds()

    def _update_effect_x(self, *args):
        vp = self._viewport
        if not vp or not self.effect_x:
            return

        if self.effect_x.is_manual:
            sw = vp.width - self._effect_x_start_width
        else:
            sw = vp.width - self.width
        if sw < 1:
            return
        sx = self.effect_x.scroll / float(sw)
        self.scroll_x = -sx
        self._trigger_update_from_scroll()

    def _update_effect_y(self, *args):
        vp = self._viewport
        if not vp or not self.effect_y:
            return
        if self.effect_y.is_manual:
            sh = vp.height - self._effect_y_start_height
        else:
            sh = vp.height - self.height
        if sh < 1:
            return
        sy = self.effect_y.scroll / float(sh)
        self.scroll_y = -sy
        self._trigger_update_from_scroll()

    def to_local(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x - tx, y - ty

    def to_parent(self, x, y, **k):
        tx, ty = self.g_translate.xy
        return x + tx, y + ty

    def _apply_transform(self, m, pos=None):
        tx, ty = self.g_translate.xy
        m.translate(tx, ty, 0)
        return super(ScrollView_Modified, self)._apply_transform(m, (0, 0))

    def simulate_touch_down(self, touch):
        # at this point the touch is in parent coords
        touch.push()
        touch.apply_transform_2d(self.to_local)
        ret = super(ScrollView_Modified, self).on_touch_down(touch)
        touch.pop()
        return ret

    def on_touch_down(self, touch):
        if self.dispatch('on_scroll_start', touch):
            self._touch = touch
            touch.grab(self)
            return True

    def _touch_in_handle(self, pos, size, touch):
        x, y = pos
        width, height = size
        return x <= touch.x <= x + width and y <= touch.y <= y + height

    def on_scroll_start(self, touch, check_children=True):
        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_start', touch):
                touch.pop()
                return True
            touch.pop()

        if not self.collide_point(*touch.pos):
            touch.ud[self._get_uid('svavoid')] = True
            return
        if self.disabled:
            return True
        if self._touch or (not (self.do_scroll_x or self.do_scroll_y)):
            return self.simulate_touch_down(touch)

        # handle mouse scrolling, only if the viewport size is bigger than the
        # scrollview size, and if the user allowed to do it
        vp = self._viewport
        if not vp:
            return True
        scroll_type = self.scroll_type
        ud = touch.ud
        scroll_bar = 'bars' in scroll_type

        # check if touch is in bar_x(horizontal) or bay_y(bertical)
        ud['in_bar_x'] = ud['in_bar_y'] = False
        width_scrollable = vp.width > self.width
        height_scrollable = vp.height > self.height
        bar_pos_x = self.bar_pos_x[0]
        bar_pos_y = self.bar_pos_y[0]

        d = {'b': True if touch.y < self.y + self.bar_width else False,
             't': True if touch.y > self.top - self.bar_width else False,
             'l': True if touch.x < self.x + self.bar_width else False,
             'r': True if touch.x > self.right - self.bar_width else False}
        if scroll_bar:
            if (width_scrollable and d[bar_pos_x]):
                ud['in_bar_x'] = True
            if (height_scrollable and d[bar_pos_y]):
                ud['in_bar_y'] = True

        if vp and 'button' in touch.profile and \
                touch.button.startswith('scroll'):
            btn = touch.button
            m = self.scroll_wheel_distance
            e = None

            if ((btn == 'scrolldown' and self.scroll_y >= 1) or
                (btn == 'scrollup' and self.scroll_y <= 0) or
                (btn == 'scrollleft' and self.scroll_x >= 1) or
                (btn == 'scrollright' and self.scroll_x <= 0)):
                return False

            if (self.effect_x and self.do_scroll_y and height_scrollable
                    and btn in ('scrolldown', 'scrollup')):
                e = self.effect_x if ud['in_bar_x'] else self.effect_y

            elif (self.effect_y and self.do_scroll_x and width_scrollable
                    and btn in ('scrollleft', 'scrollright')):
                e = self.effect_y if ud['in_bar_y'] else self.effect_x

            if e:
                if btn in ('scrolldown', 'scrollleft'):
                    e.value = max(e.value - m, e.min)
                    e.velocity = 0
                elif btn in ('scrollup', 'scrollright'):
                    e.value = min(e.value + m, e.max)
                    e.velocity = 0
                touch.ud[self._get_uid('svavoid')] = True
                e.trigger_velocity_update()
            return True

        in_bar = ud['in_bar_x'] or ud['in_bar_y']
        if scroll_type == ['bars'] and not in_bar:
            return self.simulate_touch_down(touch)

        if in_bar:
            if (ud['in_bar_y'] and not
                    self._touch_in_handle(
                        self._handle_y_pos, self._handle_y_size, touch)):
                self.scroll_y = (touch.y - self.y) / self.height
            elif (ud['in_bar_x'] and not
                    self._touch_in_handle(
                        self._handle_x_pos, self._handle_x_size, touch)):
                self.scroll_x = (touch.x - self.x) / self.width

        # no mouse scrolling, so the user is going to drag the scrollview with
        # this touch.
        self._touch = touch
        uid = self._get_uid()

        ud[uid] = {
            'mode': 'unknown',
            'dx': 0,
            'dy': 0,
            'user_stopped': in_bar,
            'frames': Clock.frames,
            'time': touch.time_start}

        if self.do_scroll_x and self.effect_x and not ud['in_bar_x']:
            self._effect_x_start_width = self.width
            self.effect_x.start(touch.x)
            self._scroll_x_mouse = self.scroll_x
        if self.do_scroll_y and self.effect_y and not ud['in_bar_y']:
            self._effect_y_start_height = self.height
            self.effect_y.start(touch.y)
            self._scroll_y_mouse = self.scroll_y

        if not in_bar:
            Clock.schedule_once(self._change_touch_mode,
                                self.scroll_timeout / 1000.)
        return True

    def on_touch_move(self, touch):
        if self._touch is not touch:
            # touch is in parent
            touch.push()
            touch.apply_transform_2d(self.to_local)
            super(ScrollView_Modified, self).on_touch_move(touch)
            touch.pop()
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True

        if touch.ud.get(self._get_uid()) is None:
            return super(ScrollView_Modified, self).on_touch_move(touch)

        touch.ud['sv.handled'] = {'x': False, 'y': False}
        if self.dispatch('on_scroll_move', touch):
            return True

    def on_scroll_move(self, touch):
        if self._get_uid('svavoid') in touch.ud:
            return False

        touch.push()
        touch.apply_transform_2d(self.to_local)
        if self.dispatch_children('on_scroll_move', touch):
            touch.pop()
            return True
        touch.pop()

        rv = True

        # By default this touch can be used to defocus currently focused
        # widget, like any touch outside of ScrollView.
        touch.ud['sv.can_defocus'] = True

        uid = self._get_uid()
        if not uid in touch.ud:
            self._touch = False
            return self.on_scroll_start(touch, False)
        ud = touch.ud[uid]
        mode = ud['mode']

        # check if the minimum distance has been travelled
        if mode == 'unknown' or mode == 'scroll':
            if not touch.ud['sv.handled']['x'] and self.do_scroll_x \
                    and self.effect_x:
                width = self.width
                if touch.ud.get('in_bar_x', False):
                    dx = touch.dx / float(width - width * self.hbar[1])
                    self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.)
                    self._trigger_update_from_scroll()
                else:
                    if self.scroll_type != ['bars']:
                        self.effect_x.update(touch.x)
                if self.scroll_x < 0 or self.scroll_x > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['x'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False
            if not touch.ud['sv.handled']['y'] and self.do_scroll_y \
                    and self.effect_y:
                height = self.height
                if touch.ud.get('in_bar_y', False):
                    dy = touch.dy / float(height - height * self.vbar[1])
                    self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.)
                    self._trigger_update_from_scroll()
                else:
                    if self.scroll_type != ['bars']:
                        self.effect_y.update(touch.y)
                if self.scroll_y < 0 or self.scroll_y > 1:
                    rv = False
                else:
                    touch.ud['sv.handled']['y'] = True
                # Touch resulted in scroll should not defocus focused widget
                touch.ud['sv.can_defocus'] = False

        if mode == 'unknown':
            ud['dx'] += abs(touch.dx)
            ud['dy'] += abs(touch.dy)
            if ((ud['dx'] > self.scroll_distance) or
                    (ud['dy'] > self.scroll_distance)):
                if not self.do_scroll_x and not self.do_scroll_y:
                    # touch is in parent, but _change expects window coords
                    touch.push()
                    touch.apply_transform_2d(self.to_local)
                    touch.apply_transform_2d(self.to_window)
                    self._change_touch_mode()
                    touch.pop()
                    return
                mode = 'scroll'
            ud['mode'] = mode

        if mode == 'scroll':
            ud['dt'] = touch.time_update - ud['time']
            ud['time'] = touch.time_update
            ud['user_stopped'] = True

        return rv
    
    def on_touch_up(self, touch):
        if self._touch is not touch and self.uid not in touch.ud:
            # touch is in parents
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if super(ScrollView_Modified, self).on_touch_up(touch):
                touch.pop()
                return True
            touch.pop()
            return False

        if self.dispatch('on_scroll_stop', touch):
            touch.ungrab(self)
            if not touch.ud.get('sv.can_defocus', True):
                # Focused widget should stay focused
                FocusBehavior.ignored_touch.append(touch)
            return True

    def on_scroll_stop(self, touch, check_children=True):
        self._touch = None

        if check_children:
            touch.push()
            touch.apply_transform_2d(self.to_local)
            if self.dispatch_children('on_scroll_stop', touch):
                touch.pop()
                return True
            touch.pop()

        if self._get_uid('svavoid') in touch.ud:
            return
        if self._get_uid() not in touch.ud:
            return False

        self._touch = None
        uid = self._get_uid()
        ud = touch.ud[uid]
        if self.do_scroll_x and self.effect_x:
            if not touch.ud.get('in_bar_x', False) and\
                    self.scroll_type != ['bars']:
                self.effect_x.stop(touch.x)
        if self.do_scroll_y and self.effect_y and\
                self.scroll_type != ['bars']:
            if not touch.ud.get('in_bar_y', False):
                self.effect_y.stop(touch.y)
        if ud['mode'] == 'unknown':
            # we must do the click at least..
            # only send the click if it was not a click to stop
            # autoscrolling
            if not ud['user_stopped']:
                self.simulate_touch_down(touch)
            Clock.schedule_once(partial(self._do_touch_up, touch), .2)

        ev = self._update_effect_bounds_ev
        if ev is None:
            ev = self._update_effect_bounds_ev = Clock.create_trigger(
                self._update_effect_bounds)
        ev()

        # if we do mouse scrolling, always accept it
        if 'button' in touch.profile and touch.button.startswith('scroll'):
            return True

        return self._get_uid() in touch.ud

    def scroll_to(self, widget, padding=10, animate=True):
        '''Scrolls the viewport to ensure that the given widget is visible,
        optionally with padding and animation. If animate is True (the
        default), then the default animation parameters will be used.
        Otherwise, it should be a dict containing arguments to pass to
        :class:`~kivy.animation.Animation` constructor.

        .. versionadded:: 1.9.1
        '''
        if not self.parent:
            return

        if isinstance(padding, (int, float)):
            padding = (padding, padding)

        pos = self.parent.to_widget(*widget.to_window(*widget.pos))
        cor = self.parent.to_widget(*widget.to_window(widget.right,
                                                      widget.top))

        dx = dy = 0

        if pos[1] < self.y:
            dy = self.y - pos[1] + dp(padding[1])
        elif cor[1] > self.top:
            dy = self.top - cor[1] - dp(padding[1])

        if pos[0] < self.x:
            dx = self.x - pos[0] + dp(padding[0])
        elif cor[0] > self.right:
            dx = self.right - cor[0] - dp(padding[0])

        dsx, dsy = self.convert_distance_to_scroll(dx, dy)
        sxp = min(1, max(0, self.scroll_x - dsx))
        syp = min(1, max(0, self.scroll_y - dsy))

        if animate:
            if animate is True:
                animate = {'d': 0.2, 't': 'out_quad'}
            Animation.stop_all(self, 'scroll_x', 'scroll_y')
            Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self)
        else:
            self.scroll_x = sxp
            self.scroll_y = syp

    def convert_distance_to_scroll(self, dx, dy):
        '''Convert a distance in pixels to a scroll distance, depending on the
        content size and the scrollview size.

        The result will be a tuple of scroll distance that can be added to
        :data:`scroll_x` and :data:`scroll_y`
        '''
        if not self._viewport:
            return 0, 0
        vp = self._viewport
        if vp.width > self.width:
            sw = vp.width - self.width
            sx = dx / float(sw)
        else:
            sx = 0
        if vp.height > self.height:
            sh = vp.height - self.height
            sy = dy / float(sh)
        else:
            sy = 1
        return sx, sy

    def update_from_scroll(self, *largs):
        '''Force the reposition of the content, according to current value of
        :attr:`scroll_x` and :attr:`scroll_y`.

        This method is automatically called when one of the :attr:`scroll_x`,
        :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or
        if the size of the content changes.
        '''
        if not self._viewport:
            return
        vp = self._viewport

        # update from size_hint
        if vp.size_hint_x is not None:
            vp.width = vp.size_hint_x * self.width
        if vp.size_hint_y is not None:
            vp.height = vp.size_hint_y * self.height

        if vp.width > self.width:
            sw = vp.width - self.width
            x = self.x - self.scroll_x * sw
        else:
            x = self.x
        if vp.height > self.height:
            sh = vp.height - self.height
            y = self.y - self.scroll_y * sh
        else:
            y = self.top - vp.height

        # from 1.8.0, we now use a matrix by default, instead of moving the
        # widget position behind. We set it here, but it will be a no-op most of
        # the time.
        vp.pos = 0, 0
        self.g_translate.xy = x, y

        # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0)
        # fade to bar_inactive_color when no scroll is happening.
        ev = self._bind_inactive_bar_color_ev
        if ev is None:
            ev = self._bind_inactive_bar_color_ev = Clock.create_trigger(
                self._bind_inactive_bar_color, .5)
        self.funbind('bar_inactive_color', self._change_bar_color)
        Animation.stop_all(self, '_bar_color')
        self.fbind('bar_color', self._change_bar_color)
        self._bar_color = self.bar_color
        ev()

    def _bind_inactive_bar_color(self, *l):
        self.funbind('bar_color', self._change_bar_color)
        self.fbind('bar_inactive_color', self._change_bar_color)
        Animation(
            _bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self)

    def _change_bar_color(self, inst, value):
        self._bar_color = value

    #
    # Private
    #
    def add_widget(self, widget, index=0):
        if self._viewport:
            raise Exception('ScrollView accept only one widget')
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView_Modified, self).add_widget(widget, index)
        self.canvas = canvas
        self._viewport = widget
        widget.bind(size=self._trigger_update_from_scroll)
        self._trigger_update_from_scroll()

    def remove_widget(self, widget):
        canvas = self.canvas
        self.canvas = self.canvas_viewport
        super(ScrollView_Modified, self).remove_widget(widget)
        self.canvas = canvas
        if widget is self._viewport:
            self._viewport = None

    def _get_uid(self, prefix='sv'):
        return '{0}.{1}'.format(prefix, self.uid)

    def _change_touch_mode(self, *largs):
        if not self._touch:
            return
        uid = self._get_uid()
        touch = self._touch
        if uid not in touch.ud:
            self._touch = False
            return
        ud = touch.ud[uid]
        if ud['mode'] != 'unknown' or ud['user_stopped']:
            return
        diff_frames = Clock.frames - ud['frames']

        # in order to be able to scroll on very slow devices, let at least 3
        # frames displayed to accumulate some velocity. And then, change the
        # touch mode. Otherwise, we might never be able to compute velocity, and
        # no way to scroll it. See #1464 and #1499
        if diff_frames < 3:
            Clock.schedule_once(self._change_touch_mode, 0)
            return

        if self.do_scroll_x and self.effect_x:
            self.effect_x.cancel()
        if self.do_scroll_y and self.effect_y:
            self.effect_y.cancel()
        # XXX the next line was in the condition. But this stop
        # the possibily to "drag" an object out of the scrollview in the
        # non-used direction: if you have an horizontal scrollview, a
        # vertical gesture will not "stop" the scroll view to look for an
        # horizontal gesture, until the timeout is done.
        # and touch.dx + touch.dy == 0:
        touch.ungrab(self)
        self._touch = None
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        touch.apply_transform_2d(self.to_parent)
        self.simulate_touch_down(touch)
        touch.pop()
        return

    def _do_touch_up(self, touch, *largs):
        # touch is in window coords
        touch.push()
        touch.apply_transform_2d(self.to_widget)
        super(ScrollView_Modified, self).on_touch_up(touch)
        touch.pop()
        # don't forget about grab event!
        for x in touch.grab_list[:]:
            touch.grab_list.remove(x)
            x = x()
            if not x:
                continue
            touch.grab_current = x
            # touch is in window coords
            touch.push()
            touch.apply_transform_2d(self.to_widget)
            super(ScrollView_Modified, self).on_touch_up(touch)
            touch.pop()
        touch.grab_current = None