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.
686 lines
19 KiB
686 lines
19 KiB
from functools import partial
|
|
|
|
from kivy.app import App
|
|
from kivy.factory import Factory
|
|
from kivy.uix.button import Button
|
|
from kivy.uix.bubble import Bubble
|
|
from kivy.uix.popup import Popup
|
|
from kivy.uix.widget import Widget
|
|
from kivy.uix.carousel import Carousel
|
|
from kivy.uix.tabbedpanel import TabbedPanelHeader
|
|
from kivy.properties import (NumericProperty, StringProperty, ListProperty,
|
|
ObjectProperty, AliasProperty, OptionProperty,
|
|
BooleanProperty)
|
|
|
|
from kivy.animation import Animation
|
|
from kivy.core.window import Window
|
|
from kivy.clock import Clock
|
|
from kivy.lang import Builder
|
|
from kivy.metrics import dp, inch
|
|
|
|
#from electrum.bitcoin import is_valid
|
|
from electrum.i18n import _
|
|
|
|
# Delayed inits
|
|
QRScanner = None
|
|
NFCSCanner = None
|
|
ScreenAddress = None
|
|
decode_uri = None
|
|
|
|
DEFAULT_PATH = '/tmp/'
|
|
app = App.get_running_app()
|
|
|
|
class CarouselHeader(TabbedPanelHeader):
|
|
|
|
slide = NumericProperty(0)
|
|
''' indicates the link to carousels slide'''
|
|
|
|
class AnimatedPopup(Popup):
|
|
|
|
def open(self):
|
|
self.opacity = 0
|
|
super(AnimatedPopup, self).open()
|
|
anim = Animation(opacity=1, d=.5).start(self)
|
|
|
|
def dismiss(self):
|
|
def on_complete(*l):
|
|
super(AnimatedPopup, self).dismiss()
|
|
anim = Animation(opacity=0, d=.5)
|
|
anim.bind(on_complete=on_complete)
|
|
anim.start(self)
|
|
|
|
|
|
class CarouselDialog(AnimatedPopup):
|
|
''' A Popup dialog with a CarouselIndicator used as the content.
|
|
'''
|
|
|
|
carousel_content = ObjectProperty(None)
|
|
|
|
def open(self):
|
|
self.opacity = 0
|
|
super(CarouselDialog, self).open()
|
|
anim = Animation(opacity=1, d=.5).start(self)
|
|
|
|
def dismiss(self):
|
|
def on_complete(*l):
|
|
super(CarouselDialog, self).dismiss()
|
|
anim = Animation(opacity=0, d=.5)
|
|
anim.bind(on_complete=on_complete)
|
|
anim.start(self)
|
|
|
|
def add_widget(self, widget, index=0):
|
|
if isinstance(widget, Carousel):
|
|
super(CarouselDialog, self).add_widget(widget, index)
|
|
return
|
|
if 'carousel_content' not in self.ids.keys():
|
|
super(CarouselDialog, self).add_widget(widget)
|
|
return
|
|
self.carousel_content.add_widget(widget, index)
|
|
|
|
|
|
|
|
class NFCTransactionDialog(AnimatedPopup):
|
|
|
|
mode = OptionProperty('send', options=('send','receive'))
|
|
|
|
scanner = ObjectProperty(None)
|
|
|
|
def __init__(self, **kwargs):
|
|
# Delayed Init
|
|
global NFCSCanner
|
|
if NFCSCanner is None:
|
|
from electrum_gui.kivy.nfc_scanner import NFCScanner
|
|
self.scanner = NFCSCanner
|
|
|
|
super(NFCTransactionDialog, self).__init__(**kwargs)
|
|
self.scanner.nfc_init()
|
|
self.scanner.bind()
|
|
|
|
def on_parent(self, instance, value):
|
|
sctr = self.ids.sctr
|
|
if value:
|
|
def _cmp(*l):
|
|
anim = Animation(rotation=2, scale=1, opacity=1)
|
|
anim.start(sctr)
|
|
anim.bind(on_complete=_start)
|
|
|
|
def _start(*l):
|
|
anim = Animation(rotation=350, scale=2, opacity=0)
|
|
anim.start(sctr)
|
|
anim.bind(on_complete=_cmp)
|
|
_start()
|
|
return
|
|
Animation.cancel_all(sctr)
|
|
|
|
|
|
class InfoBubble(Bubble):
|
|
'''Bubble to be used to display short Help Information'''
|
|
|
|
message = StringProperty(_('Nothing set !'))
|
|
'''Message to be displayed; defaults to "nothing set"'''
|
|
|
|
icon = StringProperty('')
|
|
''' Icon to be displayed along with the message defaults to ''
|
|
|
|
:attr:`icon` is a `StringProperty` defaults to `''`
|
|
'''
|
|
|
|
fs = BooleanProperty(False)
|
|
''' Show Bubble in half screen mode
|
|
|
|
:attr:`fs` is a `BooleanProperty` defaults to `False`
|
|
'''
|
|
|
|
modal = BooleanProperty(False)
|
|
''' Allow bubble to be hidden on touch.
|
|
|
|
:attr:`modal` is a `BooleanProperty` defauult to `False`.
|
|
'''
|
|
|
|
exit = BooleanProperty(False)
|
|
'''Indicates whether to exit app after bubble is closed.
|
|
|
|
:attr:`exit` is a `BooleanProperty` defaults to False.
|
|
'''
|
|
|
|
dim_background = BooleanProperty(False)
|
|
''' Indicates Whether to draw a background on the windows behind the bubble.
|
|
|
|
:attr:`dim` is a `BooleanProperty` defaults to `False`.
|
|
'''
|
|
|
|
def on_touch_down(self, touch):
|
|
if self.modal:
|
|
return True
|
|
self.hide()
|
|
if self.collide_point(*touch.pos):
|
|
return True
|
|
|
|
def show(self, pos, duration, width=None, modal=False, exit=False):
|
|
'''Animate the bubble into position'''
|
|
self.modal, self.exit = modal, exit
|
|
if width:
|
|
self.width = width
|
|
if self.modal:
|
|
from kivy.uix.modalview import ModalView
|
|
self._modal_view = m = ModalView()
|
|
Window.add_widget(m)
|
|
m.add_widget(self)
|
|
else:
|
|
Window.add_widget(self)
|
|
# wait for the bubble to adjust it's size according to text then animate
|
|
Clock.schedule_once(lambda dt: self._show(pos, duration))
|
|
|
|
def _show(self, pos, duration):
|
|
|
|
def on_stop(*l):
|
|
if duration:
|
|
Clock.schedule_once(self.hide, duration + .5)
|
|
|
|
self.opacity = 0
|
|
arrow_pos = self.arrow_pos
|
|
if arrow_pos[0] in ('l', 'r'):
|
|
pos = pos[0], pos[1] - (self.height/2)
|
|
else:
|
|
pos = pos[0] - (self.width/2), pos[1]
|
|
|
|
self.limit_to = Window
|
|
|
|
anim = Animation(opacity=1, pos=pos, d=.32)
|
|
anim.bind(on_complete=on_stop)
|
|
anim.cancel_all(self)
|
|
anim.start(self)
|
|
|
|
|
|
def hide(self, now=False):
|
|
''' Auto fade out the Bubble
|
|
'''
|
|
def on_stop(*l):
|
|
if self.modal:
|
|
m = self._modal_view
|
|
m.remove_widget(self)
|
|
Window.remove_widget(m)
|
|
Window.remove_widget(self)
|
|
if self.exit:
|
|
App.get_running_app().stop()
|
|
import sys
|
|
sys.exit()
|
|
if now:
|
|
return on_stop()
|
|
|
|
anim = Animation(opacity=0, d=.25)
|
|
anim.bind(on_complete=on_stop)
|
|
anim.cancel_all(self)
|
|
anim.start(self)
|
|
|
|
|
|
class InfoContent(Widget):
|
|
'''Abstract class to be used to add to content to InfoDialog'''
|
|
pass
|
|
|
|
|
|
class InfoButton(Button):
|
|
'''Button that is auto added to the dialog when setting `buttons:`
|
|
property.
|
|
'''
|
|
pass
|
|
|
|
|
|
class EventsDialog(AnimatedPopup):
|
|
''' Abstract Popup that provides the following events
|
|
.. events::
|
|
`on_release`
|
|
`on_press`
|
|
'''
|
|
|
|
__events__ = ('on_release', 'on_press')
|
|
|
|
def __init__(self, **kwargs):
|
|
super(EventsDialog, self).__init__(**kwargs)
|
|
self._on_release = kwargs.get('on_release')
|
|
Window.bind(size=self.on_size,
|
|
rotation=self.on_size)
|
|
self.on_size(Window, Window.size)
|
|
|
|
def on_size(self, instance, value):
|
|
if app.ui_mode[0] == 'p':
|
|
self.size = Window.size
|
|
else:
|
|
#tablet
|
|
if app.orientation[0] == 'p':
|
|
#portrait
|
|
self.size = Window.size[0]/1.67, Window.size[1]/1.4
|
|
else:
|
|
self.size = Window.size[0]/2.5, Window.size[1]
|
|
|
|
def on_release(self, instance):
|
|
pass
|
|
|
|
def on_press(self, instance):
|
|
pass
|
|
|
|
def close(self):
|
|
self._on_release = None
|
|
self.dismiss()
|
|
|
|
|
|
class InfoDialog(EventsDialog):
|
|
''' A dialog box meant to display info along with buttons at the bottom
|
|
|
|
'''
|
|
|
|
buttons = ListProperty([_('ok'), _('cancel')])
|
|
'''List of Buttons to be displayed at the bottom'''
|
|
|
|
def __init__(self, **kwargs):
|
|
self._old_buttons = self.buttons
|
|
super(InfoDialog, self).__init__(**kwargs)
|
|
self.on_buttons(self, self.buttons)
|
|
|
|
def on_buttons(self, instance, value):
|
|
if 'buttons_layout' not in self.ids.keys():
|
|
return
|
|
if value == self._old_buttons:
|
|
return
|
|
blayout = self.ids.buttons_layout
|
|
blayout.clear_widgets()
|
|
for btn in value:
|
|
ib = InfoButton(text=btn)
|
|
ib.bind(on_press=partial(self.dispatch, 'on_press'))
|
|
ib.bind(on_release=partial(self.dispatch, 'on_release'))
|
|
blayout.add_widget(ib)
|
|
self._old_buttons = value
|
|
pass
|
|
|
|
def add_widget(self, widget, index=0):
|
|
if isinstance(widget, InfoContent):
|
|
self.ids.info_content.add_widget(widget, index=index)
|
|
else:
|
|
super(InfoDialog, self).add_widget(widget)
|
|
|
|
|
|
class TakeInputDialog(InfoDialog):
|
|
''' A simple Dialog for displaying a message and taking a input
|
|
using a Textinput
|
|
'''
|
|
|
|
text = StringProperty('Nothing set yet')
|
|
|
|
readonly = BooleanProperty(False)
|
|
|
|
|
|
class EditLabelDialog(TakeInputDialog):
|
|
pass
|
|
|
|
|
|
|
|
class ImportPrivateKeysDialog(TakeInputDialog):
|
|
pass
|
|
|
|
|
|
|
|
class ShowMasterPublicKeyDialog(TakeInputDialog):
|
|
pass
|
|
|
|
|
|
class EditDescriptionDialog(TakeInputDialog):
|
|
|
|
pass
|
|
|
|
|
|
class PrivateKeyDialog(InfoDialog):
|
|
|
|
private_key = StringProperty('')
|
|
''' private key to be displayed in the TextInput
|
|
'''
|
|
|
|
address = StringProperty('')
|
|
''' address to be displayed in the dialog
|
|
'''
|
|
|
|
|
|
class SignVerifyDialog(InfoDialog):
|
|
|
|
address = StringProperty('')
|
|
'''current address being verified'''
|
|
|
|
|
|
|
|
class MessageBox(InfoDialog):
|
|
|
|
image = StringProperty('atlas://gui/kivy/theming/light/info')
|
|
'''path to image to be displayed on the left'''
|
|
|
|
message = StringProperty('Empty Message')
|
|
'''Message to be displayed on the dialog'''
|
|
|
|
def __init__(self, **kwargs):
|
|
super(MessageBox, self).__init__(**kwargs)
|
|
self.title = kwargs.get('title', _('Message'))
|
|
|
|
|
|
class MessageBoxExit(MessageBox):
|
|
|
|
def __init__(self, **kwargs):
|
|
super(MessageBox, self).__init__(**kwargs)
|
|
self.title = kwargs.get('title', _('Exiting'))
|
|
|
|
class MessageBoxError(MessageBox):
|
|
|
|
def __init__(self, **kwargs):
|
|
super(MessageBox, self).__init__(**kwargs)
|
|
self.title = kwargs.get('title', _('Error'))
|
|
|
|
|
|
class WalletAddressesDialog(CarouselDialog):
|
|
|
|
def __init__(self, **kwargs):
|
|
super(WalletAddressesDialog, self).__init__(**kwargs)
|
|
CarouselHeader = Factory.CarouselHeader
|
|
ch = CarouselHeader()
|
|
ch.slide = 0 # idx
|
|
|
|
# delayed init
|
|
global ScreenAddress
|
|
if not ScreenAddress:
|
|
from electrum_gui.kivy.screens import ScreenAddress
|
|
slide = ScreenAddress()
|
|
|
|
slide.tab=ch
|
|
|
|
labels = app.wallet.labels
|
|
addresses = app.wallet.addresses()
|
|
_labels = {}
|
|
for address in addresses:
|
|
_labels[labels.get(address, address)] = address
|
|
|
|
slide.labels = _labels
|
|
|
|
self.add_widget(slide)
|
|
self.add_widget(ch)
|
|
Clock.schedule_once(lambda dt: self.delayed_init(slide))
|
|
|
|
def delayed_init(self, slide):
|
|
# add a tab for each wallet
|
|
# for wallet in wallets
|
|
slide.ids.btn_address.values = values = slide.labels.keys()
|
|
slide.ids.btn_address.text = values[0]
|
|
|
|
|
|
|
|
class RecentActivityDialog(CarouselDialog):
|
|
|
|
def send_payment(self, address):
|
|
tabs = app.root.main_screen.ids.tabs
|
|
screen_send = tabs.ids.screen_send
|
|
# remove self
|
|
self.dismiss()
|
|
# switch_to the send screen
|
|
tabs.ids.panel.switch_to(tabs.ids.tab_send)
|
|
# populate
|
|
screen_send.ids.payto_e.text = address
|
|
|
|
def populate_inputs_outputs(self, app, tx_hash):
|
|
if tx_hash:
|
|
tx = app.wallet.transactions.get(tx_hash)
|
|
self.ids.list_outputs.content_adapter.data = \
|
|
[(address, app.gui.main_gui.format_amount(value))\
|
|
for address, value in tx.outputs]
|
|
self.ids.list_inputs.content_adapter.data = \
|
|
[(input['address'], input['prevout_hash'])\
|
|
for input in tx.inputs]
|
|
|
|
|
|
class CreateAccountDialog(EventsDialog):
|
|
''' Abstract dialog to be used as the base for all Create Account Dialogs
|
|
'''
|
|
crcontent = ObjectProperty(None)
|
|
|
|
def add_widget(self, widget, index=0):
|
|
if not self.crcontent:
|
|
super(CreateAccountDialog, self).add_widget(widget)
|
|
else:
|
|
self.crcontent.add_widget(widget, index=index)
|
|
|
|
|
|
class CreateRestoreDialog(CreateAccountDialog):
|
|
''' Initial Dialog for creating or restoring seed'''
|
|
|
|
def on_parent(self, instance, value):
|
|
if value:
|
|
self.ids.but_close.disabled = True
|
|
self.ids.but_close.opacity = 0
|
|
self._back = _back = partial(app.dispatch, 'on_back')
|
|
app.navigation_higherarchy.append(_back)
|
|
|
|
def close(self):
|
|
if self._back in app.navigation_higherarchy:
|
|
app.navigation_higherarchy.pop()
|
|
self._back = None
|
|
super(CreateRestoreDialog, self).close()
|
|
|
|
|
|
class InitSeedDialog(CreateAccountDialog):
|
|
|
|
seed_msg = StringProperty('')
|
|
'''Text to be displayed in the TextInput'''
|
|
|
|
message = StringProperty('')
|
|
'''Message to be displayed under seed'''
|
|
|
|
seed = ObjectProperty(None)
|
|
|
|
def on_parent(self, instance, value):
|
|
if value:
|
|
stepper = self.ids.stepper
|
|
stepper.opacity = 1
|
|
stepper.source = 'atlas://gui/kivy/theming/light/stepper_full'
|
|
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
|
|
app.navigation_higherarchy.append(_back)
|
|
|
|
def close(self):
|
|
if self._back in app.navigation_higherarchy:
|
|
app.navigation_higherarchy.pop()
|
|
self._back = None
|
|
super(InitSeedDialog, self).close()
|
|
|
|
class VerifySeedDialog(CreateAccountDialog):
|
|
|
|
pass
|
|
|
|
class RestoreSeedDialog(CreateAccountDialog):
|
|
|
|
def on_parent(self, instance, value):
|
|
if value:
|
|
tis = self.ids.text_input_seed
|
|
tis.focus = True
|
|
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')
|
|
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
|
|
app.navigation_higherarchy.append(_back)
|
|
|
|
def on_key_down(self, keyboard, keycode, key, modifiers):
|
|
if keycode[0] in (13, 271):
|
|
self.on_enter()
|
|
return True
|
|
#super
|
|
|
|
def on_enter(self):
|
|
#self._remove_keyboard()
|
|
# press next
|
|
self.ids.next.dispatch('on_release')
|
|
|
|
def _remove_keyboard(self):
|
|
tis = self.ids.text_input_seed
|
|
if tis._keyboard:
|
|
tis._keyboard.unbind(on_key_down=self.on_key_down)
|
|
tis.focus = False
|
|
|
|
def close(self):
|
|
self._remove_keyboard()
|
|
if self._back in app.navigation_higherarchy:
|
|
app.navigation_higherarchy.pop()
|
|
self._back = None
|
|
super(RestoreSeedDialog, self).close()
|
|
|
|
class NewContactDialog(Popup):
|
|
|
|
qrscr = ObjectProperty(None)
|
|
_decoder = None
|
|
|
|
def load_qr_scanner(self):
|
|
global QRScanner
|
|
if not QRScanner:
|
|
from electrum_gui.kivy.qr_scanner import QRScanner
|
|
qrscr = self.qrscr
|
|
if not qrscr:
|
|
self.qrscr = qrscr = QRScanner(opacity=0)
|
|
#pos=self.pos, size=self.size)
|
|
#self.bind(pos=qrscr.setter('pos'),
|
|
# size=qrscr.setter('size')
|
|
qrscr.bind(symbols=self.on_symbols)
|
|
bl = self.ids.bl
|
|
bl.clear_widgets()
|
|
bl.add_widget(qrscr)
|
|
qrscr.opacity = 1
|
|
Animation(height=dp(280)).start(self)
|
|
Animation(opacity=1).start(self)
|
|
qrscr.start()
|
|
|
|
def on_symbols(self, instance, value):
|
|
instance.stop()
|
|
self.remove_widget(instance)
|
|
self.ids.but_contact.dispatch('on_release')
|
|
global decode_uri
|
|
if not decode_uri:
|
|
from electrum_gui.kivy.qr_scanner import decode_uri
|
|
uri = decode_uri(value[0].data)
|
|
self.ids.ti.text = uri.get('address', 'empty')
|
|
self.ids.ti_lbl.text = uri.get('label', 'empty')
|
|
self.ids.ti_lbl.focus = True
|
|
|
|
|
|
class PasswordRequiredDialog(InfoDialog):
|
|
|
|
pass
|
|
|
|
|
|
class ChangePasswordDialog(CreateAccountDialog):
|
|
|
|
message = StringProperty(_('Empty Message'))
|
|
'''Message to be displayed.'''
|
|
|
|
mode = OptionProperty('new',
|
|
options=('new', 'confirm', 'create', 'restore'))
|
|
''' Defines the mode of the password dialog.'''
|
|
|
|
def validate_new_password(self):
|
|
self.ids.next.dispatch('on_release')
|
|
|
|
def on_parent(self, instance, value):
|
|
if value:
|
|
stepper = self.ids.stepper
|
|
stepper.opacity = 1
|
|
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)
|
|
|
|
def close(self):
|
|
ids = self.ids
|
|
ids.ti_wallet_name.text = ""
|
|
ids.ti_wallet_name.focus = False
|
|
ids.ti_password.text = ""
|
|
ids.ti_password.focus = False
|
|
ids.ti_new_password.text = ""
|
|
ids.ti_new_password.focus = False
|
|
ids.ti_confirm_password.text = ""
|
|
ids.ti_confirm_password.focus = False
|
|
if self._back in app.navigation_higherarchy:
|
|
app.navigation_higherarchy.pop()
|
|
self._back = None
|
|
super(ChangePasswordDialog, self).close()
|
|
|
|
|
|
|
|
class Dialog(Popup):
|
|
|
|
content_padding = NumericProperty('2dp')
|
|
'''Padding for the content area of the dialog defaults to 2dp
|
|
'''
|
|
|
|
buttons_padding = NumericProperty('2dp')
|
|
'''Padding for the bottns area of the dialog defaults to 2dp
|
|
'''
|
|
|
|
buttons_height = NumericProperty('40dp')
|
|
'''Height to be used for the Buttons at the bottom
|
|
'''
|
|
|
|
def close(self):
|
|
self.dismiss()
|
|
|
|
def add_content(self, widget, index=0):
|
|
self.ids.layout_content.add_widget(widget, index)
|
|
|
|
def add_button(self, widget, index=0):
|
|
self.ids.layout_buttons.add_widget(widget, index)
|
|
|
|
|
|
class SaveDialog(Popup):
|
|
|
|
filename = StringProperty('')
|
|
'''The default file name provided
|
|
'''
|
|
|
|
filters = ListProperty([])
|
|
''' list of files to be filtered and displayed defaults to allow all
|
|
'''
|
|
|
|
path = StringProperty(DEFAULT_PATH)
|
|
'''path to be loaded by default in this dialog
|
|
'''
|
|
|
|
file_chooser = ObjectProperty(None)
|
|
'''link to the file chooser object inside the dialog
|
|
'''
|
|
|
|
text_input = ObjectProperty(None)
|
|
'''
|
|
'''
|
|
|
|
cancel_button = ObjectProperty(None)
|
|
'''
|
|
'''
|
|
|
|
save_button = ObjectProperty(None)
|
|
'''
|
|
'''
|
|
|
|
def close(self):
|
|
self.dismiss()
|
|
|
|
|
|
class LoadDialog(SaveDialog):
|
|
|
|
def _get_load_btn(self):
|
|
return self.save_button
|
|
|
|
load_button = AliasProperty(_get_load_btn, None, bind=('save_button', ))
|
|
'''Alias to the Save Button to be used as LoadButton
|
|
'''
|
|
|
|
def __init__(self, **kwargs):
|
|
super(LoadDialog, self).__init__(**kwargs)
|
|
self.load_button.text=_("Load")
|
|
|