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
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
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()
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)
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))
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
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)
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)
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()
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
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
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)
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
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
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)
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)
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()
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)
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)
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()
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
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
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)
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))
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()
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
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()
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)
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