You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

427 lines
13 KiB

from weakref import ref
from decimal import Decimal
import re
import datetime
import traceback, sys
import threading
from kivy.app import App
from kivy.cache import Cache
from kivy.clock import Clock
from kivy.compat import string_types
from kivy.properties import (ObjectProperty, DictProperty, NumericProperty,
ListProperty, StringProperty)
from kivy.lang import Builder
from kivy.factory import Factory
from electrum.i18n import _
from electrum.util import profiler, parse_URI
from electrum import bitcoin
from electrum.util import timestamp_to_datetime
from electrum.plugins import run_hook
class CScreen(Factory.Screen):
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
action_view = ObjectProperty(None)
loaded = False
kvname = None
app = App.get_running_app()
def _change_action_view(self):
app = App.get_running_app()
action_bar = app.root.manager.current_screen.ids.action_bar
_action_view = self.action_view
if (not _action_view) or _action_view.parent:
return
action_bar.clear_widgets()
action_bar.add_widget(_action_view)
def on_enter(self):
# FIXME: use a proper event don't use animation time of screen
Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25)
pass
def update(self):
pass
@profiler
def load_screen(self):
self.screen = Builder.load_file('gui/kivy/uix/ui_screens/' + self.kvname + '.kv')
self.add_widget(self.screen)
self.loaded = True
self.update()
setattr(self.app, self.kvname + '_screen', self)
def on_activate(self):
if self.kvname and not self.loaded:
self.load_screen()
#Clock.schedule_once(lambda dt: self._change_action_view())
def on_leave(self):
self.dispatch('on_deactivate')
def on_deactivate(self):
pass
#Clock.schedule_once(lambda dt: self._change_action_view())
class HistoryScreen(CScreen):
tab = ObjectProperty(None)
kvname = 'history'
def __init__(self, **kwargs):
self.ra_dialog = None
super(HistoryScreen, self).__init__(**kwargs)
def get_history_rate(self, btc_balance, timestamp):
date = timestamp_to_datetime(timestamp)
return run_hook('historical_value_str', btc_balance, date)
def parse_history(self, items):
for item in items:
tx_hash, conf, value, timestamp, balance = item
time_str = _("unknown")
if conf > 0:
try:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
except Exception:
time_str = _("error")
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 tx_hash:
label, is_default_label = self.app.wallet.get_label(tx_hash)
else:
label = _('Pruned transaction outputs')
is_default_label = False
quote_currency = 'USD'
rate = self.get_history_rate(value, timestamp)
quote_text = "..." if rate is None else "{0:.3} {1}".format(rate, quote_currency)
yield (conf, icon, time_str, label, value, tx_hash, quote_text)
def update(self, see_all=False):
if self.app.wallet is None:
return
history_card = self.screen.ids.recent_activity_card
history = self.parse_history(reversed(
self.app.wallet.get_history(self.app.current_account)))
# 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
count = 0
for item in history:
count += 1
conf, icon, date_time, address, value, tx, quote_text = item
ri = RecentActivityItem()
ri.icon = icon
ri.date = date_time
ri.address = address
ri.value = value
ri.quote_text = quote_text
ri.confirmations = conf
ri.tx_hash = tx
history_add(ri)
if count == 8 and not see_all:
break
class ScreenAddress(CScreen):
'''This is the dialog that shows a carousel of the currently available
addresses.
'''
labels = DictProperty({})
''' A precached list of address labels.
'''
tab = ObjectProperty(None)
''' The tab associated With this Carousel
'''
class ScreenPassword(Factory.Screen):
__events__ = ('on_release', 'on_deactivate', 'on_activate')
def on_activate(self):
app = App.get_running_app()
action_bar = app.root.main_screen.ids.action_bar
action_bar.add_widget(self._action_view)
def on_deactivate(self):
self.ids.password.text = ''
def on_release(self, *args):
pass
class SendScreen(CScreen):
kvname = 'send'
def set_URI(self, uri):
print "set uri", uri
self.screen.address = uri.get('address', '')
self.screen.message = uri.get('message', '')
amount = uri.get('amount')
if amount:
amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point()))
self.screen.amount = amount_str + ' ' + self.app.base_unit
def do_clear(self):
self.screen.amount = ''
self.screen.message = ''
self.screen.address = ''
def do_paste(self):
contents = unicode(self.app._clipboard.get())
try:
uri = parse_URI(contents)
except:
self.app.show_info("Invalid URI", contents)
return
self.set_URI(uri)
def do_send(self):
address = str(self.screen.address)
if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
return
try:
amount = self.app.get_amount(self.screen.amount)
except:
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
return
message = unicode(self.screen.message)
fee = None
outputs = [('address', address, amount)]
self.app.protected(self.send_tx, (outputs, fee, message))
def send_tx(self, *args):
self.app.show_info("Sending...")
threading.Thread(target=self.send_tx_thread, args=args).start()
def send_tx_thread(self, outputs, fee, label, password):
# make unsigned transaction
coins = self.app.wallet.get_spendable_coins()
try:
tx = self.app.wallet.make_unsigned_transaction(coins, outputs, self.app.electrum_config, fee)
except Exception as e:
traceback.print_exc(file=sys.stdout)
self.app.show_error(str(e))
return
# sign transaction
try:
self.app.wallet.sign_transaction(tx, password)
except Exception as e:
traceback.print_exc(file=sys.stdout)
self.app.show_error(str(e))
return
# broadcast
ok, txid = self.app.wallet.sendtx(tx)
self.app.show_info(txid)
class ReceiveScreen(CScreen):
kvname = 'receive'
def update(self):
self.screen.address = self.app.wallet.get_unused_address(None)
def amount_callback(self, popup):
amount_label = self.screen.ids.get('amount')
amount_label.text = popup.ids.amount_label.text
self.update_qr()
def get_URI(self):
from electrum.util import create_URI
amount = self.screen.amount
if amount:
a, u = self.screen.amount.split()
assert u == self.app.base_unit
amount = Decimal(a) * pow(10, self.app.decimal_point())
return create_URI(self.screen.address, amount, self.screen.message)
@profiler
def update_qr(self):
uri = self.get_URI()
qr = self.screen.ids.get('qr')
qr.set_data(uri)
def do_copy(self):
uri = self.get_URI()
self.app._clipboard.put(uri, 'text/plain')
def do_clear(self):
self.screen.amount = ''
self.screen.message = ''
class ContactsScreen(CScreen):
kvname = 'contacts'
def add_new_contact(self):
dlg = Cache.get('electrum_widgets', 'NewContactDialog')
if not dlg:
dlg = NewContactDialog()
Cache.append('electrum_widgets', 'NewContactDialog', dlg)
dlg.open()
def update(self):
contact_list = self.screen.ids.contact_container
contact_list.clear_widgets()
child = -1
children = contact_list.children
for key in sorted(self.app.contacts.keys()):
_type, value = self.app.contacts[key]
child += 1
try:
if children[child].label == value:
continue
except IndexError:
pass
ci = Factory.ContactItem()
ci.address = key
ci.label = value
contact_list.add_widget(ci)
class CSpinner(Factory.Spinner):
'''CustomDropDown that allows fading out the dropdown
'''
def _update_dropdown(self, *largs):
dp = self._dropdown
cls = self.option_cls
if isinstance(cls, string_types):
cls = Factory.get(cls)
dp.clear_widgets()
def do_release(option):
Clock.schedule_once(lambda dt: dp.select(option.text), .25)
for value in self.values:
item = cls(text=value)
item.bind(on_release=do_release)
dp.add_widget(item)
class TabbedCarousel(Factory.TabbedPanel):
'''Custom TabbedPanel using a carousel used in the Main Screen
'''
carousel = ObjectProperty(None)
def animate_tab_to_center(self, value):
scrlv = self._tab_strip.parent
if not scrlv:
return
idx = self.tab_list.index(value)
if idx == 0:
scroll_x = 1
elif idx == len(self.tab_list) - 1:
scroll_x = 0
else:
self_center_x = scrlv.center_x
vcenter_x = value.center_x
diff_x = (self_center_x - vcenter_x)
try:
scroll_x = scrlv.scroll_x - (diff_x / scrlv.width)
except ZeroDivisionError:
pass
mation = Factory.Animation(scroll_x=scroll_x, d=.25)
mation.cancel_all(scrlv)
mation.start(scrlv)
def on_current_tab(self, instance, value):
if value.text == 'default_tab':
return
self.animate_tab_to_center(value)
def on_index(self, instance, value):
current_slide = instance.current_slide
if not hasattr(current_slide, 'tab'):
return
tab = current_slide.tab
ct = self.current_tab
try:
if ct.text != tab.text:
carousel = self.carousel
carousel.slides[ct.slide].dispatch('on_leave')
self.switch_to(tab)
carousel.slides[tab.slide].dispatch('on_enter')
except AttributeError:
current_slide.dispatch('on_enter')
def switch_to(self, header):
# we have to replace the functionality of the original switch_to
if not header:
return
if not hasattr(header, 'slide'):
header.content = self.carousel
super(TabbedCarousel, self).switch_to(header)
try:
tab = self.tab_list[-1]
except IndexError:
return
self._current_tab = tab
tab.state = 'down'
return
carousel = self.carousel
self.current_tab.state = "normal"
header.state = 'down'
self._current_tab = header
# set the carousel to load the appropriate slide
# saved in the screen attribute of the tab head
slide = carousel.slides[header.slide]
if carousel.current_slide != slide:
carousel.current_slide.dispatch('on_leave')
carousel.load_slide(slide)
slide.dispatch('on_enter')
def add_widget(self, widget, index=0):
if isinstance(widget, Factory.CScreen):
self.carousel.add_widget(widget)
return
super(TabbedCarousel, self).add_widget(widget, index=index)
class ELTextInput(Factory.TextInput):
'''Custom TextInput used in main screens for numeric entry
'''
def insert_text(self, substring, from_undo=False):
if not from_undo:
if self.input_type == 'numbers':
numeric_list = map(str, range(10))
if '.' not in self.text:
numeric_list.append('.')
if substring not in numeric_list:
return
super(ELTextInput, self).insert_text(substring, from_undo=from_undo)