electrum-bitcoinprivate/gui/kivy/main_window.py

809 lines
28 KiB
Python
Raw Normal View History

import sys
from decimal import Decimal
from electrum import WalletStorage, Wallet
from electrum.i18n import _, set_language
from electrum.wallet import format_satoshis
from kivy.app import App
from kivy.core.window import Window
2014-03-18 21:31:15 -07:00
from kivy.lang import Builder
from kivy.logger import Logger
2014-03-18 21:31:15 -07:00
from kivy.metrics import inch
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
2014-03-18 21:31:15 -07:00
from electrum_gui.kivy.drawer import Drawer
from electrum_gui.kivy.dialog import InfoBubble
# delayed imports
notification = None
class ElectrumWindow(App):
title = _('Electrum App')
wallet = ObjectProperty(None)
'''Holds the electrum wallet
:attr:`wallet` is a `ObjectProperty` defaults to None.
'''
electrum_config = ObjectProperty(None)
'''Holds the electrum config
:attr:`electrum_config` is a `ObjectProperty`, defaults to None.
'''
status = StringProperty(_('Uninitialised'))
'''The status of the connection should show the balance when connected
:attr:`status` is a `StringProperty` defaults to 'uninitialised'
'''
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 mBTC...
:attr:`base_unit` is a `AliasProperty` defaults to the unit set in
electrum config.
'''
_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'
'''
_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'
'''
navigation_higherarchy = ListProperty([])
'''This is a list of the current navigation higherarchy of the app used to
navigate using back button.
:attr:`navigation_higherarchy` is s `ListProperty` defaults to []
'''
__events__ = ('on_back', )
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')
# create triggers so as to minimize updation a max of 2 times a sec
self._trigger_update_status =\
Clock.create_trigger(self.update_status, .5)
self._trigger_update_console =\
Clock.create_trigger(self.update_console, .5)
self._trigger_notify_transactions = \
Clock.create_trigger(self.notify_transactions, .5)
def build(self):
from kivy.lang import Builder
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.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(on_key_down=self.on_key_down)
# register fonts
from kivy.core.text import Label
Label.register('Roboto',
'data/fonts/Roboto.ttf',
'data/fonts/Roboto.ttf',
'data/fonts/Roboto-Bold.ttf',
'data/fonts/Roboto-Bold.ttf')
if platform == 'android':
#
Window.bind(keyboard_height=self.on_keyboard_height)
self.on_size(Window, Window.size)
config = self.electrum_config
storage = WalletStorage(config)
Logger.info('Electrum: Check for existing wallet')
if not storage.file_exists:
# start installation wizard
Logger.debug('Electrum: Wallet not found. Launching install wizard')
import installwizard
wizard = installwizard.InstallWizard(config, self.network,
storage)
wizard.bind(on_wizard_complete=self.on_wizard_complete)
wizard.run()
else:
wallet = Wallet(storage)
wallet.start_threads(self.network)
self.on_wizard_complete(None, wallet)
self.on_resume()
def on_stop(self):
if self.wallet:
self.wallet.stop_threads()
def on_back(self):
''' Manage screen hierarchy
'''
try:
self.navigation_higherarchy.pop()()
except IndexError:
# capture back button and pause app.
self._pause()
def on_keyboard_height(self, *l):
from kivy.animation import Animation
from kivy.uix.popup import Popup
active_widg = Window.children[0]
active_widg = active_widg\
if (active_widg == self.root or\
issubclass(active_widg.__class__, Popup)) else\
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): #f1/settings button on android
self.gui.main_gui.toggle_settings(self)
return True
if key == 27:
self.dispatch('on_back')
return True
def on_wizard_complete(self, instance, wallet):
if not wallet:
Logger.debug('Electrum: No Wallet set/found. Exiting...')
app.show_error('Electrum: No Wallet set/found. Exiting...',
exit=True)
self.init_ui()
# plugins that need to change the GUI do it here
#run_hook('init')
self.load_wallet(wallet)
#TODO: URI handling
#self.windows.append(w)
#if url: w.set_url(url)
# TODO:remove properties are used instead
#Clock.schedule_interval(self.timer_actions, .5)
#TODO: remove not needed properties allow on_property events
#def timer_actions(self):
# if self.need_update.is_set():
# self.update_wallet()
# self.need_update.clear()
# run_hook('timer_actions')
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
2014-03-18 21:31:15 -07:00
self.screens = ['mainscreen']
self.load_screen(index=0)
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 electrum_gui.kivy.plugins.exchange_rate import Exchanger
self.exchanger = Exchanger(self)
self.exchanger.start()
quote_currency = self.exchanger.currency
quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
if quote_currency and mode == 'symbol':
quote_currency = self.exchanger.symbols.get(quote_currency,
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()
2014-03-18 21:31:15 -07:00
print quote_currencies
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):
'''
'''
# pause nfc
# pause qrscanner(Camera) if active
return True
def on_resume(self):
'''
'''
# resume nfc
# resume camera if active
pass
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'
Logger.debug('orientation: {} ui_mode: {}'.format(self._orientation,
self._ui_mode))
2014-03-18 21:31:15 -07:00
def load_screen(self, index=0, direction='left', manager=None):
''' Load the appropriate screen as mentioned in the parameters.
'''
2014-03-18 21:31:15 -07:00
manager = manager or self.root.manager
screen = Builder.load_file('gui/kivy/ui_screens/'\
+ self.screens[index] + '.kv')
screen.name = self.screens[index]
2014-03-18 21:31:15 -07:00
manager.switch_to(screen, direction=direction)
def load_next_screen(self):
'''
'''
manager = root.manager
try:
2014-03-18 21:31:15 -07:00
self.load_screen(self.screens.index(manager.current_screen.name)+1,
manager=manager)
except IndexError:
self.load_screen()
def load_previous_screen(self):
'''
'''
manager = root.manager
try:
self.load_screen(self.screens.index(manager.current_screen.name)-1,
2014-03-18 21:31:15 -07:00
direction='right',
manager=manager)
except IndexError:
2014-03-18 21:31:15 -07:00
pass
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/error',
duration=duration,
modal=modal,
exit=exit,
pos=pos,
arrow_pos=arrow_pos)
def show_info_bubble(self,
text=_('Hello World'),
pos=(0, 0),
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 = InfoBubble()
if info_bubble.parent:
Window.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
pos = (Window.center[0], Window.center[1] - info_bubble.center[1])
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
info_bubble.show(pos, duration, width, modal=modal, exit=exit)