handle app start, background wallet interfacing. UX to be merged next.

This commit is contained in:
qua-non 2014-03-02 00:41:58 +05:30 committed by ThomasV
parent f33fbefce0
commit a1681eeeba
4 changed files with 515 additions and 30 deletions

View File

@ -495,8 +495,8 @@ class RestoreSeedDialog(CreateAccountDialog):
tis._keyboard.bind(on_key_down=self.on_key_down) tis._keyboard.bind(on_key_down=self.on_key_down)
stepper = self.ids.stepper stepper = self.ids.stepper
stepper.opacity = 1 stepper.opacity = 1
stepper.source = ('atlas://gui/kivy/theming" stepper.source = ('atlas://gui/kivy/theming'
"/light/stepper_restore_seed') '/light/stepper_restore_seed')
self._back = _back = partial(self.ids.back.dispatch, 'on_release') self._back = _back = partial(self.ids.back.dispatch, 'on_release')
app.navigation_higherarchy.append(_back) app.navigation_higherarchy.append(_back)
@ -582,7 +582,15 @@ class ChangePasswordDialog(CreateAccountDialog):
if value: if value:
stepper = self.ids.stepper stepper = self.ids.stepper
stepper.opacity = 1 stepper.opacity = 1
self.ids.ti_wallet_name.focus = True t_wallet_name = self.ids.ti_wallet_name
if self.mode in ('create', 'restore'):
t_wallet_name.text = 'Default Wallet'
t_wallet_name.readonly = True
self.ids.ti_new_password.focus = True
else:
t_wallet_name.text = ''
t_wallet_name.readonly = False
t_wallet_name.focus = True
stepper.source = 'atlas://gui/kivy/theming/light/stepper_left' stepper.source = 'atlas://gui/kivy/theming/light/stepper_left'
self._back = _back = partial(self.ids.back.dispatch, 'on_release') self._back = _back = partial(self.ids.back.dispatch, 'on_release')
app.navigation_higherarchy.append(_back) app.navigation_higherarchy.append(_back)

View File

@ -20,7 +20,7 @@ app = App.get_running_app()
class InstallWizard(Widget): class InstallWizard(Widget):
'''Instalation Wizzard. Responsible for instantiating the '''Installation Wizard. Responsible for instantiating the
creation/restoration of wallets. creation/restoration of wallets.
events:: events::
@ -232,7 +232,7 @@ class InstallWizard(Widget):
ti_new_password.focus = True ti_new_password.focus = True
else: else:
ti_password.focus = True ti_password.focus = True
return app.show_error(_('Passwords do not match')) return app.show_error(_('Passwords do not match'), duration=.5)
if mode == 'restore': if mode == 'restore':
try: try:
@ -253,7 +253,7 @@ class InstallWizard(Widget):
try: try:
seed = wallet.decode_seed(password) seed = wallet.decode_seed(password)
except BaseException: except BaseException:
return app.show_error(_('Incorrect Password')) return app.show_error(_('Incorrect Password'), duration=.5)
# test carefully # test carefully
try: try:
@ -291,6 +291,7 @@ class InstallWizard(Widget):
if mode in ('restore', 'create'): if mode in ('restore', 'create'):
# auto cycle # auto cycle
self.config.set_key('auto_cycle', True, True) self.config.set_key('auto_cycle', True, True)
# start wallet threads # start wallet threads
wallet.start_threads(self.network) wallet.start_threads(self.network)
@ -303,14 +304,16 @@ class InstallWizard(Widget):
def on_complete(*l): def on_complete(*l):
if not self.network: if not self.network:
app.show_info(_("This wallet was restored offline." app.show_info(
"It may contain more addresses than displayed.")) _("This wallet was restored offline. It may contain more"
" addresses than displayed."), duration=.5)
return self.dispatch('on_wizard_complete', wallet) return self.dispatch('on_wizard_complete', wallet)
if wallet.is_found(): if wallet.is_found():
app.show_info(_("Recovery successful")) app.show_info(_("Recovery successful"), duration=.5)
else: else:
app.show_info(_("No transactions found for this seed")) app.show_info(_("No transactions found for this seed"),
duration=.5)
return self.dispatch('on_wizard_complete', wallet) return self.dispatch('on_wizard_complete', wallet)
self.waiting_dialog(lambda: wallet.restore(get_text), self.waiting_dialog(lambda: wallet.restore(get_text),

View File

@ -104,7 +104,7 @@
size_hint: None, 1 size_hint: None, 1
width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp') width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp')
Widget: Widget:
size_hint_y: None size_hint_x: None
width: '5dp' width: '5dp'
Label: Label:
id: lbl id: lbl
@ -112,7 +112,8 @@
font_size: '12sp' font_size: '12sp'
text: root.message text: root.message
text_size: self.width, None text_size: self.width, None
size_hint: None, 1 valign: 'middle'
size_hint: 1, 1
width: 0 if root.fs else (root.width - img.width) width: 0 if root.fs else (root.width - img.width)
<-CreateAccountDialog> <-CreateAccountDialog>

View File

@ -1,7 +1,9 @@
import sys import sys
from decimal import Decimal
from electrum import WalletStorage, Wallet from electrum import WalletStorage, Wallet
from electrum.i18n import _ from electrum.i18n import _, set_language
from electrum.wallet import format_satoshis
from kivy.app import App from kivy.app import App
from kivy.core.window import Window from kivy.core.window import Window
@ -10,11 +12,15 @@ from kivy.logger import Logger
from kivy.utils import platform from kivy.utils import platform
from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty, from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
StringProperty, ListProperty) StringProperty, ListProperty)
from kivy.clock import Clock
#inclusions for factory so that widgets can be used in kv #inclusions for factory so that widgets can be used in kv
from gui.kivy.drawer import Drawer from gui.kivy.drawer import Drawer
from gui.kivy.dialog import InfoBubble from gui.kivy.dialog import InfoBubble
# delayed imports
notification = None
class ElectrumWindow(App): class ElectrumWindow(App):
title = _('Electrum App') title = _('Electrum App')
@ -25,10 +31,10 @@ class ElectrumWindow(App):
:attr:`wallet` is a `ObjectProperty` defaults to None. :attr:`wallet` is a `ObjectProperty` defaults to None.
''' '''
conf = ObjectProperty(None) electrum_config = ObjectProperty(None)
'''Holds the electrum config '''Holds the electrum config
:attr:`conf` is a `ObjectProperty`, defaults to None. :attr:`electrum_config` is a `ObjectProperty`, defaults to None.
''' '''
status = StringProperty(_('Uninitialised')) status = StringProperty(_('Uninitialised'))
@ -37,10 +43,60 @@ class ElectrumWindow(App):
:attr:`status` is a `StringProperty` defaults to _'uninitialised' :attr:`status` is a `StringProperty` defaults to _'uninitialised'
''' '''
base_unit = StringProperty('BTC') def _get_num_zeros(self):
try:
return self.electrum_config.get('num_zeros', 0)
except AttributeError:
return 0
def _set_num_zeros(self):
try:
self.electrum_config.set_key('num_zeros', value, True)
except AttributeError:
Logger.error('Electrum: Config not available '
'While trying to save value to config')
num_zeros = AliasProperty(_get_num_zeros , _set_num_zeros)
'''Number of zeros used while representing the value in base_unit.
'''
def _get_decimal(self):
try:
return self.electrum_config.get('decimal_point', 8)
except AttributeError:
return 8
def _set_decimal(self, value):
try:
self.electrum_config.set_key('decimal_point', value, True)
except AttributeError:
Logger.error('Electrum: Config not set '
'While trying to save value to config')
decimal_point = AliasProperty(_get_decimal, _set_decimal)
'''This defines the decimal point to be used determining the
:attr:`base_unit`.
:attr:`decimal_point` is a `AliasProperty` defaults to the value gotten
from electrum config.
'''
def _get_bu(self):
assert self.decimal_point in (5,8)
return "BTC" if self.decimal_point == 8 else "mBTC"
def _set_bu(self, value):
try:
self.electrum_config.set_key('base_unit', value, True)
except AttributeError:
Logger.error('Electrum: Config not set '
'While trying to save value to config')
base_unit = AliasProperty(_get_bu, _set_bu, bind=('decimal_point',))
'''BTC or UBTC or ... '''BTC or UBTC or ...
:attr:`base_unit` is a `StringProperty` defaults to 'BTC' :attr:`base_unit` is a `AliasProperty` defaults to the unit set in
electrum config.
''' '''
_ui_mode = OptionProperty('phone', options=('tablet', 'phone')) _ui_mode = OptionProperty('phone', options=('tablet', 'phone'))
@ -84,13 +140,20 @@ class ElectrumWindow(App):
def __init__(self, **kwargs): def __init__(self, **kwargs):
# initialize variables # initialize variables
self.info_bubble = None self.info_bubble = None
self.console = None
self.exchanger = None
super(ElectrumWindow, self).__init__(**kwargs) super(ElectrumWindow, self).__init__(**kwargs)
self.network = network = kwargs.get('network') self.network = network = kwargs.get('network')
self.electrum_config = config = kwargs.get('config') self.electrum_config = config = kwargs.get('config')
def load_wallet(self, wallet): # create triggers so as to minimize updation a max of 5 times a sec
# TODO self._trigger_update_status = Clock.create_trigger(self.update_status,
pass .2)
self._trigger_update_console = Clock.create_trigger(self.update_console,
.2)
self._trigger_notify_transactions = \
Clock.create_trigger(self.notify_transactions, .2)
def build(self): def build(self):
from kivy.lang import Builder from kivy.lang import Builder
@ -98,14 +161,20 @@ class ElectrumWindow(App):
def _pause(self): def _pause(self):
if platform == 'android': if platform == 'android':
# move activity to back
from jnius import autoclass from jnius import autoclass
python_act = autoclass('org.renpy.android.PythonActivity') python_act = autoclass('org.renpy.android.PythonActivity')
mActivity = python_act.mActivity mActivity = python_act.mActivity
mActivity.moveTaskToBack(True) mActivity.moveTaskToBack(True)
def on_start(self): def on_start(self):
''' This is the start point of the kivy ui
'''
Window.bind(size=self.on_size, Window.bind(size=self.on_size,
on_keyboard=self.on_keyboard) on_keyboard=self.on_keyboard)
Window.bind(on_key_down=self.on_key_down)
if platform == 'android':
#
Window.bind(keyboard_height=self.on_keyboard_height) Window.bind(keyboard_height=self.on_keyboard_height)
self.on_size(Window, Window.size) self.on_size(Window, Window.size)
config = self.electrum_config config = self.electrum_config
@ -127,8 +196,11 @@ class ElectrumWindow(App):
self.on_resume() self.on_resume()
def on_stop(self):
self.wallet.stop_threads()
def on_back(self): def on_back(self):
''' Manage screen higherarchy ''' Manage screen hierarchy
''' '''
try: try:
self.navigation_higherarchy.pop()() self.navigation_higherarchy.pop()()
@ -146,9 +218,28 @@ class ElectrumWindow(App):
Window.children[1] Window.children[1]
Animation(y=Window.keyboard_height, d=.1).start(active_widg) Animation(y=Window.keyboard_height, d=.1).start(active_widg)
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): def on_keyboard(self, instance, key, keycode, codepoint, modifiers):
# override settings button # override settings button
if key in (319, 282): if key in (319, 282): #f1/settings button on android
self.gui.main_gui.toggle_settings(self) self.gui.main_gui.toggle_settings(self)
return True return True
if key == 27: if key == 27:
@ -160,14 +251,18 @@ class ElectrumWindow(App):
Logger.debug('Electrum: No Wallet set/found. Exiting...') Logger.debug('Electrum: No Wallet set/found. Exiting...')
app.show_error('Electrum: No Wallet set/found. Exiting...', app.show_error('Electrum: No Wallet set/found. Exiting...',
exit=True) exit=True)
return Logger.info('wizard complete')
self.init_ui()
# plugins that need to change the GUI do it here # plugins that need to change the GUI do it here
#run_hook('init') #run_hook('init')
self.load_wallet(wallet) self.load_wallet(wallet)
Clock.schedule_once(update_wallet) # check and remove this load_wallet calls update_wallet no
# need for this here
#Clock.schedule_once(update_wallet)
#self.windows.append(w) #self.windows.append(w)
#if url: w.set_url(url) #if url: w.set_url(url)
@ -177,7 +272,381 @@ class ElectrumWindow(App):
#self.app.exec_() #self.app.exec_()
wallet.stop_threads() def init_ui(self):
''' Initialize The Ux part of electrum. This function performs the basic
tasks of setting up the ui.
'''
# unused?
#self._close_electrum = False
#self._tray_icon = 'icons/" + (electrum_dark_icon.png'\
# if platform == 'mac' else 'electrum_light_icon.png')
#setup tray
#self.tray = SystemTrayIcon(self.icon, self)
#self.tray.setToolTip('Electrum')
#self.tray.activated.connect(self.tray_activated)
set_language(self.electrum_config.get('language'))
self.funds_error = False
self.completions = []
# setup UX
#self.load_dashboard
self.icon = "icons/electrum.png"
# load and focus the ui
# connect callbacks
if self.network:
self.network.register_callback(
'updated', self._trigger_update_status)
self.network.register_callback(
'banner', self._trigger_update_console)
self.network.register_callback(
'disconnected', self._trigger_update_status)
self.network.register_callback(
'disconnecting', self._trigger_update_status)
self.network.register_callback('new_transaction',
self._trigger_notify_transactions)
# set initial message
self.update_console()
self.wallet = None
def create_quote_text(self, btc_balance, mode='normal'):
'''
'''
if not self.exchanger:
from plugins.exchange_rate import Exchanger
self.exchanger = Exchanger(self)
self.exchanger.start()
quote_currency = self.electrum_config.get("currency", 'EUR')
quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
if mode == 'symbol':
if quote_currency:
quote_currency = self.exchanger.symbols[quote_currency]
if quote_balance is None:
quote_text = ""
else:
quote_text = " (%.2f %s)" % (quote_balance, quote_currency)
return quote_text
def set_currencies(self, quote_currencies):
self._trigger_update_status
#self.currencies = sorted(quote_currencies.keys())
def update_console(self, *dt):
if self.console:
self.console.showMessage(self.network.banner)
def load_wallet(self, wallet):
self.wallet = wallet
self.accounts_expanded = self.wallet.storage.get('accounts_expanded', {})
self.current_account = self.wallet.storage.get('current_account', None)
title = 'Electrum ' + self.wallet.electrum_version + ' - '\
+ self.wallet.storage.path
if wallet.is_watching_only():
title += ' [{}]'.format(_('watching only'))
self.title = title
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
self.notify_transactions()
self.update_account_selector()
#TODO
#self.new_account.setEnabled(self.wallet.seed_version>4)
#self.update_lock_icon()
#self.update_buttons_on_seed()
#run_hook('load_wallet', wallet)
def update_status(self, *dt):
if not self.wallet:
return
if self.network is None or not self.network.is_running():
text = _("Offline")
#icon = QIcon(":icons/status_disconnected.png")
elif self.network.is_connected():
unconfirmed = ''
quote_text = '.'
if not self.wallet.up_to_date:
text = _("Synchronizing...")
#icon = QIcon(":icons/status_waiting.png")
elif self.network.server_lag > 1:
text = _("Server is lagging (%d blocks)"%self.network.server_lag)
#icon = QIcon(":icons/status_lagging.png")
else:
c, u = self.wallet.get_account_balance(self.current_account)
text = self.format_amount(c)
if u:
unconfirmed = " [%s unconfirmed]"\
%( self.format_amount(u, True).strip())
quote_text = self.create_quote_text(Decimal(c+u)/100000000) or '.'
#r = {}
#run_hook('set_quote_text', c+u, r)
#quote = r.get(0)
#if quote:
# text += " (%s)"%quote
self.notify(_("Balance: ") + text)
#icon = QIcon(":icons/status_connected.png")
else:
text = _("Not connected")
#icon = QIcon(":icons/status_disconnected.png")
#TODO
#status_card = self.root.main_screen.ids.tabs.ids.\
# screen_dashboard.ids.status_card
self.status = text.strip()
#status_card.quote_text = quote_text.strip()
#status_card.uncomfirmed = unconfirmed.strip()
##app.base_unit = self.base_unit().strip()
def format_amount(self, x, is_diff=False, whitespaces=False):
'''
'''
return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
def update_wallet(self):
'''
'''
self.update_status()
if (self.wallet.up_to_date or
not self.network or not self.network.is_connected()):
#TODO
#self.update_history_tab()
#self.update_receive_tab()
#self.update_contacts_tab()
self.update_completions()
def update_account_selector(self):
# account selector
#TODO
return
accounts = self.wallet.get_account_names()
self.account_selector.clear()
if len(accounts) > 1:
self.account_selector.addItems([_("All accounts")] + accounts.values())
self.account_selector.setCurrentIndex(0)
self.account_selector.show()
else:
self.account_selector.hide()
def update_history_tab(self, see_all=False):
def parse_histories(items):
results = []
for item in items:
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
if conf > 0:
try:
time_str = datetime.datetime.fromtimestamp(
timestamp).isoformat(' ')[:-3]
except:
time_str = _("unknown")
if conf == -1:
time_str = _('unverified')
icon = "atlas://gui/kivy/theming/light/close"
elif conf == 0:
time_str = _('pending')
icon = "atlas://gui/kivy/theming/light/unconfirmed"
elif conf < 6:
time_str = '' # add new to fix error when conf < 0
conf = max(1, conf)
icon = "atlas://gui/kivy/theming/light/clock{}".format(conf)
else:
icon = "atlas://gui/kivy/theming/light/confirmed"
if value is not None:
v_str = self.format_amount(value, True, whitespaces=True)
else:
v_str = '--'
balance_str = self.format_amount(balance, whitespaces=True)
if tx_hash:
label, is_default_label = self.wallet.get_label(tx_hash)
else:
label = _('Pruned transaction outputs')
is_default_label = False
results.append((
conf, icon, time_str, label, v_str, balance_str, tx_hash))
return results
history_card = self.root.main_screen.ids.tabs.ids.\
screen_dashboard.ids.recent_activity_card
histories = parse_histories(reversed(
self.wallet.get_tx_history(self.current_account)))
#history_view.content_adapter.data = histories
# repopulate History Card
last_widget = history_card.ids.content.children[-1]
history_card.ids.content.clear_widgets()
history_add = history_card.ids.content.add_widget
history_add(last_widget)
RecentActivityItem = Factory.RecentActivityItem
history_card.ids.btn_see_all.opacity = (0 if see_all or
len(histories) < 8 else 1)
if not see_all:
histories = histories[:8]
create_quote_text = self.create_quote_text
for items in histories:
conf, icon, date_time, address, amount, balance, tx = items
ri = RecentActivityItem()
ri.icon = icon
ri.date = date_time
ri.address = address
ri.amount = amount
ri.quote_text = create_quote_text(
Decimal(amount)/100000000, mode='symbol')
ri.balance = balance
ri.confirmations = conf
ri.tx_hash = tx
history_add(ri)
def update_receive_tab(self):
#TODO move to address managment
return
data = []
if self.current_account is None:
account_items = self.wallet.accounts.items()
elif self.current_account != -1:
account_items = [(self.current_account, self.wallet.accounts.get(self.current_account))]
else:
account_items = []
for k, account in account_items:
name = account.get('name', str(k))
c, u = self.wallet.get_account_balance(k)
data = [(name, '', self.format_amount(c + u), '')]
for is_change in ([0, 1] if self.expert_mode else [0]):
if self.expert_mode:
name = "Receiving" if not is_change else "Change"
seq_item = (name, '', '', '')
data.append(seq_item)
else:
seq_item = data
is_red = False
gap = 0
for address in account[is_change]:
h = self.wallet.history.get(address, [])
if h == []:
gap += 1
if gap > self.wallet.gap_limit:
is_red = True
else:
gap = 0
num_tx = '*' if h == ['*'] else "%d" % len(h)
item = (address, self.wallet.labels.get(address, ''), '', num_tx)
data.append(item)
self.update_receive_item(item)
if self.wallet.imported_keys and (self.current_account is None
or self.current_account == -1):
c, u = self.wallet.get_imported_balance()
data.append((_('Imported'), '', self.format_amount(c + u), ''))
for address in self.wallet.imported_keys.keys():
item = (address, self.wallet.labels.get(address, ''), '', '')
data.append(item)
self.update_receive_item(item)
receive_list = app.root.main_screen.ids.tabs.ids\
.screen_receive.receive_view
receive_list.content_adapter.data = data
def update_contacts_tab(self):
data = []
for address in self.wallet.addressbook:
label = self.wallet.labels.get(address, '')
item = (address, label, "%d" % self.wallet.get_num_tx(address))
data.append(item)
# item.setFont(0, QFont(MONOSPACE_FONT))
# # 32 = label can be edited (bool)
# item.setData(0,32, True)
# # 33 = payto string
# item.setData(0,33, address)
self.run_hook('update_contacts_tab')
contact_list = app.root.main_screen.ids.tabs.ids.\
screen_contacts.ids.contacts_list
contact_list.content_adapter.data = data
def update_completions(self):
l = []
for addr, label in self.wallet.labels.items():
if addr in self.wallet.addressbook:
l.append(label + ' <' + addr + '>')
#self.run_hook('update_completions', l)
self.completions = l
def notify_transactions(self, *dt):
'''
'''
if not self.network or not self.network.is_connected():
return
iface = self.network.interface
if len(iface.pending_transactions_for_notifications) > 0:
# Combine the transactions if there are more then three
tx_amount = len(iface.pending_transactions_for_notifications)
if(tx_amount >= 3):
total_amount = 0
for tx in iface.pending_transactions_for_notifications:
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
if(v > 0):
total_amount += v
self.notify(_("{txs}s new transactions received. Total amount"
"received in the new transactions {amount}s"
"{unit}s").format(txs=tx_amount,
amount=self.format_amount(total_amount),
unit=self.base_unit()))
iface.pending_transactions_for_notifications = []
else:
for tx in iface.pending_transactions_for_notifications:
if tx:
iface.pending_transactions_for_notifications.remove(tx)
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
if(v > 0):
from pudb import set_trace; set_trace()
self.notify(
_("New transaction received. {amount}s {unit}s").
format( amount=self.format_amount(v),
unit=self.base_unit()))
def notify(self, message):
try:
global notification, os
if not notification:
from plyer import notification
import os
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): def on_pause(self):
''' '''
@ -231,7 +700,8 @@ class ElectrumWindow(App):
pos=None, pos=None,
arrow_pos=None, arrow_pos=None,
exit=False, exit=False,
icon='atlas://gui/kivy/theming/light/error',): icon='atlas://gui/kivy/theming/light/error',
duration=0):
''' Show a error Message Bubble. ''' Show a error Message Bubble.
''' '''
self.show_info_bubble( self.show_info_bubble(
@ -240,16 +710,19 @@ class ElectrumWindow(App):
width=width, width=width,
pos=pos or Window.center, pos=pos or Window.center,
arrow_pos=arrow_pos, arrow_pos=arrow_pos,
exit=exit) exit=exit,
duration=duration)
def show_info(self, error, def show_info(self, error,
width='200dp', width='200dp',
pos=None, pos=None,
arrow_pos=None, arrow_pos=None,
exit=False): exit=False,
duration=0):
''' Show a Info Message Bubble. ''' Show a Info Message Bubble.
''' '''
self.show_error(error, icon='atlas://gui/kivy/theming/light/error') self.show_error(error, icon='atlas://gui/kivy/theming/light/error',
duration=duration)
def show_info_bubble(self, def show_info_bubble(self,
text=_('Hello World'), text=_('Hello World'),