commit a1681eeeba4c212dcbb6087f1ed3201378492bd8
parent f33fbefce08468d22ecbb60802e7e38ff8db5f15
Author: qua-non <akshayaurora@gmail.com>
Date: Sun, 2 Mar 2014 00:41:58 +0530
handle app start, background wallet interfacing. UX to be merged next.
Diffstat:
4 files changed, 515 insertions(+), 30 deletions(-)
diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py
@@ -495,8 +495,8 @@ class RestoreSeedDialog(CreateAccountDialog):
tis._keyboard.bind(on_key_down=self.on_key_down)
stepper = self.ids.stepper
stepper.opacity = 1
- stepper.source = ('atlas://gui/kivy/theming"
- "/light/stepper_restore_seed')
+ stepper.source = ('atlas://gui/kivy/theming'
+ '/light/stepper_restore_seed')
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
app.navigation_higherarchy.append(_back)
@@ -582,7 +582,15 @@ class ChangePasswordDialog(CreateAccountDialog):
if value:
stepper = self.ids.stepper
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'
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
app.navigation_higherarchy.append(_back)
diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py
@@ -20,7 +20,7 @@ app = App.get_running_app()
class InstallWizard(Widget):
- '''Instalation Wizzard. Responsible for instantiating the
+ '''Installation Wizard. Responsible for instantiating the
creation/restoration of wallets.
events::
@@ -232,7 +232,7 @@ class InstallWizard(Widget):
ti_new_password.focus = True
else:
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':
try:
@@ -253,7 +253,7 @@ class InstallWizard(Widget):
try:
seed = wallet.decode_seed(password)
except BaseException:
- return app.show_error(_('Incorrect Password'))
+ return app.show_error(_('Incorrect Password'), duration=.5)
# test carefully
try:
@@ -291,6 +291,7 @@ class InstallWizard(Widget):
if mode in ('restore', 'create'):
# auto cycle
self.config.set_key('auto_cycle', True, True)
+
# start wallet threads
wallet.start_threads(self.network)
@@ -303,14 +304,16 @@ class InstallWizard(Widget):
def on_complete(*l):
if not self.network:
- app.show_info(_("This wallet was restored offline."
- "It may contain more addresses than displayed."))
+ app.show_info(
+ _("This wallet was restored offline. It may contain more"
+ " addresses than displayed."), duration=.5)
return self.dispatch('on_wizard_complete', wallet)
if wallet.is_found():
- app.show_info(_("Recovery successful"))
+ app.show_info(_("Recovery successful"), duration=.5)
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)
self.waiting_dialog(lambda: wallet.restore(get_text),
diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv
@@ -104,7 +104,7 @@
size_hint: None, 1
width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp')
Widget:
- size_hint_y: None
+ size_hint_x: None
width: '5dp'
Label:
id: lbl
@@ -112,7 +112,8 @@
font_size: '12sp'
text: root.message
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)
<-CreateAccountDialog>
diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
@@ -1,7 +1,9 @@
import sys
+from decimal import Decimal
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.core.window import Window
@@ -10,11 +12,15 @@ from kivy.logger import Logger
from kivy.utils import platform
from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
StringProperty, ListProperty)
+from kivy.clock import Clock
#inclusions for factory so that widgets can be used in kv
from gui.kivy.drawer import Drawer
from gui.kivy.dialog import InfoBubble
+# delayed imports
+notification = None
+
class ElectrumWindow(App):
title = _('Electrum App')
@@ -25,10 +31,10 @@ class ElectrumWindow(App):
:attr:`wallet` is a `ObjectProperty` defaults to None.
'''
- conf = ObjectProperty(None)
+ electrum_config = ObjectProperty(None)
'''Holds the electrum config
- :attr:`conf` is a `ObjectProperty`, defaults to None.
+ :attr:`electrum_config` is a `ObjectProperty`, defaults to None.
'''
status = StringProperty(_('Uninitialised'))
@@ -37,10 +43,60 @@ class ElectrumWindow(App):
: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 ...
- :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'))
@@ -84,13 +140,20 @@ class ElectrumWindow(App):
def __init__(self, **kwargs):
# initialize variables
self.info_bubble = None
+ self.console = None
+ self.exchanger = None
+
super(ElectrumWindow, self).__init__(**kwargs)
self.network = network = kwargs.get('network')
self.electrum_config = config = kwargs.get('config')
- def load_wallet(self, wallet):
- # TODO
- pass
+ # create triggers so as to minimize updation a max of 5 times a sec
+ self._trigger_update_status = Clock.create_trigger(self.update_status,
+ .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):
from kivy.lang import Builder
@@ -98,15 +161,21 @@ class ElectrumWindow(App):
def _pause(self):
if platform == 'android':
+ # move activity to back
from jnius import autoclass
python_act = autoclass('org.renpy.android.PythonActivity')
mActivity = python_act.mActivity
mActivity.moveTaskToBack(True)
def on_start(self):
+ ''' This is the start point of the kivy ui
+ '''
Window.bind(size=self.on_size,
on_keyboard=self.on_keyboard)
- Window.bind(keyboard_height=self.on_keyboard_height)
+ Window.bind(on_key_down=self.on_key_down)
+ if platform == 'android':
+ #
+ Window.bind(keyboard_height=self.on_keyboard_height)
self.on_size(Window, Window.size)
config = self.electrum_config
storage = WalletStorage(config)
@@ -127,8 +196,11 @@ class ElectrumWindow(App):
self.on_resume()
+ def on_stop(self):
+ self.wallet.stop_threads()
+
def on_back(self):
- ''' Manage screen higherarchy
+ ''' Manage screen hierarchy
'''
try:
self.navigation_higherarchy.pop()()
@@ -146,9 +218,28 @@ class ElectrumWindow(App):
Window.children[1]
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):
# 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)
return True
if key == 27:
@@ -160,14 +251,18 @@ class ElectrumWindow(App):
Logger.debug('Electrum: No Wallet set/found. Exiting...')
app.show_error('Electrum: No Wallet set/found. Exiting...',
exit=True)
- return
+ Logger.info('wizard complete')
+
+ self.init_ui()
# plugins that need to change the GUI do it here
#run_hook('init')
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)
#if url: w.set_url(url)
@@ -177,7 +272,381 @@ class ElectrumWindow(App):
#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):
'''
@@ -231,7 +700,8 @@ class ElectrumWindow(App):
pos=None,
arrow_pos=None,
exit=False,
- icon='atlas://gui/kivy/theming/light/error',):
+ icon='atlas://gui/kivy/theming/light/error',
+ duration=0):
''' Show a error Message Bubble.
'''
self.show_info_bubble(
@@ -240,16 +710,19 @@ class ElectrumWindow(App):
width=width,
pos=pos or Window.center,
arrow_pos=arrow_pos,
- exit=exit)
+ exit=exit,
+ duration=duration)
def show_info(self, error,
width='200dp',
pos=None,
arrow_pos=None,
- exit=False):
+ exit=False,
+ duration=0):
''' 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,
text=_('Hello World'),