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 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_nyc 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): self.electrum_config.set_key('use_change', self.use_change, True) 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 on_new_intent(self, intent): if intent.getScheme() != 'newyorkcoin': return uri = intent.getDataString() self.set_URI(uri) 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") self.wallet.clear_coin_price_cache() self._trigger_update_history() def on_fee_histogram(self, *args): self._trigger_update_history() 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 self.pause_time = 0 App.__init__(self) #, **kwargs) title = _('Electrum-NYC App') self.electrum_config = config = kwargs.get('config', None) 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', []) self.gui_object = kwargs.get('gui_object', None) self.daemon = self.gui_object.daemon self.fx = self.daemon.fx self.use_rbf = config.get('use_rbf', True) self.use_change = config.get('use_change', 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) # cached dialogs self._settings_dialog = None self._password_dialog = None self.fee_status = self.electrum_config.get_fee_status() def on_pr(self, pr): if not self.wallet: self.show_error(_('No wallet loaded.')) return if pr.verify(self.wallet.contacts): key = self.wallet.invoices.add(pr) if self.invoices_screen: self.invoices_screen.update() status = self.wallet.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_nyc.bitcoin import base_decode, is_address data = data.strip() if is_address(data): self.set_URI(data) return if data.startswith('newyorkcoin:'): self.set_URI(data) return # try to decode transaction from electrum_nyc.transaction import Transaction from electrum_nyc.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, addr): self.switch_to('receive') self.receive_screen.screen.address = addr def show_pr_details(self, req, status, is_invoice): from electrum_nyc.util import format_time requestor = req.get('requestor') exp = req.get('exp') memo = req.get('memo') amount = req.get('amount') fund = req.get('fund') popup = Builder.load_file( 'electrum_nyc/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 popup.fund = fund if fund else 0 txid = req.get('txid') popup.tx_hash = txid or '' popup.on_open = lambda: popup.ids.output_list.update( req.get('outputs', [])) popup.export = self.export_private_keys popup.open() def show_addr_details(self, req, status): from electrum_nyc.util import format_time fund = req.get('fund') isaddr = 'y' popup = Builder.load_file( 'electrum_nyc/gui/kivy/uix/ui_screens/invoice.kv') popup.isaddr = isaddr popup.is_invoice = False popup.status = status popup.requestor = req.get('address') popup.fund = fund if fund else 0 popup.export = self.export_private_keys 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, on_qr_failure) 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) 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_nyc/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() 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 newyorkcoin: 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']) # load wallet self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # 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) 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(), 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 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 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_nyc/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() else: popup = Builder.load_file('electrum_nyc/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_nyc.gui.kivy.uix.dialogs') Factory.register('QRCodeWidget', module='electrum_nyc.gui.kivy.uix.qrcodewidget') # preload widgets. Remove this if you want to load the widgets on demand #Cache.append('electrum_nyc_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) #Cache.append('electrum_nyc_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_nyc/gui/icons/electrum_nyc.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): 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) 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: status = _("Synchronizing...") elif server_lag > 1: status = _("Server lagging") 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 get_max_amount(self): from electrum_nyc.transaction import TxOutput if run_hook('abort_send', self): return '' inputs = self.wallet.get_spendable_coins(None, self.electrum_config) if not inputs: return '' addr = str( self.send_screen.screen.address) or self.wallet.dummy_address() outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] try: tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) 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 #@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-NYC', message, app_icon=icon, app_name='Electrum-NYC') 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, 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 show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://electrum_nyc/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_nyc/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_nyc/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 invoices_dialog(self, screen): from .uix.dialogs.invoices import InvoicesDialog if len(self.wallet.invoices.sorted_list()) == 0: self.show_info(' '.join([ _('No saved invoices.'), _('Signed invoices are saved automatically when you scan them.' ), _('You may also save unsigned requests or contact addresses using the save button.' ) ])) return popup = InvoicesDialog(self, screen, None) popup.update() popup.open() def requests_dialog(self, screen): from .uix.dialogs.requests import RequestsDialog if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0: self.show_info(_('No saved requests.')) return popup = RequestsDialog(self, screen, None) popup.update() popup.open() def addresses_dialog(self, screen): from .uix.dialogs.addresses import AddressesDialog popup = AddressesDialog(self, screen, None) popup.update() popup.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() dirname = os.path.dirname(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() 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.text = _('Seed') + ':\n' + seed if passphrase: label.text += '\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 RecycleView(ScrollView): """RecycleView is a flexible view for providing a limited window into a large data set. See module documentation for more informations. """ # internals _adapter = None _layout_manager = None _container = None _refresh_trigger = None _refresh_flags = { 'all': True, 'data': True, 'data_size': True, 'data_add': True, 'viewport': True } '''These flags indicate how much the view is out of sync and needs to be synchronized with the data. The goal is that the minimum amount of synchronization should be done. The flags are ordered such that for each flag, all the ops required for the proceeding flags are reduced by some known info about the change causing the refresh. Meaning of the flags: -all: Initial setup of the recycleview itself. E.g. do_scroll_x. Updates everything. -data: Signal that the data changed and all the attributes/size/pos may have changed so they need to be recomputed and all attrs re-applied. This -data_size: Signal that the pos/size attrs of the data may have changed, new data may have been added, and or old data was removed. However, no existing data attrs was changed, beyond the size/pos values. This means that we don't need to re-apply the data attrs, other than size/pos for existing data insatances because they did not change. Added data will get new class instances and will have their values applied. -data_add: Similar to `data_size`, except that data, if added, was added at the end. This will allow further possible optimizations, but may be implemented as `data_size`. -viewport: The items visible changed, so we have to move our viewport. Other than viewport, nothing is recalculated. ''' def __init__(self, **kwargs): self._refresh_flags = dict(self._refresh_flags) self._refresh_trigger = Clock.create_trigger(self.refresh_views, -1) if self._layout_manager is None: self.layout_manager = LinearRecycleLayoutManager() if self._adapter is None: self.adapter = RecycleAdapter() super(RecycleView, self).__init__(**kwargs) if self._container is None: self.container = RecycleViewLayout(size_hint=(None, None)) fbind = self.fbind if _kivy_1_9_1 else self.fast_bind fbind('size', self.ask_refresh_from_data, extent='data_size') fbind('scroll_x', self.ask_refresh_viewport) fbind('scroll_y', self.ask_refresh_viewport) self._refresh_trigger() def refresh_views(self, *largs, **kwargs): flags = self._refresh_flags flags.update(kwargs) lm = self.layout_manager try: append = False update = flags['all'] if update: flags['all'] = False lm.recycleview_setup() else: update = flags['data'] if update: flags['data'] = False self.layout_manager.clear_layout() else: append = flags['data_add'] and not flags['data_size'] update = flags['data_size'] or flags['data_add'] if update: flags['data_size'] = flags['data_add'] = False lm.compute_positions_and_sizes(append) if update or flags['viewport']: flags['viewport'] = False if self.data: lm.compute_visible_views() except LayoutChangeException: # at a minimum we will have to recompute the size flags['data_size'] = True self.refresh_views() def ask_refresh_all(self, *largs): self._refresh_flags['all'] = True self._refresh_trigger() def ask_refresh_from_data(self, *largs, **kwargs): '''Accepts extent as a flag kwarg. ''' extent = kwargs.get('extent', 'data') if extent not in ('data', 'data_size', 'data_add'): raise ValueError('{} is not a valid extent'.format(extent)) self.adapter.dispatch('on_data_changed', extent=extent) def ask_refresh_viewport(self, *largs): self._refresh_flags['viewport'] = True self._refresh_trigger() def get_views(self, i_start, i_end): return self.adapter.get_views(i_start, i_end) @property def observable_dict(self): '''It's specific to the adapter present when called. ''' return self.adapter.observable_dict def _dispatch_prop_on_source(self, prop_name, *largs): '''Dispatches the prop of this class when the adapter/layout_manager property changes. ''' getattr(self.__class__, prop_name).dispatch(self) def _handle_ask_data_refresh(self, *largs, **kwargs): self._refresh_flags[kwargs['extent']] = True self._refresh_trigger() def _get_adapter(self): return self._adapter def _set_adapter(self, value): adapter = self._adapter if value is adapter: return if adapter is not None: adapter.detach_recycleview() funbind = adapter.funbind if _kivy_1_9_1 else adapter.fast_unbind funbind('on_data_changed', self._handle_ask_data_refresh) funbind('viewclass', self._dispatch_prop_on_source, 'viewclass') funbind('key_viewclass', self._dispatch_prop_on_source, 'key_viewclass') funbind('data', self._dispatch_prop_on_source, 'data') if value is None: self._adapter = adapter = RecycleAdapter() else: if not isinstance(value, RecycleAdapter): raise ValueError( 'Expected object based on RecycleAdapter, got {}'.format( value.__class__)) self._adapter = adapter = value adapter.attach_recycleview(self) fbind = adapter.fbind if _kivy_1_9_1 else adapter.fast_bind fbind('on_data_changed', self._handle_ask_data_refresh) fbind('viewclass', self._dispatch_prop_on_source, 'viewclass') fbind('key_viewclass', self._dispatch_prop_on_source, 'key_viewclass') fbind('data', self._dispatch_prop_on_source, 'data') self.ask_refresh_from_data() return True adapter = AliasProperty(_get_adapter, _set_adapter) """Adapter responsible for providing views that represent items in a data set.""" def _get_layout_manager(self): return self._layout_manager def _set_layout_manager(self, value): lm = self._layout_manager if value is lm: return if lm is not None: lm.detach_recycleview() funbind = lm.funbind if _kivy_1_9_1 else lm.fast_unbind funbind('default_size', self._dispatch_prop_on_source, 'default_size') funbind('key_size', self._dispatch_prop_on_source, 'key_size') if value is None: self._layout_manager = lm = LinearRecycleLayoutManager() else: if not isinstance(value, RecycleLayoutManager): raise ValueError( 'Expected object based on RecycleLayoutManager, got {}'. format(value.__class__)) self._layout_manager = lm = value lm.attach_recycleview(self) fbind = lm.fbind if _kivy_1_9_1 else lm.fast_bind fbind('default_size', self._dispatch_prop_on_source, 'default_size') fbind('key_size', self._dispatch_prop_on_source, 'key_size') if self.adapter is not None: self.ask_refresh_from_data() return True layout_manager = AliasProperty(_get_layout_manager, _set_layout_manager) """Layout manager responsible to position views within the recycleview """ def _get_container(self): return self._container def _set_container(self, value): container = self._container if value is container: return if container is not None: self.remove_widget(container) if value is None: c = self._container = RecycleViewLayout(size_hint=(None, None)) else: c = self._container = value self.add_widget(c) self.ask_refresh_from_data(extent='data_size') return True container = AliasProperty(_get_container, _set_container) """Container. """ # or easier way to use def _get_data(self): return self.adapter.data def _set_data(self, value): self.adapter.data = value data = AliasProperty(_get_data, _set_data, bind=["adapter"]) """Set the data on the current adapter """ def _get_viewclass(self): return self.adapter.viewclass def _set_viewclass(self, value): self.adapter.viewclass = value viewclass = AliasProperty(_get_viewclass, _set_viewclass, bind=["adapter"]) """Set the viewclass on the current adapter """ def _get_key_viewclass(self): return self.adapter.key_viewclass def _set_key_viewclass(self, value): self.adapter.key_viewclass = value key_viewclass = AliasProperty(_get_key_viewclass, _set_key_viewclass, bind=["adapter"]) """Set the key viewclass on the current adapter """ def _get_default_size(self): return self.layout_manager.default_size def _set_default_size(self, value): self.layout_manager.default_size = value default_size = AliasProperty(_get_default_size, _set_default_size, bind=["layout_manager"]) """Set the default size on the current `layout_manager` """ def _get_key_size(self): return self.layout_manager.key_size def _set_key_size(self, value): self.layout_manager.key_size = value key_size = AliasProperty(_get_key_size, _set_key_size, bind=["layout_manager"]) """Set the key to look for the size on the current `layout_manager`
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 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 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 Image(Widget): '''Image class, see module documentation for more information. ''' source = StringProperty(None) '''Filename / source of your image. :attr:`source` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' texture = ObjectProperty(None, allownone=True) '''Texture object of the image. The texture represents the original, loaded image texture. It is stretched and positioned during rendering according to the :attr:`allow_stretch` and :attr:`keep_ratio` properties. Depending of the texture creation, the value will be a :class:`~kivy.graphics.texture.Texture` or a :class:`~kivy.graphics.texture.TextureRegion` object. :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' texture_size = ListProperty([0, 0]) '''Texture size of the image. This represents the original, loaded image texture size. .. warning:: The texture size is set after the texture property. So if you listen to the change on :attr:`texture`, the property texture_size will not be up-to-date. Use self.texture.size instead. ''' def get_image_ratio(self): if self.texture: return self.texture.width / float(self.texture.height) return 1. mipmap = BooleanProperty(False) '''Indicate if you want OpenGL mipmapping to be applied to the texture. Read :ref:`mipmap` for more information. .. versionadded:: 1.0.7 :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' image_ratio = AliasProperty(get_image_ratio, None, bind=('texture', )) '''Ratio of the image (width / float(height). :attr:`image_ratio` is an :class:`~kivy.properties.AliasProperty` and is read-only. ''' color = ListProperty([1, 1, 1, 1]) '''Image color, in the format (r, g, b, a). This attribute can be used to 'tint' an image. Be careful: if the source image is not gray/white, the color will not really work as expected. .. versionadded:: 1.0.6 :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' allow_stretch = BooleanProperty(False) '''If True, the normalized image size will be maximized to fit in the image box. Otherwise, if the box is too tall, the image will not be stretched more than 1:1 pixels. .. versionadded:: 1.0.7 :attr:`allow_stretch` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' keep_ratio = BooleanProperty(True) '''If False along with allow_stretch being True, the normalized image size will be maximized to fit in the image box and ignores the aspect ratio of the image. Otherwise, if the box is too tall, the image will not be stretched more than 1:1 pixels. .. versionadded:: 1.0.8 :attr:`keep_ratio` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' keep_data = BooleanProperty(False) '''If True, the underlaying _coreimage will store the raw image data. This is useful when performing pixel based collision detection. .. versionadded:: 1.3.0 :attr:`keep_data` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' anim_delay = NumericProperty(.25) '''Delay the animation if the image is sequenced (like an animated gif). If anim_delay is set to -1, the animation will be stopped. .. versionadded:: 1.0.8 :attr:`anim_delay` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.25 (4 FPS). ''' anim_loop = NumericProperty(0) '''Number of loops to play then stop animating. 0 means keep animating. .. versionadded:: 1.9.0 :attr:`anim_loop` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' nocache = BooleanProperty(False) '''If this property is set True, the image will not be added to the internal cache. The cache will simply ignore any calls trying to append the core image. .. versionadded:: 1.6.0 :attr:`nocache` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def get_norm_image_size(self): if not self.texture: return self.size ratio = self.image_ratio w, h = self.size tw, th = self.texture.size # ensure that the width is always maximized to the containter width if self.allow_stretch: if not self.keep_ratio: return w, h iw = w else: iw = min(w, tw) # calculate the appropriate height ih = iw / ratio # if the height is too higher, take the height of the container # and calculate appropriate width. no need to test further. :) if ih > h: if self.allow_stretch: ih = h else: ih = min(h, th) iw = ih * ratio return iw, ih norm_image_size = AliasProperty(get_norm_image_size, None, bind=('texture', 'size', 'image_ratio', 'allow_stretch')) '''Normalized image size within the widget box. This size will always fit the widget size and will preserve the image ratio. :attr:`norm_image_size` is an :class:`~kivy.properties.AliasProperty` and is read-only. ''' def __init__(self, **kwargs): self._coreimage = None self._loops = 0 super(Image, self).__init__(**kwargs) fbind = self.fbind update = self.texture_update fbind('source', update) fbind('mipmap', update) if self.source: update() self.on_anim_delay(self, kwargs.get('anim_delay', .25)) def texture_update(self, *largs): if not self.source: self.texture = None else: filename = resource_find(self.source) self._loops = 0 if filename is None: return Logger.error( 'Image: Error reading file {filename}'.format( filename=self.source)) mipmap = self.mipmap if self._coreimage is not None: self._coreimage.unbind(on_texture=self._on_tex_change) try: if PY2: filename = filename.decode('utf-8') self._coreimage = ci = CoreImage(filename, mipmap=mipmap, anim_delay=self.anim_delay, keep_data=self.keep_data, nocache=self.nocache) except: Logger.error('Image: Error loading texture {filename}'.format( filename=self.source)) self._coreimage = ci = None if ci: ci.bind(on_texture=self._on_tex_change) self.texture = ci.texture def on_anim_delay(self, instance, value): self._loop = 0 if self._coreimage is None: return self._coreimage.anim_delay = value if value < 0: self._coreimage.anim_reset(False) def on_texture(self, instance, value): if value is not None: self.texture_size = list(value.size) def _on_tex_change(self, *largs): # update texture from core image self.texture = self._coreimage.texture ci = self._coreimage if self.anim_loop and ci._anim_index == len(ci._image.textures) - 1: self._loops += 1 if self.anim_loop == self._loops: ci.anim_reset(False) self._loops = 0 def reload(self): '''Reload image from disk. This facilitates re-loading of images from disk in case the image content changes. .. versionadded:: 1.3.0 Usage:: im = Image(source = '1.jpg') # -- do something -- im.reload() # image will be re-loaded from disk ''' try: self._coreimage.remove_from_cache() except AttributeError: pass oldsource = self.source self.source = '' self.source = oldsource def on_nocache(self, *args): if self.nocache and self._coreimage: self._coreimage.remove_from_cache() self._coreimage._nocache = True
class CheckBox(ToggleButtonBehavior, Widget): '''CheckBox class, see module documentation for more information. ''' def _get_active(self): return self.state == 'down' def _set_active(self, value): self.state = 'down' if value else 'normal' active = AliasProperty(_get_active, _set_active, bind=('state', ), cache=True) '''Indicates if the switch is active or inactive. :attr:`active` is a boolean and reflects and sets whether the underlying :attr:`~kivy.uix.button.Button.state` is 'down' (True) or 'normal' (False). It is a :class:`~kivy.properties.AliasProperty`, which accepts boolean values and defaults to False. .. versionchanged:: 1.11.0 It changed from a BooleanProperty to a AliasProperty. ''' background_checkbox_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_off') '''Background image of the checkbox used for the default graphical representation when the checkbox is not active. .. versionadded:: 1.9.0 :attr:`background_checkbox_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_off'. ''' background_checkbox_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_on') '''Background image of the checkbox used for the default graphical representation when the checkbox is active. .. versionadded:: 1.9.0 :attr:`background_checkbox_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_on'. ''' background_checkbox_disabled_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_disabled_off') '''Background image of the checkbox used for the default graphical representation when the checkbox is disabled and not active. .. versionadded:: 1.9.0 :attr:`background_checkbox_disabled_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_disabled_off'. ''' background_checkbox_disabled_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_disabled_on') '''Background image of the checkbox used for the default graphical representation when the checkbox is disabled and active. .. versionadded:: 1.9.0 :attr:`background_checkbox_disabled_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_disabled_on'. ''' background_radio_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_off') '''Background image of the radio button used for the default graphical representation when the radio button is not active. .. versionadded:: 1.9.0 :attr:`background_radio_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_off'. ''' background_radio_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_on') '''Background image of the radio button used for the default graphical representation when the radio button is active. .. versionadded:: 1.9.0 :attr:`background_radio_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_on'. ''' background_radio_disabled_normal = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_disabled_off') '''Background image of the radio button used for the default graphical representation when the radio button is disabled and not active. .. versionadded:: 1.9.0 :attr:`background_radio_disabled_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_disabled_off'. ''' background_radio_disabled_down = StringProperty( 'atlas://data/images/defaulttheme/checkbox_radio_disabled_on') '''Background image of the radio button used for the default graphical representation when the radio button is disabled and active. .. versionadded:: 1.9.0 :attr:`background_radio_disabled_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/checkbox_radio_disabled_on'. ''' color = ColorProperty([1, 1, 1, 1]) '''Color is used for tinting the default graphical representation of checkbox and radio button (images). Color is in the format (r, g, b, a). .. versionadded:: 1.10.0 :attr:`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`. ''' def __init__(self, **kwargs): self.fbind('state', self._on_state) super(CheckBox, self).__init__(**kwargs) def _on_state(self, instance, value): if self.group and self.state == 'down': self._release_group(self) def on_group(self, *largs): super(CheckBox, self).on_group(*largs) if self.active: self._release_group(self)
class CircularTimePicker(BoxLayout): """Widget that makes use of :class:`CircularHourPicker` and :class:`CircularMinutePicker` to create a user-friendly, animated time picker like the one seen on Android. See module documentation for more details. """ hours = NumericProperty(0) """The hours, in military format (0-23). :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and defaults to 0 (12am). """ minutes = NumericProperty(0) """The minutes. :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ time_list = ReferenceListProperty(hours, minutes) """Packs :attr:`hours` and :attr:`minutes` in a list for convenience. :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`. """ # military = BooleanProperty(False) time_format = StringProperty( "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]" ) """String that will be formatted with the time and shown in the time label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]". """ ampm_format = StringProperty( "[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]" ) """String that will be formatted and shown in the AM/PM label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n[color={pm_color}][ref=pm]PM[/ref][/color]". """ picker = OptionProperty("hours", options=("minutes", "hours")) """Currently shown time picker. Can be one of "minutes", "hours". :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and defaults to "hours". """ selector_color = ListProperty([.337, .439, .490]) """Color of the number selector and of the highlighted text. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([1, 1, 1]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ _am = BooleanProperty(True) _h_picker = ObjectProperty(None) _m_picker = ObjectProperty(None) _bound = DictProperty({}) def _get_time(self): return datetime.time(*self.time_list) def _set_time(self, dt): self.time_list = [dt.hour, dt.minute] time = AliasProperty(_get_time, _set_time, bind=("time_list", )) """Selected time as a datetime.time object. :attr:`time` is an :class:`~kivy.properties.AliasProperty`. """ def _get_picker(self): if self.picker == "hours": return self._h_picker return self._m_picker _picker = AliasProperty(_get_picker, None) def _get_time_text(self): hc = rgb_to_hex( *self.selector_color) if self.picker == "hours" else rgb_to_hex( *self.color) mc = rgb_to_hex( *self.selector_color) if self.picker == "minutes" else rgb_to_hex( *self.color) h = self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12 m = self.minutes return self.time_format.format(hours_color=hc, minutes_color=mc, hours=h, minutes=m) time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) def _get_ampm_text(self): amc = rgb_to_hex(*self.selector_color) if self._am else rgb_to_hex( *self.color) pmc = rgb_to_hex(*self.selector_color) if not self._am else rgb_to_hex( *self.color) return self.ampm_format.format(am_color=amc, pm_color=pmc) ampm_text = AliasProperty(_get_ampm_text, None, bind=("hours", "ampm_format", "_am")) def __init__(self, **kw): super(CircularTimePicker, self).__init__(**kw) if self.hours >= 12: self._am = False self.bind(time_list=self.on_time_list, picker=self._switch_picker, _am=self.on_ampm) self._h_picker = CircularHourPicker() self._m_picker = CircularMinutePicker() Clock.schedule_once(self.on_selected) Clock.schedule_once(self.on_time_list) Clock.schedule_once(self._init_later) Clock.schedule_once(lambda *a: self._switch_picker(noanim=True)) # print "TIMEee", self.time def _init_later(self, *args): self.ids.timelabel.bind(on_ref_press=self.on_ref_press) self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press) def on_ref_press(self, ign, ref): if ref == "hours": self.picker = "hours" elif ref == "minutes": self.picker = "minutes" elif ref == "am": self._am = True elif ref == "pm": self._am = False def on_selected(self, *a): if not self._picker: return if self.picker == "hours": hours = self._picker.selected if self._am else self._picker.selected + 12 if hours == 24 and not self._am: hours = 12 elif hours == 12 and self._am: hours = 0 self.hours = hours elif self.picker == "minutes": self.minutes = self._picker.selected def on_time_list(self, *a): # print "TIME", self.time if not self._picker: return if self.picker == "hours": self._picker.selected = self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 elif self.picker == "minutes": self._picker.selected = self.minutes def on_ampm(self, *a): if self._am: self.hours = self.hours if self.hours < 12 else self.hours - 12 else: self.hours = self.hours if self.hours >= 12 else self.hours + 12 def _switch_picker(self, *a, **kw): noanim = "noanim" in kw if noanim: noanim = kw["noanim"] try: container = self.ids.picker_container except (AttributeError, NameError): Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim)) if self.picker == "hours": picker = self._h_picker prevpicker = self._m_picker elif self.picker == "minutes": picker = self._m_picker prevpicker = self._h_picker if len(self._bound) > 0: prevpicker.unbind(selected=self.on_selected) self.unbind(**self._bound) picker.bind(selected=self.on_selected) self._bound = { "selector_color": picker.setter("selector_color"), "color": picker.setter("color"), "selector_alpha": picker.setter("selector_alpha") } self.bind(**self._bound) if len(container._bound) > 0: container.unbind(**container._bound) container._bound = { "size": picker.setter("size"), "pos": picker.setter("pos") } container.bind(**container._bound) picker.pos = container.pos picker.size = container.size picker.selector_color = self.selector_color picker.color = self.color picker.selector_alpha = self.selector_alpha if noanim: # print "noanim" if prevpicker in container.children: container.remove_widget(prevpicker) if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) else: if prevpicker in container.children: anim = Animation(scale=1.5, d=.5, t="in_back") & Animation( opacity=0, d=.5, t="in_cubic") anim.start(prevpicker) Clock.schedule_once( lambda *a: container.remove_widget(prevpicker), .5) # .31) picker.scale = 1.5 picker.opacity = 0 if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) anim = Animation(scale=1, d=.5, t="out_back") & Animation( opacity=1, d=.5, t="out_cubic") Clock.schedule_once(lambda *a: anim.start(picker), .3)
class EditScreen(Screen): price = NumericProperty() product_ids = DictProperty() # there's no SetProperty, this is the closest match product_count = AliasProperty(lambda self: len(self.product_ids), bind=("product_ids",)) def _add_price_char(self, char: str, max_chars: int = 4): if len(str(self.price)) >= max_chars: return try: self.price = int(f"{self.price}{char}") except ValueError: return def _delete_price_char(self): try: self.price = int(f"0{self.price}"[:-1]) except ValueError: return def _toggle_product_id(self, id_: int, button: KToggleButton): if button.state == "normal": try: del self.product_ids[id_] except KeyError: pass elif button.state == "down": self.product_ids[id_] = None def _submit_succeeded(self, *args): self.price = 0 self.product_ids = dict() for button in self.ids.products_keypad.children: button.state = "normal" KStatusPopup(text=_("Saved")).open() @staticmethod def _submit_failed(*args): KStatusPopup(text=_("Could not save")).open() def build(self): price_keypad: GridLayout = self.ids.price_keypad for char in (7, 8, 9, 0, 4, 5, 6, None, 1, 2, 3): if char is None: button = KButton() else: button = KButton(text=str(char)) button.on_press = lambda char_copy=char: self._add_price_char(char_copy) price_keypad.add_widget(button) button = KIconButton(icon_text="\ue14a") button.on_press = lambda: self._delete_price_char() price_keypad.add_widget(button) products_keypad: GridLayout = self.ids.products_keypad for id_, name in enumerate(get_product_names()): button = KToggleButton(text=name) button.on_press = lambda id_copy=id_, button_copy=button: self._toggle_product_id(id_copy, button_copy) products_keypad.add_widget(button) def submit(self): KLoadingPopup(text=_("Saving")).open() UrlRequest( "http://localhost:5000", method="POST", req_body=json.dumps({"price": self.price, "product_ids": list(self.product_ids.keys())}), req_headers={"Content-Type": "application/json"}, on_success=self._submit_succeeded, on_failure=self._submit_failed, on_error=self._submit_failed, )
class CircularNumberPicker(CircularLayout): """A circular number picker based on CircularLayout. A selector will help you pick a number. You can also set :attr:`multiples_of` to make it show only some numbers and use the space in between for the other numbers. """ min = NumericProperty(0) """The first value of the range. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ max = NumericProperty(0) """The last value of the range. Note that it behaves like xrange, so the actual last displayed value will be :attr:`max` - 1. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ range = ReferenceListProperty(min, max) """Packs :attr:`min` and :attr:`max` into a list for convenience. See their documentation for further information. :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`. """ multiples_of = NumericProperty(1) """Only show numbers that are multiples of this number. The other numbers will be selectable, but won't have their own label. :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ selector_color = ListProperty([.337, .439, .490]) """Color of the number selector. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([1, 1, 1]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ selected = NumericProperty(None) """Currently selected number. :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and defaults to :attr:`min`. """ number_size_factor = NumericProperty(.5) """Font size scale factor fot the :class:`Number`s. :attr:`number_size_factor` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.5. """ number_format_string = StringProperty("{}") """String that will be formatted with the selected number as the first argument. Can be anything supported by :meth:`str.format` (es. "{:02d}"). :attr:`number_format_string` is a :class:`~kivy.properties.StringProperty` and defaults to "{}". """ scale = NumericProperty(1) """Canvas scale factor. Used in :class:`CircularTimePicker` transitions. :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ _selection_circle = ObjectProperty(None) _selection_line = ObjectProperty(None) _selection_dot = ObjectProperty(None) _selection_dot_color = ObjectProperty(None) _selection_color = ObjectProperty(None) _center_dot = ObjectProperty(None) _center_color = ObjectProperty(None) def _get_items(self): return self.max - self.min items = AliasProperty(_get_items, None) def _get_shown_items(self): c = 0 for i in range(*self.range): if i % self.multiples_of == 0: c += 1 return c shown_items = AliasProperty(_get_shown_items, None) def __init__(self, **kw): self._trigger_genitems = Clock.create_trigger(self._genitems, -1) self.bind(min=self._trigger_genitems, max=self._trigger_genitems, multiples_of=self._trigger_genitems) super(CircularNumberPicker, self).__init__(**kw) self.selected = self.min self.bind(selected=self.on_selected, pos=self.on_selected, size=self.on_selected) cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [ i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) ] esize = [self.delta_radii * self.number_size_factor * 2] * 2 dsize = [i * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 color = list(self.selector_color) with self.canvas: self._selection_color = Color(*(color + [self.selector_alpha])) self._selection_circle = Ellipse(pos=epos, size=esize) self._selection_line = Line(points=[cx, cy, sx, sy]) self._selection_dot_color = Color(*(color + [dot_alpha])) self._selection_dot = Ellipse(pos=dpos, size=dsize) self._center_color = Color(*self.color) self._center_dot = Ellipse(pos=cpos, size=csize) self.bind(selector_color=lambda ign, c: setattr( self._selection_color, "rgba", c + [self.selector_alpha])) self.bind(selector_color=lambda ign, c: setattr( self._selection_dot_color, "rgb", c)) self.bind(color=lambda ign, c: setattr(self._center_color, "rgb", c)) Clock.schedule_once(self._genitems) Clock.schedule_once( self.on_selected) # Just to make sure pos/size are set def _genitems(self, *a): self.clear_widgets() for i in range(*self.range): if i % self.multiples_of != 0: continue n = Number(text=self.number_format_string.format(i), size_factor=self.number_size_factor, color=self.color) self.bind(color=n.setter("color")) self.add_widget(n) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return touch.grab(self) self.selected = self.number_at_pos(*touch.pos) def on_touch_move(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).on_touch_move(touch) self.selected = self.number_at_pos(*touch.pos) def on_touch_up(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).on_touch_up(touch) touch.ungrab(self) def on_selected(self, *a): cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [ i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) ] esize = [self.delta_radii * self.number_size_factor * 2] * 2 dsize = [i * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 if self._selection_circle: self._selection_circle.pos = epos self._selection_circle.size = esize if self._selection_line: self._selection_line.points = [cx, cy, sx, sy] if self._selection_dot: self._selection_dot.pos = dpos self._selection_dot.size = dsize if self._selection_dot_color: self._selection_dot_color.a = dot_alpha if self._center_dot: self._center_dot.pos = cpos self._center_dot.size = csize # print self.selected def pos_for_number(self, n): """Returns the center x, y coordinates for a given number. """ if self.items == 0: return 0, 0 radius = min(self.width - self.padding[0] - self.padding[2], self.height - self.padding[1] - self.padding[3]) / 2. middle_r = radius * sum(self.radius_hint) / 2. cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sign = +1. angle_offset = radians(self.start_angle) if self.direction == 'cw': angle_offset = 2 * pi - angle_offset sign = -1. quota = 2 * pi / self.items mult_quota = 2 * pi / self.shown_items angle = angle_offset + n * sign * quota if self.items == self.shown_items: angle += quota / 2 else: angle -= mult_quota / 2 # kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery x = cos(angle) * middle_r + cx y = sin(angle) * middle_r + cy return x, y def number_at_pos(self, x, y): """Returns the number at a given x, y position. The number is found using the widget's center as a starting point for angle calculations. Not thoroughly tested, may yield wrong results. """ if self.items == 0: return self.min cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] lx = x - cx ly = y - cy quota = 2 * pi / self.items mult_quota = 2 * pi / self.shown_items if lx == 0 and ly > 0: angle = pi / 2 elif lx == 0 and ly < 0: angle = 3 * pi / 2 else: angle = atan(ly / lx) if lx < 0 and ly > 0: angle += pi if lx > 0 and ly < 0: angle += 2 * pi if lx < 0 and ly < 0: angle += pi angle += radians(self.start_angle) if self.direction == "cw": angle = 2 * pi - angle if mult_quota != quota: angle -= mult_quota / 2 if angle < 0: angle += 2 * pi elif angle > 2 * pi: angle -= 2 * pi return int(angle / quota) + self.min
class CircularLayout(Layout): """Circular layout class. See module documentation for more information. """ padding = VariableListProperty([0, 0, 0, 0]) '''Padding between the layout box and it's children: [padding_left, padding_top, padding_right, padding_bottom]. padding also accepts a two argument form [padding_horizontal, padding_vertical] and a one argument form [padding]. .. versionchanged:: 1.7.0 Replaced NumericProperty with VariableListProperty. :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0, 0, 0]. ''' start_angle = NumericProperty(0) '''Angle (in degrees) at which the first widget will be placed. Start counting angles from the X axis, going counterclockwise. :attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and defaults to 0 (start from the right). ''' circle_quota = BoundedNumericProperty(360, min=0, max=360) '''Size (in degrees) of the part of the circumference that will actually be used to place widgets. :attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 360 (all the circumference). ''' direction = OptionProperty("ccw", options=("cw", "ccw")) '''Direction of widgets in the circle. :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise). ''' outer_radius_hint = NumericProperty(1) '''Sets the size of the outer circle. A number greater than 1 will make the widgets larger than the actual widget, a number smaller than 1 will leave a gap. :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' inner_radius_hint = NumericProperty(.6) '''Sets the size of the inner circle. A number greater than :attr:`outer_radius_hint` will cause glitches. The closest it is to :attr:`outer_radius_hint`, the smallest will be the widget in the layout. :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint) '''Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint` in a list for convenience. See their documentation for more details. :attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`. ''' def _get_delta_radii(self): radius = min(self.width - self.padding[0] - self.padding[2], self.height - self.padding[1] - self.padding[3]) / 2. outer_r = radius * self.outer_radius_hint inner_r = radius * self.inner_radius_hint return outer_r - inner_r delta_radii = AliasProperty(_get_delta_radii, None, bind=("radius_hint", "padding", "size")) def __init__(self, **kwargs): super(CircularLayout, self).__init__(**kwargs) self.bind( start_angle=self._trigger_layout, parent=self._trigger_layout, # padding=self._trigger_layout, children=self._trigger_layout, size=self._trigger_layout, radius_hint=self._trigger_layout, pos=self._trigger_layout) def do_layout(self, *largs): # optimize layout by preventing looking at the same attribute in a loop len_children = len(self.children) if len_children == 0: return selfcx = self.center_x selfcy = self.center_y direction = self.direction cquota = radians(self.circle_quota) # selfw = self.width # selfh = self.height start_angle_r = radians(self.start_angle) padding_left = self.padding[0] padding_top = self.padding[1] padding_right = self.padding[2] padding_bottom = self.padding[3] padding_x = padding_left + padding_right padding_y = padding_top + padding_bottom radius = min(self.width - padding_x, self.height - padding_y) / 2. outer_r = radius * self.outer_radius_hint inner_r = radius * self.inner_radius_hint middle_r = radius * sum(self.radius_hint) / 2. delta_r = outer_r - inner_r # calculate maximum space used by size_hint stretch_weight_angle = 0. for w in self.children: sha = w.size_hint_x if sha is None: raise ValueError( "size_hint_x cannot be None in a CircularLayout") else: stretch_weight_angle += sha sign = +1. angle_offset = start_angle_r if direction == 'cw': angle_offset = 2 * pi - start_angle_r sign = -1. for c in reversed(self.children): sha = c.size_hint_x shs = c.size_hint_y angle_quota = cquota / stretch_weight_angle * sha angle = angle_offset + (sign * angle_quota / 2) angle_offset += sign * angle_quota # kived: looking it up, yes. x = cos(angle) * radius + centerx; y = sin(angle) * radius + centery ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top c.center_x = ccx c.center_y = ccy if shs: s = delta_r * shs c.width = s c.height = s
class Exchanger(EventDispatcher): ''' Provide exchanges rate between crypto and different national currencies. See Module Documentation for details. ''' symbols = { 'ALL': u'Lek', 'AED': u'د.إ', 'AFN': u'؋', 'ARS': u'$', 'AMD': u'֏', 'AWG': u'ƒ', 'ANG': u'ƒ', 'AOA': u'Kz', 'BDT': u'৳', 'BHD': u'BD', 'BIF': u'FBu', 'BTC': u'BTC', 'BTN': u'Nu', 'CDF': u'FC', 'CHF': u'CHF', 'CLF': u'UF', 'CLP': u'$', 'CVE': u'$', 'DJF': u'Fdj', 'DZD': u'دج', 'AUD': u'$', 'AZN': u'ман', 'BSD': u'$', 'BBD': u'$', 'BYR': u'p', 'CRC': u'₡', 'BZD': u'BZ$', 'BMD': u'$', 'BOB': u'$b', 'BAM': u'KM', 'BWP': u'P', 'BGN': 'uлв', 'BRL': u'R$', 'BND': u'$', 'KHR': u'៛', 'CAD': u'$', 'ERN': u'Nfk', 'ETB': u'Br', 'KYD': u'$', 'USD': u'$', 'CLP': u'$', 'HRK': u'kn', 'CUP': u'₱', 'CZK': u'Kč', 'DKK': u'kr', 'DOP': u'RD$', 'XCD': u'$', 'EGP': u'£', 'SVC': u'$', 'EEK': u'kr', 'EUR': u'€', u'FKP': u'£', 'FJD': u'$', 'GHC': u'¢', 'GIP': u'£', 'GTQ': u'Q', 'GBP': u'£', 'GYD': u'$', 'HNL': u'L', 'HKD': u'$', 'HUF': u'Ft', 'ISK': u'kr', 'INR': u'₹', 'IDR': u'Rp', 'IRR': u'﷼', 'IMP': '£', 'ILS': '₪', 'COP': '$', 'JMD': u'J$', 'JPY': u'¥', 'JEP': u'£', 'KZT': u'лв', 'KPW': u'₩', 'KRW': u'₩', 'KGS': u'лв', 'LAK': u'₭', 'LVL': u'Ls', 'CNY': u'¥' } _use_exchange = OptionProperty('Blockchain', options=EXCHANGES) '''This is the exchange to be used for getting the currency exchange rates ''' _currency = StringProperty('EUR') '''internal use only ''' def _set_currency(self, value): value = str(value) if self.use_exchange == 'CoinDesk': self._update_cd_currency(self.currency) return self._currency = value self.parent.electrum_config.set_key('currency', value, True) def _get_currency(self): self._currency = self.parent.electrum_config.get('currency', 'EUR') return self._currency currency = AliasProperty(_get_currency, _set_currency, bind=('_currency', )) currencies = ListProperty(['EUR', 'GBP', 'USD']) '''List of currencies supported by the current exchanger plugin. :attr:`currencies` is a `ListProperty` default to ['Eur', 'GBP'. 'USD']. ''' def _get_useex(self): if not self.parent: return self._use_exchange self._use_exchange = self.parent.electrum_config.get( 'use_exchange', 'Blockchain') return self._use_exchange def _set_useex(self, value): if not self.parent: return self._use_exchange self.parent.electrum_config.set_key('use_exchange', value, True) self._use_exchange = value use_exchange = AliasProperty(_get_useex, _set_useex, bind=('_use_exchange', )) def __init__(self, parent): super(Exchanger, self).__init__() self.parent = parent self.quote_currencies = None self.exchanges = EXCHANGES self.history_exchanges = HISTORY_EXCHNAGES def exchange(self, btc_amount, quote_currency): if self.quote_currencies is None: return None quote_currencies = self.quote_currencies.copy() if quote_currency not in quote_currencies: return None return btc_amount * decimal.Decimal(quote_currencies[quote_currency]) def get_history_rate(self, item, btc_amt, mintime, maxtime): def on_success(request, response): response = json.loads(response) try: hrate = response['bpi'][mintime] hrate = abs(btc_amt) * decimal.Decimal(hrate) Cache.append('history_rate', uid, hrate) except KeyError: hrate = 'not found' self.parent.set_history_rate(item, hrate) # Check local cache before getting data from remote exchange = 'coindesk' uid = '{}:{}'.format(exchange, mintime) hrate = Cache.get('history_rate', uid) if hrate: return hrate req = UrlRequest(url='https://api.coindesk.com/v1/bpi/historical' '/close.json?start={}&end={}'.format( mintime, maxtime), on_success=on_success, timeout=15) return None def update_rate(self, dt): ''' This is called from :method:`start` every X seconds; to update the rates for currencies for the currently selected exchange. ''' if not self.parent.network or not self.parent.network.is_connected(): return update_rates = { "BitcoinAverage": self.update_ba, "BitcoinVenezuela": self.update_bv, "BitPay": self.update_bp, "Blockchain": self.update_bc, "BTCChina": self.update_CNY, "CaVirtEx": self.update_cv, "CoinDesk": self.update_cd, "Coinbase": self.update_cb, "LocalBitcoins": self.update_lb, "Winkdex": self.update_wd, } try: update_rates[self.use_exchange]() except KeyError: return def update_wd(self): def on_success(request, response): response = json.loads(response) quote_currencies = {'USD': 0.0} lenprices = len(response["prices"]) usdprice = response['prices'][lenprices - 1]['y'] try: quote_currencies["USD"] = decimal.Decimal(usdprice) except KeyError: pass self.quote_currencies = quote_currencies self.parent.set_currencies(quote_currencies) req = UrlRequest(url='https://winkdex.com/static/data/0_600_288.json', on_success=on_success, timeout=5) def update_cd_currency(self, currency): def on_success(request, response): response = json.loads(response) quote_currencies = self.quote_currencies quote_currencies[currency] =\ str(response['bpi'][str(currency)]['rate_float']) self.parent.set_currencies(quote_currencies) req = UrlRequest( url='https://api.coindesk.com/v1/bpi/currentprice/'\ + str(currency) + '.json',on_success=on_success, timeout=5) def update_cd(self): def on_success(request, response): quote_currencies = {} response = json.loads(response) for cur in response: quote_currencies[str(cur["currency"])] = 0.0 self.quote_currencies = quote_currencies self.update_cd_currency(self.currency) req = UrlRequest( url='https://api.coindesk.com/v1/bpi/supported-currencies.json', on_success=on_success, timeout=5) def update_cv(self): def on_success(request, response): response = json.loads(response) quote_currencies = {"CAD": 0.0} cadprice = response["last"] try: quote_currencies["CAD"] = decimal.Decimal(cadprice) self.quote_currencies = quote_currencies except KeyError: pass self.parent.set_currencies(quote_currencies) req = UrlRequest(url='https://www.cavirtex.com/api/CAD/ticker.json', on_success=on_success, timeout=5) def update_CNY(self): def on_success(request, response): quote_currencies = {"CNY": 0.0} cnyprice = response["ticker"]["last"] try: quote_currencies["CNY"] = decimal.Decimal(cnyprice) self.quote_currencies = quote_currencies except KeyError: pass self.parent.set_currencies(quote_currencies) req = UrlRequest(url='https://data.btcchina.com/data/ticker', on_success=on_success, timeout=5) def update_bp(self): def on_success(request, response): quote_currencies = {} try: for r in response: quote_currencies[str(r['code'])] = decimal.Decimal( r['rate']) self.quote_currencies = quote_currencies except KeyError: pass self.parent.set_currencies(quote_currencies) req = UrlRequest(url='https://bitpay.com/api/rates', on_success=on_success, timeout=5) def update_cb(self): def _lookup_rate(response, quote_id): return decimal.Decimal(str(response[str(quote_id)])) def on_success(request, response): quote_currencies = {} try: for r in response: if r[:7] == "btc_to_": quote_currencies[r[7:].upper()] =\ _lookup_rate(response, r) self.quote_currencies = quote_currencies except KeyError: pass self.parent.set_currencies(quote_currencies) req = UrlRequest( url='https://coinbase.com/api/v1/currencies/exchange_rates', on_success=on_success, timeout=5) def update_bc(self): def _lookup_rate(response, quote_id): return decimal.Decimal(str(response[str(quote_id)]["15m"])) def on_success(request, response): quote_currencies = {} try: for r in response: quote_currencies[r] = _lookup_rate(response, r) self.quote_currencies = quote_currencies except KeyError, TypeError: pass self.parent.set_currencies(quote_currencies) req = UrlRequest(url='https://blockchain.info/ticker', on_success=on_success, timeout=5)
class MDFloatingActionButton(ThemableBehavior, CircularRippleBehavior, RoundElevationBehavior, ButtonBehavior, AnchorLayout): _bg_color_down = ListProperty([]) background_color = ListProperty() background_color_down = ListProperty() background_color_disabled = ListProperty() theme_text_color = OptionProperty( None, allownone=True, options=['Primary', 'Secondary', 'Hint', 'Error', 'Custom']) text_color = ListProperty(None, allownone=True) def _get_bg_color_down(self): return self._bg_color_down def _set_bg_color_down(self, color, alpha=None): if len(color) == 2: self._bg_color_down = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._bg_color_down[3] = alpha elif len(color) == 4: self._bg_color_down = color background_color_down = AliasProperty(_get_bg_color_down, _set_bg_color_down, bind=('_bg_color_down', )) _bg_color_disabled = ListProperty([]) def _get_bg_color_disabled(self): return self._bg_color_disabled def _set_bg_color_disabled(self, color, alpha=None): if len(color) == 2: self._bg_color_disabled = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._bg_color_disabled[3] = alpha elif len(color) == 4: self._bg_color_disabled = color background_color_disabled = AliasProperty(_get_bg_color_disabled, _set_bg_color_disabled, bind=('_bg_color_disabled', )) icon = StringProperty('android') _elev_norm = NumericProperty(6) def _get_elev_norm(self): return self._elev_norm def _set_elev_norm(self, value): self._elev_norm = value if value <= 12 else 12 self._elev_raised = (value + 6) if value + 6 <= 12 else 12 self.elevation = self._elev_norm elevation_normal = AliasProperty(_get_elev_norm, _set_elev_norm, bind=('_elev_norm', )) _elev_raised = NumericProperty(12) def _get_elev_raised(self): return self._elev_raised def _set_elev_raised(self, value): self._elev_raised = value if value + self._elev_norm <= 12 else 12 elevation_raised = AliasProperty(_get_elev_raised, _set_elev_raised, bind=('_elev_raised', )) def __init__(self, **kwargs): if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: self.elevation_raised = self.elevation_normal + 6 elif self.elevation_raised == 0: self.elevation_raised = 12 super(MDFloatingActionButton, self).__init__(**kwargs) self.elevation_press_anim = Animation(elevation=self.elevation_raised, duration=.2, t='out_quad') self.elevation_release_anim = Animation( elevation=self.elevation_normal, duration=.2, t='out_quad') def _set_ellipse(self, instance, value): ellipse = self.ellipse ripple_rad = self.ripple_rad ellipse.size = (ripple_rad, ripple_rad) ellipse.pos = (self.center_x - ripple_rad / 2., self.center_y - ripple_rad / 2.) def on_disabled(self, instance, value): super(MDFloatingActionButton, self).on_disabled(instance, value) if self.disabled: self.elevation = 0 else: self.elevation = self.elevation_normal def on_touch_down(self, touch): if not self.disabled: if touch.is_mouse_scrolling: return False if not self.collide_point(touch.x, touch.y): return False if self in touch.ud: return False self.elevation_press_anim.stop(self) self.elevation_press_anim.start(self) return super(MDFloatingActionButton, self).on_touch_down(touch) def on_touch_up(self, touch): if not self.disabled: if touch.grab_current is not self: return super(ButtonBehavior, self).on_touch_up(touch) self.elevation_release_anim.stop(self) self.elevation_release_anim.start(self) return super(MDFloatingActionButton, self).on_touch_up(touch) def on_elevation_normal(self, instance, value): self.elevation = value def on_elevation_raised(self, instance, value): if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: self.elevation_raised = self.elevation_normal + 6 elif self.elevation_raised == 0: self.elevation_raised = 12
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. warning:: Adding a `__del__` method to a class derived from Widget with python prior to 3.4 will disable automatic garbage collection for instances of that class. This is because the Widget class creates reference cycles, thereby `preventing garbage collection <https://docs.python.org/2/library/gc.html#gc.garbage>`_. .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. .. code-block:: python >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. .. code-block:: python >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list .. versionadded:: 1.0.5 .. code-block:: python >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. .. code-block:: python >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.8.1 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size, with_stencilbuffer=True) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Translate(-self.x, -self.y, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # we pass index only when we are going on the parent. # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # if we want to continue with our parent, just do it if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # self is root, if wanted to loopback from first element then -> if not loopback: return # if we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: bool, defaults to False If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: bool, defaults to False If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.8.1 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: bool, defaults to False If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.8.1 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`), ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`), ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.), ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`). See :attr:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write:: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used by the :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a empty dict {}. The :attr:`ids` are populated for each root level widget definition. For example:: # in kv <MyWidget@Widget>: id: my_widget Label: id: label_widget Widget: id: inner_widget Label: id: inner_label TextInput: id: text_input OtherWidget: id: other_widget <OtherWidget@Widget> id: other_widget Label: id: other_label TextInput: id: other_textinput Then, in python: .. code-block:: python >>> widget = MyWidget() >>> print(widget.ids) {'other_widget': <weakproxy at 041CFED0 to OtherWidget at 041BEC38>, 'inner_widget': <weakproxy at 04137EA0 to Widget at 04138228>, 'inner_label': <weakproxy at 04143540 to Label at 04138260>, 'label_widget': <weakproxy at 04137B70 to Label at 040F97A0>, 'text_input': <weakproxy at 041BB5D0 to TextInput at 041BEC00>} >>> print(widget.ids['other_widget'].ids) {'other_textinput': <weakproxy at 041DBB40 to TextInput at 041BEF48>, 'other_label': <weakproxy at 041DB570 to Label at 041BEEA0>} >>> print(widget.ids['label_widget'].ids) {} ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as: .. code-block:: python frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class ScreenManager(FloatLayout): '''Screen manager. This is the main class that will control your :class:`Screen` stack, and memory. By default, the manager will show only one screen at time. ''' current = StringProperty(None) '''Name of the screen currently show, or the screen to show. :: from kivy.uix.screenmanager import ScreenManager, Screen sm = ScreenManager() sm.add_widget(Screen(name='first')) sm.add_widget(Screen(name='second')) # by default, the first added screen will be showed. If you want to show # another one, just set the current string: sm.current = 'second' ''' transition = ObjectProperty(SwapTransition()) '''Transition object to use for animate the screen that will be hidden, and the screen that will be showed. By default, an instance of :class:`SwapTransition` will be given. For example, if you want to change to a :class:`WipeTransition`:: from kivy.uix.screenmanager import ScreenManager, Screen, WipeTransition sm = ScreenManager(transition=WipeTransition()) sm.add_widget(Screen(name='first')) sm.add_widget(Screen(name='second')) # by default, the first added screen will be showed. If you want to show another one, just set the current string: sm.current = 'second' ''' screens = ListProperty() '''List of all the :class:`Screen` widgets added. You must not change the list manually. Use :meth:`Screen.add_widget` instead. :data:`screens` is a :class:`~kivy.properties.ListProperty`, default to [], read-only. ''' current_screen = ObjectProperty(None) '''Contain the current displayed screen. You must not change this property manually, use :data:`current` instead. :data:`current_screen` is an :class:`~kivy.properties.ObjectProperty`, default to None, read-only. ''' def _get_screen_names(self): return [s.name for s in self.screens] screen_names = AliasProperty(_get_screen_names, None, bind=('screens', )) '''List of the names of all the :class:`Screen` widgets added. The list is read only. :data:`screens_names` is a :class:`~kivy.properties.AliasProperty`, it is read-only and updated if the screen list changes, or the name of a screen changes. ''' def __init__(self, **kwargs): super(ScreenManager, self).__init__(**kwargs) self.bind(pos=self._update_pos) def _screen_name_changed(self, screen, name): self.property('screen_names').dispatch(self) if screen == self.current_screen: self.current = name def add_widget(self, screen): if not isinstance(screen, Screen): raise ScreenManagerException( 'ScreenManager accept only Screen widget.') if screen.manager: raise ScreenManagerException( 'Screen already managed by another ScreenManager.') screen.manager = self screen.bind(name=self._screen_name_changed) self.screens.append(screen) if self.current is None: self.current = screen.name def remove_widget(self, *l): screen = l[0] if not isinstance(screen, Screen): raise ScreenManagerException( 'ScreenManager uses remove_widget only to remove' + 'screens added via add_widget! use real_remove_widget.') if not screen in self.screens: return if self.current_screen == screen: other = self.next() if other: self.current = other screen.manager = None screen.unbind(name=self._screen_name_changed) self.screens.remove(screen) def real_add_widget(self, *l): # ensure screen is removed from it's previous parent before adding' if l[0].parent: l[0].parent.remove_widget(l[0]) super(ScreenManager, self).add_widget(*l) def real_remove_widget(self, *l): super(ScreenManager, self).remove_widget(*l) def on_current(self, instance, value): screen = self.get_screen(value) if not screen: return if screen == self.current_screen: return previous_screen = self.current_screen self.current_screen = screen if previous_screen: self.transition.stop() self.transition.screen_in = screen self.transition.screen_out = previous_screen self.transition.start(self) else: screen.pos = self.pos self.real_add_widget(screen) def get_screen(self, name): '''Return the screen widget associated to the name, or None if not found. ''' matches = [s for s in self.screens if s.name == name] num_matches = len(matches) if num_matches == 0: raise ScreenManagerException('No Screen with name "%s".' % name) if num_matches > 1: Logger.warn('Multiple screens named "%s": %s' % (name, matches)) return matches[0] def next(self): '''Return the name of the next screen from the screen list. ''' screens = self.screens if not screens: return try: index = screens.index(self.current_screen) index = (index + 1) % len(screens) return screens[index].name except ValueError: return def previous(self): '''Return the name of the previous screen from the screen list. ''' screens = self.screens if not screens: return try: index = screens.index(self.current_screen) index = (index - 1) % len(screens) return screens[index].name except ValueError: return def _update_pos(self, instance, value): for child in self.children: if self.transition.is_active and \ (child == self.transition.screen_in or child == self.transition.screen_out): continue child.pos = value def on_touch_down(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_down(touch) def on_touch_move(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_move(touch) def on_touch_up(self, touch): if self.transition.is_active: return False return super(ScreenManager, self).on_touch_up(touch)
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
class Pawn(PawnSpot): """A token to represent a :class:`Thing`. :class:`Thing` is the LiSE class to represent items that are located in some :class:`Place` or other. Accordingly, :class:`Pawn`'s coordinates are never set directly; they are instead derived from the location of the :class:`Thing` represented. That means a :class:`Pawn` will appear next to the :class:`Spot` representing the :class:`Place` that its :class:`Thing` is in. The exception is if the :class:`Thing` is currently moving from its current :class:`Place` to another one, in which case the :class:`Pawn` will appear some distance along the :class:`Arrow` that represents the :class:`Portal` it's moving through. """ _touch_ox_diff = NumericProperty() _touch_oy_diff = NumericProperty() _touch_opos_diff = ReferenceListProperty(_touch_ox_diff, _touch_oy_diff) _touch = ObjectProperty(None, allownone=True) travel_on_drop = BooleanProperty(False) loc_name = ObjectProperty() next_loc_name = ObjectProperty(None, allownone=True) thing = AliasProperty( lambda self: self.remote, lambda self, v: self.remote.setter()(v), bind=('remote',) ) default_image_paths = ['atlas://rltiles/base.atlas/unseen'] def __init__(self, **kwargs): if 'thing' in kwargs: kwargs['remote'] = kwargs['thing'] del kwargs['thing'] super().__init__(**kwargs) def on_parent(self, *args): if self.parent: self._board = self.parent.board self.bind( loc_name=self._trigger_relocate, next_loc_name=self._trigger_relocate ) if self.remote: self._trigger_relocate() else: if not hasattr(self, '_board'): return for canvas in ( self._board.pawnlayout.canvas.before, self._board.pawnlayout.canvas.after, self._board.pawnlayout.canvas ): if self.group in canvas.children: canvas.remove(self.group) del self._board def relocate(self, *args): if not self.remote.exists: return try: location = self.board.arrow[self.loc_name][self.next_loc_name] except KeyError: try: location = self.board.spot[self.loc_name] except KeyError: return if location != self.parent: self.parent.remove_widget(self) location.add_widget(self) _trigger_relocate = trigger(relocate) def on_remote(self, *args): super().on_remote(*args) if not self.remote or not self.remote.exists: return self.loc_name = self.remote['location'] self.next_loc_name = self.remote.get('next_location', None) def finalize(self): super().finalize() self.bind( loc_name=self._trigger_push_location ) def unfinalize(self): super().unfinalize() self.unbind( loc_name=self._trigger_push_location ) def pull_from_remote(self, *args): super().pull_from_remote(*args) relocate = False if self.loc_name != self.remote['location']: self.loc_name = self.remote['location'] # aliasing? could be trouble relocate = True if self.next_loc_name != self.remote['next_location']: self.next_loc_name = self.remote['next_location'] relocate = True if relocate: self.relocate() def push_location(self, *args): self.remote['location'] = self.loc_name _trigger_push_location = trigger(push_location) def add_widget(self, pawn, index=0, canvas='after'): """Apart from the normal behavior, bind my ``center`` so that the child's lower left corner will always be there, so long as it's my child. """ super().add_widget(pawn, index, canvas) pawn.pos = self.center self.bind(center=pawn.setter('pos')) def remove_widget(self, pawn): """Unbind my ``center`` from the child before removing it.""" if pawn not in self.children: raise ValueError("Not my child") self.unbind(center=pawn.setter('pos')) super().remove_widget(pawn) def on_touch_move(self, touch): """Move with the touch if I'm grabbed.""" if touch.grab_current != self: return False if hasattr(self.parent, 'place') and \ not hasattr(self, '_pospawn_unbound'): self.parent.unbind_trigger_pospawn(self) self._pospawn_bound = True self.center = touch.pos return True def on_touch_up(self, touch): """See if I've been dropped on a :class:`Spot`. If so, command the underlying :class:`Thing` to either travel there or teleport there. Otherwise, snap back to my present location. """ if touch.grab_current != self: return False if hasattr(self.parent, 'place') and hasattr(self, '_pospawn_unbound'): self.parent.bind_trigger_pospawn(self) del self._pospawn_unbound for spot in self.board.spot.values(): if self.collide_widget(spot) and spot.name != self.loc_name: new_spot = spot break else: parent = self.parent parent.remove_widget(self) parent.add_widget(self) return True myplace = self.loc_name theirplace = new_spot.name if myplace != theirplace: if hasattr(self, '_start'): del self._start if self.travel_on_drop: self.thing.travel_to(new_spot.name) else: self.loc_name = new_spot.name self.parent.remove_widget(self) new_spot.add_widget(self) return True def __repr__(self): """Give my ``thing``'s name and its location's name.""" return '{}-in-{}'.format( self.name, self.loc_name )
class MapView(Widget): """MapView is the widget that control the map displaying, navigation, and layers management. """ lon = NumericProperty() """Longitude at the center of the widget """ lat = NumericProperty() """Latitude at the center of the widget """ zoom = NumericProperty(0) """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and :meth:`MapSource.get_max_zoom`. Default to 0. """ map_source = ObjectProperty(MapSource()) """Provider of the map, default to a empty :class:`MapSource`. """ double_tap_zoom = BooleanProperty(False) """If True, this will activate the double-tap to zoom. """ pause_on_action = BooleanProperty(True) """Pause any map loading / tiles loading when an action is done. This allow better performance on mobile, but can be safely deactivated on desktop. """ snap_to_zoom = BooleanProperty(True) """When the user initiate a zoom, it will snap to the closest zoom for better graphics. The map can be blur if the map is scaled between 2 zoom. Default to True, even if it doesn't fully working yet. """ animation_duration = NumericProperty(100) """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. Default to 100 as 100ms. Use 0 to deactivate. """ delta_x = NumericProperty(0) delta_y = NumericProperty(0) background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1]) cache_dir = StringProperty(CACHE_DIR) _zoom = NumericProperty(0) _pause = BooleanProperty(False) _scale = 1. _disabled_count = 0 __events__ = ["on_map_relocated"] # Public API @property def viewport_pos(self): vx, vy = self._scatter.to_local(self.x, self.y) return vx - self.delta_x, vy - self.delta_y @property def scale(self): if self._invalid_scale: self._invalid_scale = False self._scale = self._scatter.scale return self._scale def get_bbox(self, margin=0): """Returns the bounding box from the bottom/left (lat1, lon1) to top/right (lat2, lon2). """ x1, y1 = self.to_local(0 - margin, 0 - margin) x2, y2 = self.to_local((self.width + margin), (self.height + margin)) c1 = self.get_latlon_at(x1, y1) c2 = self.get_latlon_at(x2, y2) return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) def unload(self): """Unload the view and all the layers. It also cancel all the remaining downloads. """ self.remove_all_tiles() def get_window_xy_from(self, lat, lon, zoom): """Returns the x/y position in the widget absolute coordinates from a lat/lon""" scale = self.scale vx, vy = self.viewport_pos ms = self.map_source x = ms.get_x(zoom, lon) - vx y = ms.get_y(zoom, lat) - vy x *= scale y *= scale x = x + self.pos[0] y = y + self.pos[1] return x, y def center_on(self, *args): """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) """ map_source = self.map_source zoom = self._zoom if len(args) == 1 and isinstance(args[0], Coordinate): coord = args[0] lat = coord.lat lon = coord.lon elif len(args) == 2: lat, lon = args else: raise Exception("Invalid argument for center_on") lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) scale = self._scatter.scale x = map_source.get_x(zoom, lon) - self.center_x / scale y = map_source.get_y(zoom, lat) - self.center_y / scale self.delta_x = -x self.delta_y = -y self.lon = lon self.lat = lat self._scatter.pos = 0, 0 self.trigger_update(True) def set_zoom_at(self, zoom, x, y, scale=None): """Sets the zoom level, leaving the (x, y) at the exact same point in the view. """ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()) if int(zoom) == int(self._zoom): if scale is None: return elif scale == self.scale: return scale = scale or 1. # first, rescale the scatter scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) # adjust position if the zoom changed c1 = self.map_source.get_col_count(self._zoom) c2 = self.map_source.get_col_count(zoom) if c1 != c2: f = float(c2) / float(c1) self.delta_x = scatter.x + self.delta_x * f self.delta_y = scatter.y + self.delta_y * f # back to 0 every time scatter.apply_transform(Matrix().translate( -scatter.x, -scatter.y, 0 ), post_multiply=True) # avoid triggering zoom changes. self._zoom = zoom self.zoom = self._zoom def on_zoom(self, instance, zoom): if zoom == self._zoom: return x = self.map_source.get_x(zoom, self.lon) - self.delta_x y = self.map_source.get_y(zoom, self.lat) - self.delta_y self.set_zoom_at(zoom, x, y) self.center_on(self.lat, self.lon) def get_latlon_at(self, x, y, zoom=None): """Return the current :class:`Coordinate` within the (x, y) widget coordinate. """ if zoom is None: zoom = self._zoom vx, vy = self.viewport_pos scale = self._scale return Coordinate( lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx)) def add_marker(self, marker, layer=None): """Add a marker into the layer. If layer is None, it will be added in the default marker layer. If there is no default marker layer, a new one will be automatically created """ if layer is None: if not self._default_marker_layer: layer = MarkerMapLayer() self.add_layer(layer) else: layer = self._default_marker_layer layer.add_widget(marker) layer.set_marker_position(self, marker) def remove_marker(self, marker): """Remove a marker from its layer """ marker.detach() def add_layer(self, layer, mode="window"): """Add a new layer to update at the same time the base tile layer. mode can be either "scatter" or "window". If "scatter", it means the layer will be within the scatter transformation. It's perfect if you want to display path / shape, but not for text. If "window", it will have no transformation. You need to position the widget yourself: think as Z-sprite / billboard. Defaults to "window". """ assert (mode in ("scatter", "window")) if self._default_marker_layer is None and \ isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer self._layers.append(layer) c = self.canvas if mode == "scatter": self.canvas = self.canvas_layers else: self.canvas = self.canvas_layers_out layer.canvas_parent = self.canvas super(MapView, self).add_widget(layer) self.canvas = c def remove_layer(self, layer): """Remove the layer """ c = self.canvas self._layers.remove(layer) self.canvas = layer.canvas_parent super(MapView, self).remove_widget(layer) self.canvas = c def sync_to(self, other): """Reflect the lat/lon/zoom of the other MapView to the current one. """ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center) self.center_on(other.get_latlon_at(*self.center)) # Private API def __init__(self, **kwargs): from kivy.base import EventLoop EventLoop.ensure_window() self._invalid_scale = True self._tiles = [] self._tiles_bg = [] self._tilemap = {} self._layers = [] self._default_marker_layer = None self._need_redraw_all = False self._transform_lock = False self.trigger_update(True) self.canvas = Canvas() self._scatter = MapViewScatter() self.add_widget(self._scatter) with self._scatter.canvas: self.canvas_map = Canvas() self.canvas_layers = Canvas() with self.canvas: self.canvas_layers_out = Canvas() self._scale_target_anim = False self._scale_target = 1. self._touch_count = 0 self.map_source.cache_dir = self.cache_dir Clock.schedule_interval(self._animate_color, 1 / 60.) self.lat = kwargs.get("lat", self.lat) self.lon = kwargs.get("lon", self.lon) super(MapView, self).__init__(**kwargs) def _animate_color(self, dt): # fast path d = self.animation_duration if d == 0: for tile in self._tiles: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" for tile in self._tiles_bg: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" else: d = d / 1000. for tile in self._tiles: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" for tile in self._tiles_bg: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" def add_widget(self, widget): if isinstance(widget, MapMarker): self.add_marker(widget) elif isinstance(widget, MapLayer): self.add_layer(widget) else: super(MapView, self).add_widget(widget) def remove_widget(self, widget): if isinstance(widget, MapMarker): self.remove_marker(widget) elif isinstance(widget, MapLayer): self.remove_layer(widget) else: super(MapView, self).remove_widget(widget) def on_map_relocated(self, zoom, coord): pass def animated_diff_scale_at(self, d, x, y): self._scale_target_time = 1. self._scale_target_pos = x, y if self._scale_target_anim == False: self._scale_target_anim = True self._scale_target = d else: self._scale_target += d Clock.unschedule(self._animate_scale) Clock.schedule_interval(self._animate_scale, 1 / 60.) def _animate_scale(self, dt): diff = self._scale_target / 3. if abs(diff) < 0.01: diff = self._scale_target self._scale_target = 0 else: self._scale_target -= diff self._scale_target_time -= dt self.diff_scale_at(diff, *self._scale_target_pos) ret = self._scale_target != 0 if not ret: self._pause = False return ret def diff_scale_at(self, d, x, y): scatter = self._scatter scale = scatter.scale * (2 ** d) self.scale_at(scale, x, y) def scale_at(self, scale, x, y): scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.pause_on_action: self._pause = True if "button" in touch.profile and touch.button in ( "scrolldown", "scrollup"): d = 1 if touch.button == "scrollup" else -1 self.animated_diff_scale_at(d, *touch.pos) return True elif touch.is_double_tap and self.double_tap_zoom: self.animated_diff_scale_at(1, *touch.pos) return True touch.grab(self) self._touch_count += 1 if self._touch_count == 1: self._touch_zoom = (self.zoom, self._scale) return super(MapView, self).on_touch_down(touch) def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) self._touch_count -= 1 if self._touch_count == 0: # animate to the closest zoom zoom, scale = self._touch_zoom cur_zoom = self.zoom cur_scale = self._scale if cur_zoom < zoom or cur_scale < scale: self.animated_diff_scale_at(1. - cur_scale, *touch.pos) elif cur_zoom > zoom or cur_scale > scale: self.animated_diff_scale_at(2. - cur_scale, *touch.pos) self._pause = False return True return super(MapView, self).on_touch_up(touch) def on_transform(self, *args): self._invalid_scale = True if self._transform_lock: return self._transform_lock = True # recalculate viewport map_source = self.map_source zoom = self._zoom scatter = self._scatter scale = scatter.scale if scale >= 2.: zoom += 1 scale /= 2. elif scale < 1: zoom -= 1 scale *= 2. zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) if zoom != self._zoom: self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) self.trigger_update(True) else: if zoom == map_source.min_zoom and scatter.scale < 1.: scatter.scale = 1. self.trigger_update(True) else: self.trigger_update(False) if map_source.bounds: self._apply_bounds() self._transform_lock = False self._scale = self._scatter.scale def _apply_bounds(self): # if the map_source have any constraints, apply them here. map_source = self.map_source zoom = self._zoom min_lon, min_lat, max_lon, max_lat = map_source.bounds xmin = map_source.get_x(zoom, min_lon) xmax = map_source.get_x(zoom, max_lon) ymin = map_source.get_y(zoom, min_lat) ymax = map_source.get_y(zoom, max_lat) dx = self.delta_x dy = self.delta_y oxmin, oymin = self._scatter.to_local(self.x, self.y) oxmax, oymax = self._scatter.to_local(self.right, self.top) s = self._scale cxmin = (oxmin - dx) if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s cymin = (oymin - dy) if cymin < ymin: self._scatter.y += (cymin - ymin) * s cxmax = (oxmax - dx) if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s cymax = (oymax - dy) if cymax > ymax: self._scatter.y -= (ymax - cymax) * s def on__pause(self, instance, value): if not value: self.trigger_update(True) def trigger_update(self, full): self._need_redraw_full = full or self._need_redraw_full Clock.unschedule(self.do_update) Clock.schedule_once(self.do_update, -1) def do_update(self, dt): zoom = self._zoom scale = self._scale self.lon = self.map_source.get_lon(zoom, ( self.center_x - self._scatter.x) / scale - self.delta_x) self.lat = self.map_source.get_lat(zoom, ( self.center_y - self._scatter.y) / scale - self.delta_y) self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) for layer in self._layers: layer.reposition() if self._need_redraw_full: self._need_redraw_full = False self.move_tiles_to_background() self.load_visible_tiles() else: self.load_visible_tiles() def bbox_for_zoom(self, vx, vy, w, h, zoom): # return a tile-bbox for the zoom map_source = self.map_source size = map_source.dp_tile_size scale = self._scale max_x_end = map_source.get_col_count(zoom) max_y_end = map_source.get_row_count(zoom) x_count = int(ceil(w / scale / float(size))) + 1 y_count = int(ceil(h / scale / float(size))) + 1 tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) tile_x_last = tile_x_first + x_count tile_y_last = tile_y_first + y_count tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) x_count = tile_x_last - tile_x_first y_count = tile_y_last - tile_y_first return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) def load_visible_tiles(self): map_source = self.map_source vx, vy = self.viewport_pos zoom = self._zoom dirs = [0, 1, 0, -1, 0] bbox_for_zoom = self.bbox_for_zoom size = map_source.dp_tile_size tile_x_first, tile_y_first, tile_x_last, tile_y_last, \ x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom) # print "Range {},{} to {},{}".format( # tile_x_first, tile_y_first, # tile_x_last, tile_y_last) # Adjust tiles behind us for tile in self._tiles_bg[:]: tile_x = tile.tile_x tile_y = tile.tile_y f = 2 ** (zoom - tile.zoom) w = self.width / f h = self.height / f btile_x_first, btile_y_first, btile_x_last, btile_y_last, \ _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) if tile_x < btile_x_first or tile_x >= btile_x_last or \ tile_y < btile_y_first or tile_y >= btile_y_last: tile.state = "done" self._tiles_bg.remove(tile) self.canvas_map.before.remove(tile.g_color) self.canvas_map.before.remove(tile) continue tsize = size * f tile.size = tsize, tsize tile.pos = ( tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) # Get rid of old tiles first for tile in self._tiles[:]: tile_x = tile.tile_x tile_y = tile.tile_y if tile_x < tile_x_first or tile_x >= tile_x_last or \ tile_y < tile_y_first or tile_y >= tile_y_last: tile.state = "done" self.tile_map_set(tile_x, tile_y, False) self._tiles.remove(tile) self.canvas_map.remove(tile) self.canvas_map.remove(tile.g_color) else: tile.size = (size, size) tile.pos = ( tile_x * size + self.delta_x, tile_y * size + self.delta_y) # Load new tiles if needed x = tile_x_first + x_count // 2 - 1 y = tile_y_first + y_count // 2 - 1 arm_max = max(x_count, y_count) + 2 arm_size = 1 turn = 0 while arm_size < arm_max: for i in range(arm_size): if not self.tile_in_tile_map(x, y) and \ y >= tile_y_first and y < tile_y_last and \ x >= tile_x_first and x < tile_x_last: self.load_tile(x, y, size, zoom) x += dirs[turn % 4 + 1] y += dirs[turn % 4] if turn % 2 == 1: arm_size += 1 turn += 1 def load_tile(self, x, y, size, zoom): if self.tile_in_tile_map(x, y) or zoom != self._zoom: return self.load_tile_for_source(self.map_source, 1., size, x, y, zoom) # XXX do overlay support self.tile_map_set(x, y, True) def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): tile = Tile(size=(size, size), cache_dir=self.cache_dir) tile.g_color = Color(1, 1, 1, 0) tile.tile_x = x tile.tile_y = y tile.zoom = zoom tile.pos = (x * size + self.delta_x, y * size + self.delta_y) tile.map_source = map_source tile.state = "loading" if not self._pause: map_source.fill_tile(tile) self.canvas_map.add(tile.g_color) self.canvas_map.add(tile) self._tiles.append(tile) def move_tiles_to_background(self): # remove all the tiles of the main map to the background map # retain only the one who are on the current zoom level # for all the tile in the background, stop the download if not yet started. zoom = self._zoom tiles = self._tiles btiles = self._tiles_bg canvas_map = self.canvas_map tile_size = self.map_source.tile_size # move all tiles to background while tiles: tile = tiles.pop() if tile.state == "loading": tile.state = "done" continue btiles.append(tile) # clear the canvas canvas_map.clear() canvas_map.before.clear() self._tilemap = {} # unsure if it's really needed, i personnally didn't get issues right now # btiles.sort(key=lambda z: -z.zoom) # add all the btiles into the back canvas. # except for the tiles that are owned by the current zoom level for tile in btiles[:]: if tile.zoom == zoom: btiles.remove(tile) tiles.append(tile) tile.size = tile_size, tile_size canvas_map.add(tile.g_color) canvas_map.add(tile) self.tile_map_set(tile.tile_x, tile.tile_y, True) continue canvas_map.before.add(tile.g_color) canvas_map.before.add(tile) def remove_all_tiles(self): # clear the map of all tiles. self.canvas_map.clear() self.canvas_map.before.clear() for tile in self._tiles: tile.state = "done" del self._tiles[:] del self._tiles_bg[:] self._tilemap = {} def tile_map_set(self, tile_x, tile_y, value): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x if value: self._tilemap[key] = value else: self._tilemap.pop(key, None) def tile_in_tile_map(self, tile_x, tile_y): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x return key in self._tilemap def on_size(self, instance, size): for layer in self._layers: layer.size = size self.center_on(self.lat, self.lon) self.trigger_update(True) def on_pos(self, instance, pos): self.center_on(self.lat, self.lon) self.trigger_update(True) def on_map_source(self, instance, source): if isinstance(source, string_types): self.map_source = MapSource.from_provider(source) elif isinstance(source, (tuple, list)): cache_key, min_zoom, max_zoom, url, attribution, options = source self.map_source = MapSource(url=url, cache_key=cache_key, min_zoom=min_zoom, max_zoom=max_zoom, attribution=attribution, cache_dir=self.cache_dir, **options) elif isinstance(source, MapSource): self.map_source = source else: raise Exception("Invalid map source provider") self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) self.remove_all_tiles() self.trigger_update(True)
class FileChooser(FileChooserController): '''Implementation of a :class:`FileChooserController` which supports switching between multiple, synced layout views. The FileChooser can be used as follows: .. code-block:: kv BoxLayout: orientation: 'vertical' BoxLayout: size_hint_y: None height: sp(52) Button: text: 'Icon View' on_press: fc.view_mode = 'icon' Button: text: 'List View' on_press: fc.view_mode = 'list' FileChooser: id: fc FileChooserIconLayout FileChooserListLayout .. versionadded:: 1.9.0 ''' manager = ObjectProperty() ''' Reference to the :class:`~kivy.uix.screenmanager.ScreenManager` instance. manager is an :class:`~kivy.properties.ObjectProperty`. ''' _view_list = ListProperty() def get_view_list(self): return self._view_list view_list = AliasProperty(get_view_list, bind=('_view_list', )) ''' List of views added to this FileChooser. view_list is an :class:`~kivy.properties.AliasProperty` of type :class:`list`. ''' _view_mode = StringProperty() def get_view_mode(self): return self._view_mode def set_view_mode(self, mode): if mode not in self._view_list: raise ValueError('unknown view mode %r' % mode) self._view_mode = mode view_mode = AliasProperty(get_view_mode, set_view_mode, bind=('_view_mode', )) ''' Current layout view mode. view_mode is an :class:`~kivy.properties.AliasProperty` of type :class:`str`. ''' @property def _views(self): return [screen.children[0] for screen in self.manager.screens] def __init__(self, **kwargs): super(FileChooser, self).__init__(**kwargs) self.manager = ScreenManager() super(FileChooser, self).add_widget(self.manager) self.trigger_update_view = Clock.create_trigger(self.update_view) self.fbind('view_mode', self.trigger_update_view) def add_widget(self, widget, **kwargs): if widget is self._progress: super(FileChooser, self).add_widget(widget, **kwargs) elif hasattr(widget, 'VIEWNAME'): name = widget.VIEWNAME + 'view' screen = Screen(name=name) widget.controller = self screen.add_widget(widget) self.manager.add_widget(screen) self.trigger_update_view() else: raise ValueError('widget must be a FileChooserLayout,' ' not %s' % type(widget).__name__) def rebuild_views(self): views = [view.VIEWNAME for view in self._views] if views != self._view_list: self._view_list = views if self._view_mode not in self._view_list: self._view_mode = self._view_list[0] self._trigger_update() def update_view(self, *args): self.rebuild_views() sm = self.manager viewlist = self._view_list view = self.view_mode current = sm.current[:-4] viewindex = viewlist.index(view) if view in viewlist else 0 currentindex = viewlist.index(current) if current in viewlist else 0 direction = 'left' if currentindex < viewindex else 'right' sm.transition.direction = direction sm.current = view + 'view' def _create_entry_widget(self, ctx): return [ Builder.template(view._ENTRY_TEMPLATE, **ctx) for view in self._views ] def _get_file_paths(self, items): if self._views: return [file[0].path for file in items] return [] def _update_item_selection(self, *args): for viewitem in self._items: selected = viewitem[0].path in self.selection for item in viewitem: item.selected = selected def on_entry_added(self, node, parent=None): for index, view in enumerate(self._views): view.dispatch('on_entry_added', node[index], parent[index] if parent else None) def on_entries_cleared(self): for view in self._views: view.dispatch('on_entries_cleared') def on_subentry_to_entry(self, subentry, entry): for index, view in enumerate(self._views): view.dispatch('on_subentry_to_entry', subentry[index], entry) def on_remove_subentry(self, subentry, entry): for index, view in enumerate(self._views): view.dispatch('on_remove_subentry', subentry[index], entry) def on_submit(self, selected, touch=None): view_mode = self.view_mode for view in self._views: if view_mode == view.VIEWNAME: view.dispatch('on_submit', selected, touch) return
class MDTabsBar(ThemableBehavior, RectangularElevationBehavior, MDBoxLayout): """ This class is just a boxlayout that contains the scroll view for tabs. He is also responsible for resizing the tab shortcut when necessary. """ target = ObjectProperty(None, allownone=True) """ Is the carousel reference of the next tab / slide. When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the target tab / slide of the carousel. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` and default to `None`. """ def get_rect_instruction(self): for i in self.layout.canvas.after.children: if isinstance(i, Rectangle): return i indicator = AliasProperty(get_rect_instruction, cache=True) """ Is the Rectangle instruction reference of the tab indicator. :attr:`indicator` is an :class:`~kivy.properties.AliasProperty`. """ def get_last_scroll_x(self): return self.scrollview.scroll_x last_scroll_x = AliasProperty(get_last_scroll_x, bind=("target", ), cache=True) """ Is the carousel reference of the next tab/slide. When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the target tab/slide of the carousel. :attr:`last_scroll_x` is an :class:`~kivy.properties.AliasProperty`. """ def __init__(self, **kwargs): self._trigger_update_tab_bar = Clock.schedule_once( self._update_tab_bar, 0) super().__init__(**kwargs) def _update_tab_bar(self, *args): if self.parent.allow_stretch: # update width of the labels when it is needed width, tabs = self.scrollview.width, self.layout.children tabs_widths = [t.min_space for t in tabs if t.min_space] tabs_space = float(sum(tabs_widths)) if not tabs_space: return ratio = width / tabs_space use_ratio = True in (width / len(tabs) < w for w in tabs_widths) for t in tabs: t.width = (t.min_space if tabs_space > width else t.min_space * ratio if use_ratio is True else width / len(tabs)) def update_indicator(self, x, w): # update position and size of the indicator self.indicator.pos = (x, 0) self.indicator.size = (w, self.indicator.size[1]) def tab_bar_autoscroll(self, target, step): # automatic scroll animation of the tab bar. bound_left = self.center_x bound_right = self.layout.width - bound_left dt = target.center_x - bound_left sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0) # last scroll x of the tab bar lsx = self.last_scroll_x # determine scroll direction scroll_is_late = lsx < sx # distance to run dst = abs(lsx - sx) * step if not dst: return if scroll_is_late and target.center_x > bound_left: x = lsx + dst elif not scroll_is_late and target.center_x < bound_right: x = lsx - dst else: x = lsx + dst x = boundary(x, 0.0, 1.0) self.scrollview.goto(x, None) def android_animation(self, carousel, offset): # try to reproduce the android animation effect. if offset != 0 and abs(offset) < carousel.width: forward = offset < 0 offset = abs(offset) step = offset / float(carousel.width) distance = abs(offset - carousel.width) threshold = self.parent.anim_threshold breakpoint = carousel.width - (carousel.width * threshold) traveled = distance / breakpoint if breakpoint else 0 break_step = 1.0 - traveled indicator_animation = self.parent.tab_indicator_anim try: skip_slide = (carousel.slides[carousel._skip_slide] if carousel._skip_slide is not None else None) except IndexError: skip_slide = (None) next_slide = (carousel.next_slide if forward else carousel.previous_slide) self.target = skip_slide if skip_slide else next_slide if not self.target: return a = carousel.current_slide.tab_label b = self.target.tab_label self.tab_bar_autoscroll(b, step) if not indicator_animation: return if step <= threshold: if forward: gap_w = abs((a.x + a.width) - (b.x + b.width)) w_step = a.width + (gap_w * step) x_step = a.x else: gap = abs((a.x - b.x)) x_step = a.x - gap * step w_step = a.width + gap * step else: if forward: x_step = a.x + abs((a.x - b.x)) * break_step gap_w = abs((a.x + a.width) - (b.x + b.width)) ind_width = a.width + gap_w * threshold gap_w = ind_width - b.width w_step = ind_width - (gap_w * break_step) else: x_step = a.x - abs((a.x - b.x)) * threshold x_step = x_step - abs(x_step - b.x) * break_step ind_width = ((a.x + a.width) - x_step if threshold else a.width) gap_w = ind_width - b.width w_step = ind_width - (gap_w * break_step) w_step = (w_step if w_step + x_step <= a.x + a.width else ind_width) self.update_indicator(x_step, w_step)
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 MDSlider(ThemableBehavior, Slider): active = BooleanProperty(False) """ If the slider is clicked. :attr:`active` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ hint = BooleanProperty(True) """ If True, then the current value is displayed above the slider. :attr:`hint` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ hint_bg_color = ListProperty() """ Hint rectangle color in ``rgba`` format. :attr:`hint_bg_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ hint_text_color = ListProperty() """ Hint text color in ``rgba`` format. :attr:`hint_text_color` is an :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ hint_radius = NumericProperty(4) """ Hint radius. :attr:`hint_text_color` is an :class:`~kivy.properties.NumericProperty` and defaults to `4`. """ show_off = BooleanProperty(True) """ Show the `'off'` ring when set to minimum value. :attr:`show_off` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ # Internal state of ring _is_off = BooleanProperty(False) # Internal adjustment to reposition sliders for ring _offset = ListProperty((0, 0)) _thumb_color = ListProperty(get_color_from_hex(colors["Gray"]["50"])) def _get_thumb_color(self): return self._thumb_color def _set_thumb_color(self, color, alpha=None): if len(color) == 2: self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) if alpha: self._thumb_color[3] = alpha elif len(color) == 4: self._thumb_color = color thumb_color = AliasProperty( _get_thumb_color, _set_thumb_color, bind=["_thumb_color"] ) """ Current color slider in ``rgba`` format. :attr:`thumb_color` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current color slider, property is readonly. """ _thumb_color_down = ListProperty([1, 1, 1, 1]) def _get_thumb_color_down(self): return self._thumb_color_down def _set_thumb_color_down(self, color, alpha=None): if len(color) == 2: self._thumb_color_down = get_color_from_hex( colors[color[0]][color[1]] ) if alpha: self._thumb_color_down[3] = alpha else: self._thumb_color_down[3] = 1 elif len(color) == 4: self._thumb_color_down = color _thumb_color_disabled = ListProperty( get_color_from_hex(colors["Gray"]["400"]) ) def _get_thumb_color_disabled(self): return self._thumb_color_disabled def _set_thumb_color_disabled(self, color, alpha=None): if len(color) == 2: self._thumb_color_disabled = get_color_from_hex( colors[color[0]][color[1]] ) if alpha: self._thumb_color_disabled[3] = alpha elif len(color) == 4: self._thumb_color_disabled = color thumb_color_down = AliasProperty( _get_thumb_color_disabled, _set_thumb_color_disabled, bind=["_thumb_color_disabled"], ) """ Color slider in ``rgba`` format. :attr:`thumb_color_down` is an :class:`~kivy.properties.AliasProperty` that returns and set the value of color slider. """ _track_color_active = ListProperty() _track_color_normal = ListProperty() _track_color_disabled = ListProperty() _thumb_pos = ListProperty([0, 0]) def __init__(self, **kwargs): super().__init__(**kwargs) self.theme_cls.bind( theme_style=self._set_colors, primary_color=self._set_colors, primary_palette=self._set_colors, ) self._set_colors() def on_hint(self, instance, value): if not value: self.remove_widget(self.ids.hint_box) def _set_colors(self, *args): if self.theme_cls.theme_style == "Dark": self._track_color_normal = get_color_from_hex("FFFFFF") self._track_color_normal[3] = 0.3 self._track_color_active = self._track_color_normal self._track_color_disabled = self._track_color_normal self.thumb_color = get_color_from_hex(colors["Gray"]["400"]) self.thumb_color_down = get_color_from_hex( colors[self.theme_cls.primary_palette]["200"] ) self.thumb_color_disabled = get_color_from_hex( colors["Gray"]["800"] ) else: self._track_color_normal = get_color_from_hex("000000") self._track_color_normal[3] = 0.26 self._track_color_active = get_color_from_hex("000000") self._track_color_active[3] = 0.38 self._track_color_disabled = get_color_from_hex("000000") self._track_color_disabled[3] = 0.26 self.thumb_color_down = self.theme_cls.primary_color def on_value_normalized(self, *args): """When the ``value == min`` set it to `'off'` state and make slider a ring. """ self._update_is_off() def on_show_off(self, *args): self._update_is_off() def _update_is_off(self): self._is_off = self.show_off and (self.value_normalized == 0) def on__is_off(self, *args): self._update_offset() def on_active(self, *args): self._update_offset() def _update_offset(self): """Offset is used to shift the sliders so the background color shows through the off circle. """ d = 2 if self.active else 0 self._offset = (dp(11 + d), dp(11 + d)) if self._is_off else (0, 0) def on_touch_down(self, touch): if super().on_touch_down(touch): self.active = True def on_touch_up(self, touch): if super().on_touch_up(touch): self.active = False
class Widget(EventDispatcher): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. ''' def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas() # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :meth:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :meth:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if widget is self: raise WidgetException('You cannot add yourself in a Widget') if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to this widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied to the current global opacity, and the result is applied to the current context color. For example: if your parent have an opacity of 0.5, and one children have an opacity of 0.2, the real opacity of the children will be 0.5 * 0.2 = 0.1. Then, the opacity is applied on the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty`, default to 1.0. ''' def on_opacity(self, instance, value): self.canvas.opacity = value canvas = None '''Canvas of the widget.
class ActionItem(object): ''' ActionItem class, an abstract class for all ActionBar widgets. To create a custom widget for an ActionBar, inherit from this class. See module documentation for more information. ''' minimum_width = NumericProperty('90sp') ''' Minimum Width required by an ActionItem. :attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and defaults to '90sp'. ''' def get_pack_width(self): return max(self.minimum_width, self.width) pack_width = AliasProperty(get_pack_width, bind=('minimum_width', 'width'), cache=True) ''' (read-only) The actual width to use when packing the items. Equal to the greater of minimum_width and width. :attr:`pack_width` is an :class:`~kivy.properties.AliasProperty`. ''' important = BooleanProperty(False) ''' Determines if an ActionItem is important or not. If an item is important and space is limited, this item will be displayed in preference to others. :attr:`important` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' inside_group = BooleanProperty(False) ''' (internal) Determines if an ActionItem is displayed inside an ActionGroup or not. :attr:`inside_group` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' background_normal = StringProperty( 'atlas://data/images/defaulttheme/action_item') ''' Background image of the ActionItem used for the default graphical representation when the ActionItem is not pressed. :attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/action_item'. ''' background_down = StringProperty( 'atlas://data/images/defaulttheme/action_item_down') ''' Background image of the ActionItem used for the default graphical representation when an ActionItem is pressed. :attr:`background_down` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/action_item_down'. ''' mipmap = BooleanProperty(True) '''
class TreeView(Widget): '''TreeView class. See module documentation for more information. :Events: `on_node_expand`: (node, ) Fired when a node is being expanded `on_node_collapse`: (node, ) Fired when a node is being collapsed ''' def __init__(self, **kwargs): self.register_event_type('on_node_expand') self.register_event_type('on_node_collapse') self._trigger_layout = Clock.create_trigger(self._do_layout, -1) super(TreeView, self).__init__(**kwargs) tvlabel = TreeViewLabel(text='Root', is_open=True, level=0) for key, value in self.root_options.iteritems(): setattr(tvlabel, key, value) self._root = self.add_node(tvlabel, None) self.bind( pos=self._trigger_layout, size=self._trigger_layout, indent_level=self._trigger_layout, indent_start=self._trigger_layout) self._trigger_layout() def add_node(self, node, parent=None): '''Add a new node in the tree. :Parameters: `node`: instance of a :class:`TreeViewNode` Node to add into the tree `parent`: instance of a :class:`TreeViewNode`, defaults to None Parent node to attach the new node ''' # check if the widget is "ok" for a node if not isinstance(node, TreeViewNode): raise TreeViewException( 'The node must be a subclass of TreeViewNode') # create node if parent is None and self._root: parent = self._root if parent: parent.is_leaf = False parent.nodes.append(node) node.parent_node = parent node.level = parent.level + 1 node.bind(size=self._trigger_layout) self._trigger_layout() return node def remove_node(self, node): '''Remove a node in a tree. .. versionadded:: 1.0.7 :Parameters: `node`: instance of a :class:`TreeViewNode` Node to remove from the tree ''' # check if the widget is "ok" for a node if not isinstance(node, TreeViewNode): raise TreeViewException( 'The node must be a subclass of TreeViewNode') parent = node.parent_node if parent is not None: nodes = parent.nodes if node in nodes: nodes.remove(node) parent.is_leaf = not bool(len(nodes)) node.parent_node = None node.unbind(size=self._trigger_layout) self._trigger_layout() def on_node_expand(self, node): pass def on_node_collapse(self, node): pass def select_node(self, node): '''Select a node in the tree. ''' if node.no_selection: return if self._selected_node: self._selected_node.is_selected = False node.is_selected = True self._selected_node = node def toggle_node(self, node): '''Toggle the state of the node (open/collapse). ''' node.is_open = not node.is_open if node.is_open: if self.load_func and not node.is_loaded: self._do_node_load(node) self.dispatch('on_node_expand', node) else: self.dispatch('on_node_collapse', node) self._trigger_layout() def get_node_at_pos(self, pos): '''Get a node at the position (x, y). ''' x, y = pos for node in self.iterate_open_nodes(self.root): if self.x <= x <= self.right and \ node.y <= y <= node.top: return node def iterate_open_nodes(self, node=None): '''Generator to iterate over expanded nodes. To get all the open nodes:: treeview = TreeView() # ... add nodes ... for node in treeview.iterate_open_nodes(): print node ''' if not node: node = self.root if self.hide_root and node is self.root: pass else: yield node if not node.is_open: return f = self.iterate_open_nodes for cnode in node.nodes: for ynode in f(cnode): yield ynode def iterate_all_nodes(self, node=None): '''Generator to iterate over all nodes, expanded or not. ''' if not node: node = self.root yield node f = self.iterate_all_nodes for cnode in node.nodes: for ynode in f(cnode): yield ynode # # Private # def on_load_func(self, instance, value): if value: Clock.schedule_once(self._do_initial_load) def _do_initial_load(self, *largs): if not self.load_func: return self._do_node_load(None) def _do_node_load(self, node): gen = self.load_func(self, node) if node: node.is_loaded = True if not gen: return for cnode in gen: self.add_node(cnode, node) def on_root_options(self, instance, value): if not self.root: return for key, value in value.iteritems(): setattr(self.root, key, value) def _do_layout(self, *largs): self.clear_widgets() # display only the one who are is_open self._do_open_node(self.root) # now do layout self._do_layout_node(self.root, 0, self.top) # now iterate for calculating minimum size min_width = min_height = 0 count = 0 for node in self.iterate_open_nodes(self.root): node.odd = False if count % 2 else True count += 1 min_width = max(min_width, node.width + self.indent_level + node.level * self.indent_level) min_height += node.height self.minimum_size = (min_width, min_height) def _do_open_node(self, node): if self.hide_root and node is self.root: height = 0 else: self.add_widget(node) height = node.height if not node.is_open: return height for cnode in node.nodes: height += self._do_open_node(cnode) return height def _do_layout_node(self, node, level, y): if self.hide_root and node is self.root: level -= 1 else: node.x = self.x + self.indent_start + level * self.indent_level node.top = y if node.size_hint_x: node.width = (self.width - (node.x - self.x)) * node.size_hint_x y -= node.height if not node.is_open: return y for cnode in node.nodes: y = self._do_layout_node(cnode, level + 1, y) return y def on_touch_down(self, touch): node = self.get_node_at_pos(touch.pos) if not node: return # toggle node or selection ? if node.x - self.indent_start <= touch.x < node.x: self.toggle_node(node) elif node.x <= touch.x: self.select_node(node) node.dispatch('on_touch_down', touch) return True # # Private properties # _root = ObjectProperty(None) _selected_node = ObjectProperty(None) # # Properties # minimum_width = NumericProperty(0) '''Minimum width needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_width` is a :class:`kivy.properties.NumericProperty`, defaults to 0. ''' minimum_height = NumericProperty(0) '''Minimum height needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_height` is a :class:`kivy.properties.NumericProperty`, defaults to 0. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Minimum size needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`minimum_width`, :data:`minimum_height`) properties. ''' indent_level = NumericProperty('16dp') '''Width used for identation of each level, except the first level. Computation of spacing for eaching level of tree is:: :data:`indent_start` + level * :data:`indent_level` :data:`indent_level` is a :class:`~kivy.properties.NumericProperty`, defaults to 16. ''' indent_start = NumericProperty('24dp') '''Indentation width of the level 0 / root node. This is mostly the initial size to accommodate a tree icon (collapsed / expanded). See :data:`indent_level` for more information about the computation of level indentation. :data:`indent_start` is a :class:`~kivy.properties.NumericProperty`, defaults to 24. ''' hide_root = BooleanProperty(False) '''Use this property to show/hide the initial root node. If True, the root node will be appear as a closed node. :data:`hide_root` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' def get_selected_node(self): return self._selected_node selected_node = AliasProperty(get_selected_node, None, bind=('_selected_node', )) '''Node selected by :meth:`TreeView.select_node`, or by touch. :data:`selected_node` is a :class:`~kivy.properties.AliasProperty`, defaults to None, and is read-only. ''' def get_root(self): return self._root root = AliasProperty(get_root, None, bind=('_root', )) '''Root node. By default, the root node widget is a :class:`TreeViewLabel`, with text 'Root'. If you want to change the default options passed to the widget creation, use the :data:`root_options` property:: treeview = TreeView(root_options={ 'text': 'Root directory', 'font_size': 15}) :data:`root_options` will change the properties of the :class:`TreeViewLabel` instance. However, you cannot change the class used for root node yet. :data:`root` is a :class:`~kivy.properties.AliasProperty`, defaults to None, and is read-only. However, the content of the widget can be changed. ''' root_options = ObjectProperty({}) '''Default root options to pass for root widget. See :data:`root` property for more information about the usage of root_options. :data:`root_options` is a :class:`~kivy.properties.ObjectProperty`, default to {}. ''' load_func = ObjectProperty(None) '''Callback to use for asynchronous loading. If set, asynchronous loading
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 Camera(EventDispatcher): """ Base camera class """ scale = NumericProperty(1.0) up = ObjectProperty(Vector3(0, 1, 0)) def __init__(self): super(Camera, self).__init__() self.projection_matrix = Matrix() self.modelview_matrix = Matrix() self.renderer = None # renderer camera is bound to self._position = Vector3(0, 0, 0) self._position.set_change_cb(self.on_pos_changed) self._look_at = None self.look_at(Vector3(0, 0, -1)) def _set_position(self, val): if isinstance(val, Vector3): self._position = val else: self._position = Vector3(val) self._position.set_change_cb(self.on_pos_changed) self.look_at(self._look_at) self.update() def _get_position(self): return self._position position = AliasProperty(_get_position, _set_position) pos = position # just shortcut def on_pos_changed(self, coord, v): """ Camera position was changed """ self.look_at(self._look_at) self.update() def on_up(self, instance, up): """ Camera up vector was changed """ pass def on_scale(self, instance, scale): """ Handler for change scale parameter event """ def look_at(self, *v): if len(v) == 1: v = v[0] m = Matrix() pos = self._position * -1 m = m.look_at(pos[0], pos[1], pos[2], v[0], v[1], v[2], self.up[0], self.up[1], self.up[2]) self.modelview_matrix = m self._look_at = v self.update() def bind_to(self, renderer): """ Bind this camera to renderer """ self.renderer = renderer def update(self): if self.renderer: self.renderer._update_matrices() def update_projection_matrix(self): """ This function should be overridden in the subclasses
class ELiDEApp(App): """Extensible LiSE Development Environment. """ title = 'ELiDE' icon = 'icon.png' engine = ObjectProperty() branch = StringProperty('trunk') tick = NumericProperty(0) character = ObjectProperty() selection = ObjectProperty(None, allownone=True) selected_remote = ObjectProperty() def _get_character_name(self, *args): if self.character is None: return return self.character.name def _set_character_name(self, name): if self.character.name != name: self.character = self.engine.character[name] character_name = AliasProperty(_get_character_name, _set_character_name, bind=('character', )) def _pull_time(self, *args): if not self.engine: Clock.schedule_once(self._pull_time, 0) return (self.branch, self.tick) = self.engine.time pull_time = trigger(_pull_time) @trigger def _push_time(self, *args): if self.engine.time != (self.branch, self.tick): self.engine.time_travel(self.branch, self.tick, chars=[self.character.name], cb=self.mainscreen._update_from_chardiff) def set_tick(self, t): """Set my tick to the given value, cast to an integer.""" self.tick = int(t) def set_time(self, b, t=None): if t is None: (b, t) = b t = int(t) (self.branch, self.tick) = (b, t) def select_character(self, char): """Change my ``character`` to the selected character object if they aren't the same. """ if char == self.character: return self.character = char def build_config(self, config): """Set config defaults""" for sec in 'LiSE', 'ELiDE': config.adddefaultsection(sec) config.setdefaults( 'LiSE', { 'world': 'sqlite:///LiSEworld.db', 'code': 'LiSEcode.db', 'language': 'eng', 'logfile': '', 'loglevel': 'info' }) config.setdefaults( 'ELiDE', { 'boardchar': 'physical', 'debugger': 'no', 'inspector': 'no', 'user_kv': 'yes', 'play_speed': '1', 'thing_graphics': json.dumps( [("Marsh Davies' Island", 'marsh_davies_island_fg.atlas'), ('RLTiles: Body', 'base.atlas'), ('RLTiles: Basic clothes', 'body.atlas'), ('RLTiles: Armwear', 'arm.atlas'), ('RLTiles: Legwear', 'leg.atlas'), ('RLTiles: Right hand', 'hand1.atlas'), ('RLTiles: Left hand', 'hand2.atlas'), ('RLTiles: Boots', 'boot.atlas'), ('RLTiles: Hair', 'hair.atlas'), ('RLTiles: Beard', 'beard.atlas'), ('RLTiles: Headwear', 'head.atlas')]), 'place_graphics': json.dumps( [("Marsh Davies' Island", 'marsh_davies_island_bg.atlas'), ("Marsh Davies' Crypt", 'marsh_davies_crypt.atlas'), ('RLTiles: Dungeon', 'dungeon.atlas')]) }) config.write() def build(self): """Make sure I can use the database, create the tables as needed, and return the root widget. """ config = self.config Logger.debug( "ELiDEApp: starting with world {}, code {}, path {}".format( config['LiSE']['world'], config['LiSE']['code'], LiSE.__path__[-1])) if config['ELiDE']['debugger'] == 'yes': import pdb pdb.set_trace() self.manager = ScreenManager() if config['ELiDE']['inspector'] == 'yes': from kivy.core.window import Window from kivy.modules import inspector inspector.create_inspector(Window, self.manager) Clock.schedule_once(self._start_subprocess, 0.1) Clock.schedule_once(self._add_screens, 0.2) return self.manager def _start_subprocess(self, *args): config = self.config self.procman = EngineProcessManager() enkw = {'logger': Logger} if config['LiSE'].get('logfile'): enkw['logfile'] = config['LiSE']['logfile'] if config['LiSE'].get('loglevel'): enkw['loglevel'] = config['LiSE']['loglevel'] self.engine = self.procman.start(config['LiSE']['world'], config['LiSE']['code'], **enkw) self.pull_time() @self.engine.time.connect def pull_time(inst, **kwargs): self.branch = inst.branch self.tick = inst.tick @self.engine.string.language.connect def pull_lang(inst, **kwargs): self.strings.language = kwargs['language'] @self.engine.character.connect def pull_chars(*args): self.chars.names = list(self.engine.character) self.bind(branch=self._push_time, tick=self._push_time) char = config['ELiDE']['boardchar'] if char not in self.engine.character: self.engine.add_character(char) def _add_screens(self, *args): def toggler(screenname): def tog(*args): if self.manager.current == screenname: self.manager.current = 'main' else: self.manager.current = screenname return tog config = self.config self.pawncfg = ELiDE.spritebuilder.PawnConfigScreen( toggle=toggler('pawncfg'), data=json.loads(config['ELiDE']['thing_graphics'])) self.spotcfg = ELiDE.spritebuilder.SpotConfigScreen( toggle=toggler('spotcfg'), data=json.loads(config['ELiDE']['place_graphics'])) self.rules = ELiDE.rulesview.RulesScreen(engine=self.engine, toggle=toggler('rules')) self.chars = ELiDE.charsview.CharactersScreen( engine=self.engine, toggle=toggler('chars'), names=list(self.engine.character), new_board=self.new_board) self.bind(character_name=self.chars.setter('character_name')) def chars_push_character_name(*args): self.unbind(character_name=self.chars.setter('character_name')) self.character_name = self.chars.character_name self.bind(character_name=self.chars.setter('character_name')) self.chars.push_character_name = chars_push_character_name self.strings = ELiDE.stores.StringsEdScreen( language=self.engine.string.language, language_setter=self._set_language, toggle=toggler('strings')) self.funcs = ELiDE.stores.FuncsEdScreen(name='funcs', toggle=toggler('funcs')) self.select_character( self.engine.character[config['ELiDE']['boardchar']]) self.statcfg = ELiDE.statcfg.StatScreen(toggle=toggler('statcfg'), branch=self.branch, tick=self.tick, engine=self.engine) self.bind(selected_remote=self.statcfg.setter('remote'), branch=self.statcfg.setter('branch'), tick=self.statcfg.setter('tick')) self.mainscreen = ELiDE.screen.MainScreen( use_kv=config['ELiDE']['user_kv'] == 'yes', play_speed=int(config['ELiDE']['play_speed']), boards={ name: Board(character=char) for name, char in self.engine.character.items() }) if self.mainscreen.statlist: self.statcfg.statlist = self.mainscreen.statlist self.mainscreen.bind(statlist=self.statcfg.setter('statlist')) self.bind(selection=self.reremote, character=self.reremote) self.selected_remote = self._get_selected_remote() for wid in (self.mainscreen, self.pawncfg, self.spotcfg, self.statcfg, self.rules, self.chars, self.strings, self.funcs): self.manager.add_widget(wid) def _set_language(self, lang): self.engine.string.language = lang def _get_selected_remote(self): if self.selection is None: return self.character.stat elif hasattr(self.selection, 'remote'): return self.selection.remote elif (hasattr(self.selection, 'portal') and self.selection.portal is not None): return self.selection.portal else: raise ValueError("Invalid selection: {}".format(self.selection)) def reremote(self, *args): self.selected_remote = self._get_selected_remote() def on_character_name(self, *args): if self.config['ELiDE']['boardchar'] != self.character_name: self.config['ELiDE']['boardchar'] = self.character_name def on_character(self, *args): self.selection = None def on_pause(self): """Sync the database with the current state of the game.""" self.engine.commit() self.config.write() def on_stop(self, *largs): """Sync the database, wrap up the game, and halt.""" self.procman.shutdown() self.config.write() def on_selection(self, *args): Logger.debug("ELiDEApp: selection {}".format(self.selection)) def delete_selection(self): """Delete both the selected widget and whatever it represents.""" selection = self.selection if selection is None: return if isinstance(selection, ArrowWidget): self.selection = None self.mainscreen.boardview.board.rm_arrow( selection.origin.name, selection.destination.name) selection.portal.delete() elif isinstance(selection, Spot): self.selection = None self.mainscreen.boardview.board.rm_spot(selection.name) selection.remote.delete() else: assert isinstance(selection, Pawn) self.selection = None self.mainscreen.boardview.board.rm_pawn(selection.name) selection.remote.delete() def new_board(self, name): """Make a board for a character name, and switch to it.""" char = self.engine.character[name] board = Board(character=char) self.mainscreen.boards[name] = board self.character = char
def test_alias(self): from kivy.properties import NumericProperty, AliasProperty wid.__class__.x = x = NumericProperty(0) x.link(wid, 'x') x.link_deps(wid, 'x') wid.__class__.width = width = NumericProperty(100) width.link(wid, 'width') width.link_deps(wid, 'width') def get_right(self): return x.get(self) + width.get(self) def set_right(self, value): x.set(self, value - width.get(self)) right = AliasProperty(get_right, set_right, bind=('x', 'width')) right.link(wid, 'right') right.link_deps(wid, 'right') self.assertEqual(right.get(wid), 100) x.set(wid, 500) self.assertEqual(right.get(wid), 600) width.set(wid, 50) self.assertEqual(right.get(wid), 550) right.set(wid, 100) self.assertEqual(width.get(wid), 50) self.assertEqual(x.get(wid), 50) # test observer global observe_called observe_called = 0 def observe(obj, value): global observe_called observe_called = 1 right.bind(wid, observe) x.set(wid, 100) self.assertEqual(observe_called, 1) observe_called = 0 x.set(wid, 100) self.assertEqual(observe_called, 0) width.set(wid, 900) self.assertEqual(observe_called, 1) observe_called = 0 right.set(wid, 700) self.assertEqual(observe_called, 1) observe_called = 0 right.set(wid, 700) self.assertEqual(observe_called, 0)
class MDRaisedButton(ThemableBehavior, RectangularRippleBehavior, ElevationBehavior, ButtonBehavior, AnchorLayout): _bg_color_down = ListProperty([]) background_color = ListProperty() background_color_down = ListProperty() background_color_disabled = ListProperty() theme_text_color = OptionProperty( None, allownone=True, options=['Primary', 'Secondary', 'Hint', 'Error', 'Custom']) text_color = ListProperty(None, allownone=True) def _get_bg_color_down(self): return self._bg_color_down def _set_bg_color_down(self, color, alpha=None): if len(color) == 2: self._bg_color_down = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._bg_color_down[3] = alpha elif len(color) == 4: self._bg_color_down = color background_color_down = AliasProperty(_get_bg_color_down, _set_bg_color_down, bind=('_bg_color_down', )) _bg_color_disabled = ListProperty([]) def _get_bg_color_disabled(self): return self._bg_color_disabled def _set_bg_color_disabled(self, color, alpha=None): if len(color) == 2: self._bg_color_disabled = get_color_from_hex( colors[color[0]][color[1]]) if alpha: self._bg_color_disabled[3] = alpha elif len(color) == 4: self._bg_color_disabled = color background_color_disabled = AliasProperty(_get_bg_color_disabled, _set_bg_color_disabled, bind=('_bg_color_disabled', )) _elev_norm = NumericProperty(2) def _get_elev_norm(self): return self._elev_norm def _set_elev_norm(self, value): self._elev_norm = value if value <= 12 else 12 self._elev_raised = (value + 6) if value + 6 <= 12 else 12 self.elevation = self._elev_norm elevation_normal = AliasProperty(_get_elev_norm, _set_elev_norm, bind=('_elev_norm', )) _elev_raised = NumericProperty(8) def _get_elev_raised(self): return self._elev_raised def _set_elev_raised(self, value): self._elev_raised = value if value + self._elev_norm <= 12 else 12 elevation_raised = AliasProperty(_get_elev_raised, _set_elev_raised, bind=('_elev_raised', )) text = StringProperty() _text = StringProperty() def __init__(self, **kwargs): super(MDRaisedButton, self).__init__(**kwargs) self.elevation_press_anim = Animation(elevation=self.elevation_raised, duration=.2, t='out_quad') self.elevation_release_anim = Animation( elevation=self.elevation_normal, duration=.2, t='out_quad') def on_disabled(self, instance, value): if value: self.elevation = 0 else: self.elevation = self.elevation_normal super(MDRaisedButton, self).on_disabled(instance, value) def on_touch_down(self, touch): if not self.disabled: if touch.is_mouse_scrolling: return False if not self.collide_point(touch.x, touch.y): return False if self in touch.ud: return False Animation.cancel_all(self, 'elevation') self.elevation_press_anim.start(self) return super(MDRaisedButton, self).on_touch_down(touch) def on_touch_up(self, touch): if not self.disabled: if touch.grab_current is not self: return super(ButtonBehavior, self).on_touch_up(touch) Animation.cancel_all(self, 'elevation') self.elevation_release_anim.start(self) return super(MDRaisedButton, self).on_touch_up(touch) def on_text(self, instance, text): self._text = text.upper() def on__elev_norm(self, instance, value): self.elevation_release_anim = Animation(elevation=value, duration=.2, t='out_quad') def on__elev_raised(self, instance, value): self.elevation_press_anim = Animation(elevation=value, duration=.2, t='out_quad')