kivy: separate base wizard class from gui

This commit is contained in:
ThomasV 2016-06-16 19:25:44 +02:00
parent 362e4be6f0
commit 9182392b55
5 changed files with 850 additions and 829 deletions

View File

@ -67,7 +67,6 @@ class EventsDialog(Factory.Popup):
def __init__(self, **kwargs):
super(EventsDialog, self).__init__(**kwargs)
self._on_release = kwargs.get('on_release')
def on_release(self, instance):
pass
@ -76,7 +75,6 @@ class EventsDialog(Factory.Popup):
pass
def close(self):
self._on_release = None
self.dismiss()

View File

@ -1,623 +0,0 @@
from functools import partial
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty, OptionProperty
from kivy.core.window import Window
from kivy.uix.button import Button
from kivy.utils import platform
from electrum_gui.kivy.uix.dialogs import EventsDialog
from electrum_gui.kivy.i18n import _
test_xpub = "xpub661MyMwAqRbcFpV2JqonBDKdgJiExpxiSAtvphtpviunv42FNVJNNRA3Zdy5kQXoK7NpwUC2QQPXVMLKLoHxaekNfemFs5zkfrNnk91dobZ"
test_seed = "powder idea leader task pretty harsh resemble alert quit athlete clerk almost able"
is_test = platform != 'android'
Builder.load_string('''
#:import Window kivy.core.window.Window
#:import _ electrum_gui.kivy.i18n._
<WizardTextInput@TextInput>
border: 4, 4, 4, 4
font_size: '15sp'
padding: '15dp', '15dp'
background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
hint_text_color: self.foreground_color
background_active: 'atlas://gui/kivy/theming/light/create_act_text_active'
background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active'
size_hint_y: None
height: '48sp'
<WizardButton@Button>:
root: None
size_hint: 1, None
height: '48sp'
on_press: if self.root: self.root.dispatch('on_press', self)
on_release: if self.root: self.root.dispatch('on_release', self)
<BigLabel@Label>
color: .854, .925, .984, 1
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
bold: True
<-WizardDialog>
text_color: .854, .925, .984, 1
value: ''
#auto_dismiss: False
size_hint: None, None
canvas.before:
Color:
rgba: 0, 0, 0, .9
Rectangle:
size: Window.size
Color:
rgba: .239, .588, .882, 1
Rectangle:
size: Window.size
crcontent: crcontent
# add electrum icon
BoxLayout:
orientation: 'vertical' if self.width < self.height else 'horizontal'
padding:
min(dp(27), self.width/32), min(dp(27), self.height/32),\
min(dp(27), self.width/32), min(dp(27), self.height/32)
spacing: '10dp'
GridLayout:
id: grid_logo
cols: 1
pos_hint: {'center_y': .5}
size_hint: 1, None
height: self.minimum_height
Label:
color: root.text_color
text: 'ELECTRUM'
size_hint: 1, None
height: self.texture_size[1] if self.opacity else 0
font_size: '33sp'
font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf'
GridLayout:
cols: 1
id: crcontent
spacing: '1dp'
Widget:
size_hint: 1, 0.3
GridLayout:
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: next
text: _('Next')
root: root
disabled: root.value == ''
<WizardMultisigDialog>
value: 'next'
Widget
size_hint: 1, 1
Label:
color: root.text_color
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
text: _("Choose the number of signatures needed to unlock funds in your wallet")
Widget
size_hint: 1, 1
GridLayout:
orientation: 'vertical'
cols: 2
spacing: '14dp'
size_hint: 1, 1
height: self.minimum_height
Label:
color: root.text_color
text: _('From %d cosigners')%n.value
Slider:
id: n
range: 2, 5
step: 1
value: 2
Label:
color: root.text_color
text: _('Require %d signatures')%m.value
Slider:
id: m
range: 1, n.value
step: 1
value: 2
<WizardChoiceDialog>
msg : ''
Widget:
size_hint: 1, 1
Label:
color: root.text_color
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
text: root.msg
Widget
size_hint: 1, 1
GridLayout:
row_default_height: '48dp'
orientation: 'vertical'
id: choices
cols: 1
spacing: '14dp'
size_hint: 1, None
<MButton@Button>:
size_hint: 1, None
height: '33dp'
on_release:
self.parent.update_amount(self.text)
<WordButton@Button>:
size_hint: None, None
padding: '5dp', '5dp'
text_size: None, self.height
width: self.texture_size[0]
height: '30dp'
on_release:
self.parent.new_word(self.text)
<SeedButton@Button>:
height: dp(100)
border: 4, 4, 4, 4
halign: 'justify'
valign: 'top'
font_size: '18dp'
text_size: self.width - dp(24), self.height - dp(12)
color: .1, .1, .1, 1
background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top'
background_down: self.background_normal
size_hint_y: None
<SeedLabel@Label>:
font_size: '12sp'
text_size: self.width, None
size_hint: 1, None
height: self.texture_size[1]
halign: 'justify'
valign: 'middle'
border: 4, 4, 4, 4
<RestoreSeedDialog>
word: ''
BigLabel:
text: "ENTER YOUR SEED PHRASE"
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input_seed
text: ''
on_text: Clock.schedule_once(root.on_text)
SeedLabel:
text: root.message
BoxLayout:
id: suggestions
height: '35dp'
size_hint: 1, None
new_word: root.on_word
BoxLayout:
id: line1
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
MButton:
text: 'Q'
MButton:
text: 'W'
MButton:
text: 'E'
MButton:
text: 'R'
MButton:
text: 'T'
MButton:
text: 'Y'
MButton:
text: 'U'
MButton:
text: 'I'
MButton:
text: 'O'
MButton:
text: 'P'
BoxLayout:
id: line2
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
Widget:
size_hint: 0.5, None
height: '33dp'
MButton:
text: 'A'
MButton:
text: 'S'
MButton:
text: 'D'
MButton:
text: 'F'
MButton:
text: 'G'
MButton:
text: 'H'
MButton:
text: 'J'
MButton:
text: 'K'
MButton:
text: 'L'
Widget:
size_hint: 0.5, None
height: '33dp'
BoxLayout:
id: line3
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
Widget:
size_hint: 1, None
MButton:
text: 'Z'
MButton:
text: 'X'
MButton:
text: 'C'
MButton:
text: 'V'
MButton:
text: 'B'
MButton:
text: 'N'
MButton:
text: 'M'
MButton:
text: ' '
MButton:
text: '<'
<AddXpubDialog>
title: ''
message: ''
BigLabel:
text: root.title
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input
text: ''
on_text: Clock.schedule_once(root.check_text)
SeedLabel:
text: root.message
GridLayout
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
IconButton:
id: scan
height: '48sp'
on_release: root.scan_xpub()
icon: 'atlas://gui/kivy/theming/light/camera'
size_hint: 1, None
WizardButton:
text: _('Paste')
on_release: root.do_paste()
WizardButton:
text: _('Clear')
on_release: root.do_clear()
<ShowXpubDialog>
xpub: ''
message: _('Here is your master public key. Share it with your cosigners.')
BigLabel:
text: "MASTER PUBLIC KEY"
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input
text: root.xpub
SeedLabel:
text: root.message
GridLayout
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
text: _('QR code')
on_release: root.do_qr()
WizardButton:
text: _('Copy')
on_release: root.do_copy()
WizardButton:
text: _('Share')
on_release: root.do_share()
<ShowSeedDialog>
spacing: '12dp'
value: 'next'
BigLabel:
text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
GridLayout:
id: grid
cols: 1
pos_hint: {'center_y': .5}
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: '12dp'
SeedButton:
text: root.seed_text
SeedLabel:
text: root.message
''')
class WizardDialog(EventsDialog):
''' Abstract dialog to be used as the base for all Create Account Dialogs
'''
crcontent = ObjectProperty(None)
def __init__(self, **kwargs):
super(WizardDialog, self).__init__(**kwargs)
self.action = kwargs.get('action')
_trigger_size_dialog = Clock.create_trigger(self._size_dialog)
Window.bind(size=_trigger_size_dialog,
rotation=_trigger_size_dialog)
_trigger_size_dialog()
def _size_dialog(self, dt):
app = App.get_running_app()
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 add_widget(self, widget, index=0):
if not self.crcontent:
super(WizardDialog, self).add_widget(widget)
else:
self.crcontent.add_widget(widget, index=index)
def on_dismiss(self):
app = App.get_running_app()
if app.wallet is None and self._on_release is not None:
app.stop()
class WizardMultisigDialog(WizardDialog):
pass
class WizardChoiceDialog(WizardDialog):
''' Multiple choices dialog '''
def __init__(self, **kwargs):
super(WizardChoiceDialog, self).__init__(**kwargs)
self.msg = kwargs.get('msg', '')
choices = kwargs.get('choices', [])
layout = self.ids.choices
layout.bind(minimum_height=layout.setter('height'))
for text, action in choices:
l = WizardButton(text=text)
l.action = action
l.height = '48dp'
l.root = self
layout.add_widget(l)
def on_parent(self, instance, value):
if value:
app = App.get_running_app()
self._back = _back = partial(app.dispatch, 'on_back')
class ShowSeedDialog(WizardDialog):
seed_text = StringProperty('')
message = StringProperty('')
def on_parent(self, instance, value):
if value:
app = App.get_running_app()
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
class WordButton(Button):
pass
class WizardButton(Button):
pass
class RestoreSeedDialog(WizardDialog):
message = StringProperty('')
def __init__(self, **kwargs):
super(RestoreSeedDialog, self).__init__(**kwargs)
self._test = kwargs['test']
from electrum.mnemonic import Mnemonic
from electrum.old_mnemonic import words as old_wordlist
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
self.ids.text_input_seed.text = test_seed if is_test else ''
def get_suggestions(self, prefix):
for w in self.words:
if w.startswith(prefix):
yield w
def on_text(self, dt):
self.ids.next.disabled = not bool(self._test(self.get_text()))
text = self.ids.text_input_seed.text
if not text:
last_word = ''
elif text[-1] == ' ':
last_word = ''
else:
last_word = text.split(' ')[-1]
enable_space = False
self.ids.suggestions.clear_widgets()
suggestions = [x for x in self.get_suggestions(last_word)]
if suggestions and len(suggestions) < 10:
for w in suggestions:
if w == last_word:
enable_space = True
else:
b = WordButton(text=w)
self.ids.suggestions.add_widget(b)
i = len(last_word)
p = set()
for x in suggestions:
if len(x)>i: p.add(x[i])
for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
for c in line.children:
if isinstance(c, Button):
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
c.disabled = (c.text.lower() not in p) and last_word
elif c.text == ' ':
c.disabled = not enable_space
def on_word(self, w):
text = self.get_text()
words = text.split(' ')
words[-1] = w
text = ' '.join(words)
self.ids.text_input_seed.text = text + ' '
self.ids.suggestions.clear_widgets()
def get_text(self):
ti = self.ids.text_input_seed
text = unicode(ti.text).strip()
text = ' '.join(text.split())
return text
def update_text(self, c):
c = c.lower()
text = self.ids.text_input_seed.text
if c == '<':
text = text[:-1]
else:
text += c
self.ids.text_input_seed.text = text
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)
self._back = _back = partial(self.ids.back.dispatch,
'on_release')
app = App.get_running_app()
def on_key_down(self, keyboard, keycode, key, modifiers):
if keycode[0] in (13, 271):
self.on_enter()
return True
def on_enter(self):
#self._remove_keyboard()
# press next
next = self.ids.next
if not next.disabled:
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
class ShowXpubDialog(WizardDialog):
def __init__(self, **kwargs):
WizardDialog.__init__(self, **kwargs)
self.app = App.get_running_app()
self.xpub = kwargs['xpub']
self.ids.next.disabled = False
def do_copy(self):
self.app._clipboard.copy(self.xpub)
def do_share(self):
self.app.do_share(self.xpub, _("Master Public Key"))
def do_qr(self):
from qr_dialog import QRDialog
popup = QRDialog(_("Master Public Key"), self.xpub, True)
popup.open()
class AddXpubDialog(WizardDialog):
def __init__(self, **kwargs):
WizardDialog.__init__(self, **kwargs)
self.app = App.get_running_app()
self._test = kwargs['test']
self.title = kwargs['title']
self.message = kwargs['message']
def check_text(self, dt):
self.ids.next.disabled = not bool(self._test(self.get_text()))
def get_text(self):
ti = self.ids.text_input
return unicode(ti.text).strip()
def scan_xpub(self):
def on_complete(text):
self.ids.text_input.text = text
self.app.scan_qr(on_complete)
def do_paste(self):
self.ids.text_input.text = test_xpub if is_test else unicode(self.app._clipboard.paste())
def do_clear(self):
self.ids.text_input.text = ''

View File

@ -1,30 +1,675 @@
import os
from electrum.wallet import Wallet, Multisig_Wallet
from electrum_gui.kivy.i18n import _
from functools import partial
import threading
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import ObjectProperty, StringProperty, OptionProperty
from kivy.core.window import Window
from kivy.uix.button import Button
from kivy.utils import platform
from kivy.uix.widget import Widget
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.factory import Factory
import sys
import threading
from functools import partial
import weakref
from create_restore import WizardChoiceDialog, ShowSeedDialog, RestoreSeedDialog, AddXpubDialog, ShowXpubDialog, WizardMultisigDialog
from password_dialog import PasswordDialog
from electrum_gui.kivy.uix.dialogs import EventsDialog
from electrum_gui.kivy.i18n import _
# global Variables
app = App.get_running_app()
from password_dialog import PasswordDialog
class InstallWizard(Widget):
'''Installation Wizard. Responsible for instantiating the
creation/restoration of wallets.
from electrum.base_wizard import BaseWizard
Builder.load_string('''
#:import Window kivy.core.window.Window
#:import _ electrum_gui.kivy.i18n._
<WizardTextInput@TextInput>
border: 4, 4, 4, 4
font_size: '15sp'
padding: '15dp', '15dp'
background_color: (1, 1, 1, 1) if self.focus else (0.454, 0.698, 0.909, 1)
foreground_color: (0.31, 0.31, 0.31, 1) if self.focus else (0.835, 0.909, 0.972, 1)
hint_text_color: self.foreground_color
background_active: 'atlas://gui/kivy/theming/light/create_act_text_active'
background_normal: 'atlas://gui/kivy/theming/light/create_act_text_active'
size_hint_y: None
height: '48sp'
<WizardButton@Button>:
root: None
size_hint: 1, None
height: '48sp'
on_press: if self.root: self.root.dispatch('on_press', self)
on_release: if self.root: self.root.dispatch('on_release', self)
<BigLabel@Label>
color: .854, .925, .984, 1
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
bold: True
<-WizardDialog>
text_color: .854, .925, .984, 1
value: ''
#auto_dismiss: False
size_hint: None, None
canvas.before:
Color:
rgba: 0, 0, 0, .9
Rectangle:
size: Window.size
Color:
rgba: .239, .588, .882, 1
Rectangle:
size: Window.size
crcontent: crcontent
# add electrum icon
BoxLayout:
orientation: 'vertical' if self.width < self.height else 'horizontal'
padding:
min(dp(27), self.width/32), min(dp(27), self.height/32),\
min(dp(27), self.width/32), min(dp(27), self.height/32)
spacing: '10dp'
GridLayout:
id: grid_logo
cols: 1
pos_hint: {'center_y': .5}
size_hint: 1, None
height: self.minimum_height
Label:
color: root.text_color
text: 'ELECTRUM'
size_hint: 1, None
height: self.texture_size[1] if self.opacity else 0
font_size: '33sp'
font_name: 'gui/kivy/data/fonts/tron/Tr2n.ttf'
GridLayout:
cols: 1
id: crcontent
spacing: '1dp'
Widget:
size_hint: 1, 0.3
GridLayout:
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
id: back
text: _('Back')
root: root
WizardButton:
id: next
text: _('Next')
root: root
disabled: root.value == ''
<WizardMultisigDialog>
value: 'next'
Widget
size_hint: 1, 1
Label:
color: root.text_color
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
text: _("Choose the number of signatures needed to unlock funds in your wallet")
Widget
size_hint: 1, 1
GridLayout:
orientation: 'vertical'
cols: 2
spacing: '14dp'
size_hint: 1, 1
height: self.minimum_height
Label:
color: root.text_color
text: _('From %d cosigners')%n.value
Slider:
id: n
range: 2, 5
step: 1
value: 2
Label:
color: root.text_color
text: _('Require %d signatures')%m.value
Slider:
id: m
range: 1, n.value
step: 1
value: 2
<WizardChoiceDialog>
msg : ''
Widget:
size_hint: 1, 1
Label:
color: root.text_color
size_hint: 1, None
text_size: self.width, None
height: self.texture_size[1]
text: root.msg
Widget
size_hint: 1, 1
GridLayout:
row_default_height: '48dp'
orientation: 'vertical'
id: choices
cols: 1
spacing: '14dp'
size_hint: 1, None
<MButton@Button>:
size_hint: 1, None
height: '33dp'
on_release:
self.parent.update_amount(self.text)
<WordButton@Button>:
size_hint: None, None
padding: '5dp', '5dp'
text_size: None, self.height
width: self.texture_size[0]
height: '30dp'
on_release:
self.parent.new_word(self.text)
<SeedButton@Button>:
height: dp(100)
border: 4, 4, 4, 4
halign: 'justify'
valign: 'top'
font_size: '18dp'
text_size: self.width - dp(24), self.height - dp(12)
color: .1, .1, .1, 1
background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top'
background_down: self.background_normal
size_hint_y: None
<SeedLabel@Label>:
font_size: '12sp'
text_size: self.width, None
size_hint: 1, None
height: self.texture_size[1]
halign: 'justify'
valign: 'middle'
border: 4, 4, 4, 4
<RestoreSeedDialog>
word: ''
BigLabel:
text: "ENTER YOUR SEED PHRASE"
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input_seed
text: ''
on_text: Clock.schedule_once(root.on_text)
SeedLabel:
text: root.message
BoxLayout:
id: suggestions
height: '35dp'
size_hint: 1, None
new_word: root.on_word
BoxLayout:
id: line1
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
MButton:
text: 'Q'
MButton:
text: 'W'
MButton:
text: 'E'
MButton:
text: 'R'
MButton:
text: 'T'
MButton:
text: 'Y'
MButton:
text: 'U'
MButton:
text: 'I'
MButton:
text: 'O'
MButton:
text: 'P'
BoxLayout:
id: line2
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
Widget:
size_hint: 0.5, None
height: '33dp'
MButton:
text: 'A'
MButton:
text: 'S'
MButton:
text: 'D'
MButton:
text: 'F'
MButton:
text: 'G'
MButton:
text: 'H'
MButton:
text: 'J'
MButton:
text: 'K'
MButton:
text: 'L'
Widget:
size_hint: 0.5, None
height: '33dp'
BoxLayout:
id: line3
update_amount: root.update_text
size_hint: 1, None
height: '30dp'
Widget:
size_hint: 1, None
MButton:
text: 'Z'
MButton:
text: 'X'
MButton:
text: 'C'
MButton:
text: 'V'
MButton:
text: 'B'
MButton:
text: 'N'
MButton:
text: 'M'
MButton:
text: ' '
MButton:
text: '<'
<AddXpubDialog>
title: ''
message: ''
BigLabel:
text: root.title
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input
text: ''
on_text: Clock.schedule_once(root.check_text)
SeedLabel:
text: root.message
GridLayout
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
IconButton:
id: scan
height: '48sp'
on_release: root.scan_xpub()
icon: 'atlas://gui/kivy/theming/light/camera'
size_hint: 1, None
WizardButton:
text: _('Paste')
on_release: root.do_paste()
WizardButton:
text: _('Clear')
on_release: root.do_clear()
<ShowXpubDialog>
xpub: ''
message: _('Here is your master public key. Share it with your cosigners.')
BigLabel:
text: "MASTER PUBLIC KEY"
GridLayout
cols: 1
padding: 0, '12dp'
orientation: 'vertical'
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
SeedButton:
id: text_input
text: root.xpub
SeedLabel:
text: root.message
GridLayout
rows: 1
spacing: '12dp'
size_hint: 1, None
height: self.minimum_height
WizardButton:
text: _('QR code')
on_release: root.do_qr()
WizardButton:
text: _('Copy')
on_release: root.do_copy()
WizardButton:
text: _('Share')
on_release: root.do_share()
<ShowSeedDialog>
spacing: '12dp'
value: 'next'
BigLabel:
text: "PLEASE WRITE DOWN YOUR SEED PHRASE"
GridLayout:
id: grid
cols: 1
pos_hint: {'center_y': .5}
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: '12dp'
SeedButton:
text: root.seed_text
SeedLabel:
text: root.message
''')
class WizardDialog(EventsDialog):
''' Abstract dialog to be used as the base for all Create Account Dialogs
'''
crcontent = ObjectProperty(None)
def __init__(self, **kwargs):
super(WizardDialog, self).__init__(**kwargs)
#self.action = kwargs.get('action')
self.run_next = kwargs['run_next']
self.run_prev = kwargs['run_prev']
_trigger_size_dialog = Clock.create_trigger(self._size_dialog)
Window.bind(size=_trigger_size_dialog,
rotation=_trigger_size_dialog)
_trigger_size_dialog()
self._on_release = False
def _size_dialog(self, dt):
app = App.get_running_app()
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 add_widget(self, widget, index=0):
if not self.crcontent:
super(WizardDialog, self).add_widget(widget)
else:
self.crcontent.add_widget(widget, index=index)
def on_dismiss(self):
app = App.get_running_app()
if app.wallet is None and not self._on_release:
app.stop()
def get_params(self, button):
return ()
def on_release(self, button):
self._on_release = True
self.close()
if not button:
self.parent.dispatch('on_wizard_complete', None)
return
if button is self.ids.back:
self.run_prev()
return
params = self.get_params(button)
self.run_next(*params)
class WizardMultisigDialog(WizardDialog):
def get_params(self, button):
m = self.ids.m.value
n = self.ids.n.value
return m, n
class WizardChoiceDialog(WizardDialog):
def __init__(self, **kwargs):
super(WizardChoiceDialog, self).__init__(**kwargs)
self.msg = kwargs.get('msg', '')
choices = kwargs.get('choices', [])
layout = self.ids.choices
layout.bind(minimum_height=layout.setter('height'))
for text, action in choices:
l = WizardButton(text=text)
l.action = action
l.height = '48dp'
l.root = self
layout.add_widget(l)
def on_parent(self, instance, value):
if value:
app = App.get_running_app()
self._back = _back = partial(app.dispatch, 'on_back')
def get_params(self, button):
return (button.action,)
class ShowSeedDialog(WizardDialog):
seed_text = StringProperty('')
message = StringProperty('')
def on_parent(self, instance, value):
if value:
app = App.get_running_app()
self._back = _back = partial(self.ids.back.dispatch, 'on_release')
def get_params(self, b):
return(self.seed_text,)
class WordButton(Button):
pass
class WizardButton(Button):
pass
class RestoreSeedDialog(WizardDialog):
message = StringProperty('')
def __init__(self, **kwargs):
super(RestoreSeedDialog, self).__init__(**kwargs)
self._test = kwargs['test']
from electrum.mnemonic import Mnemonic
from electrum.old_mnemonic import words as old_wordlist
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
self.ids.text_input_seed.text = ''
def get_suggestions(self, prefix):
for w in self.words:
if w.startswith(prefix):
yield w
def on_text(self, dt):
self.ids.next.disabled = not bool(self._test(self.get_text()))
text = self.ids.text_input_seed.text
if not text:
last_word = ''
elif text[-1] == ' ':
last_word = ''
else:
last_word = text.split(' ')[-1]
enable_space = False
self.ids.suggestions.clear_widgets()
suggestions = [x for x in self.get_suggestions(last_word)]
if suggestions and len(suggestions) < 10:
for w in suggestions:
if w == last_word:
enable_space = True
else:
b = WordButton(text=w)
self.ids.suggestions.add_widget(b)
i = len(last_word)
p = set()
for x in suggestions:
if len(x)>i: p.add(x[i])
for line in [self.ids.line1, self.ids.line2, self.ids.line3]:
for c in line.children:
if isinstance(c, Button):
if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
c.disabled = (c.text.lower() not in p) and last_word
elif c.text == ' ':
c.disabled = not enable_space
def on_word(self, w):
text = self.get_text()
words = text.split(' ')
words[-1] = w
text = ' '.join(words)
self.ids.text_input_seed.text = text + ' '
self.ids.suggestions.clear_widgets()
def get_text(self):
ti = self.ids.text_input_seed
text = unicode(ti.text).strip()
text = ' '.join(text.split())
return text
def get_params(self, b):
return (self.get_text(),)
def update_text(self, c):
c = c.lower()
text = self.ids.text_input_seed.text
if c == '<':
text = text[:-1]
else:
text += c
self.ids.text_input_seed.text = text
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)
self._back = _back = partial(self.ids.back.dispatch,
'on_release')
app = App.get_running_app()
def on_key_down(self, keyboard, keycode, key, modifiers):
if keycode[0] in (13, 271):
self.on_enter()
return True
def on_enter(self):
#self._remove_keyboard()
# press next
next = self.ids.next
if not next.disabled:
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
class ShowXpubDialog(WizardDialog):
def __init__(self, **kwargs):
WizardDialog.__init__(self, **kwargs)
self.app = App.get_running_app()
self.xpub = kwargs['xpub']
self.ids.next.disabled = False
def do_copy(self):
self.app._clipboard.copy(self.xpub)
def do_share(self):
self.app.do_share(self.xpub, _("Master Public Key"))
def do_qr(self):
from qr_dialog import QRDialog
popup = QRDialog(_("Master Public Key"), self.xpub, True)
popup.open()
class AddXpubDialog(WizardDialog):
def __init__(self, **kwargs):
WizardDialog.__init__(self, **kwargs)
self.app = App.get_running_app()
self._test = kwargs['test']
self.title = kwargs['title']
self.message = kwargs['message']
def check_text(self, dt):
self.ids.next.disabled = not bool(self._test(self.get_text()))
def get_text(self):
ti = self.ids.text_input
return unicode(ti.text).strip()
def get_params(self, button):
return (self.get_text(),)
def scan_xpub(self):
def on_complete(text):
self.ids.text_input.text = text
self.app.scan_qr(on_complete)
def do_paste(self):
self.ids.text_input.text = unicode(self.app._clipboard.paste())
def do_clear(self):
self.ids.text_input.text = ''
class InstallWizard(BaseWizard, Widget):
'''
events::
`on_wizard_complete` Fired when the wizard is done creating/ restoring
wallet/s.
@ -32,12 +677,9 @@ class InstallWizard(Widget):
__events__ = ('on_wizard_complete', )
def __init__(self, config, network, storage):
super(InstallWizard, self).__init__()
self.config = config
self.network = network
self.storage = storage
self.wallet = None
def on_wizard_complete(self, wallet):
"""overriden by main_window"""
pass
def waiting_dialog(self, task, msg, on_complete=None):
'''Perform a blocking task in the background by running the passed
@ -60,189 +702,17 @@ class InstallWizard(Widget):
t = threading.Thread(target = target)
t.start()
def run(self, action, *args):
'''Entry point of our Installation wizard'''
if not action:
return
if hasattr(self, action):
f = getattr(self, action)
apply(f, *args)
else:
raise BaseException("unknown action", action)
def choice_dialog(self, **kwargs): WizardChoiceDialog(**kwargs).open()
def multisig_dialog(self, **kwargs): WizardMultisigDialog(**kwargs).open()
def show_seed_dialog(self, **kwargs): ShowSeedDialog(**kwargs).open()
def restore_seed_dialog(self, **kwargs): RestoreSeedDialog(**kwargs).open()
def add_xpub_dialog(self, **kwargs): AddXpubDialog(**kwargs).open()
def show_xpub_dialog(self, **kwargs): ShowXpubDialog(**kwargs).open()
def on_release(self, dialog, button):
if not button or button.action == 'cancel':
# soft back or escape button pressed
return self.dispatch('on_wizard_complete', None)
action = button.action if self.wallet is None else self.wallet.get_action()
print "action", action
dialog.close()
self.run(action)
def add_seed_or_xpub(self):
msg = ' '.join([
_("Do you want to create a new seed, or to restore a wallet using an existing seed?")
])
choices = [
(_('Create a new seed'), 'create_seed'),
(_('I already have a seed'), 'restore_seed'),
(_('Watching-only wallet'), 'restore_xpub')
]
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
def new(self):
name = os.path.basename(self.storage.path)
msg = "\n".join([
_("Welcome to the Electrum installation wizard."),
_("The wallet '%s' does not exist.") % name,
_("What kind of wallet do you want to create?")
])
choices = [
(_('Standard wallet'), 'create_standard'),
(_('Multi-signature wallet'), 'create_multisig'),
]
WizardChoiceDialog(msg=msg, choices=choices, on_release=self.on_release).open()
def choose_cosigners(self):
def on_release(dialog, button):
if not button:
# soft back or escape button pressed
return self.dispatch('on_wizard_complete', None)
m = dialog.ids.m.value
n = dialog.ids.n.value
dialog.close()
self.wallet_type = "%dof%d"%(m, n)
self.run('add_seed_or_xpub')
name = os.path.basename(self.storage.path)
WizardMultisigDialog(on_release=on_release).open()
def restore_seed(self):
def on_seed(_dlg, btn):
_dlg.close()
if btn is _dlg.ids.back:
self.run('new')
return
text = _dlg.get_text()
self.run('enter_pin', (text,))
msg = _('Please type your seed phrase using the virtual keyboard.')
RestoreSeedDialog(test=Wallet.is_seed, message=msg, on_release=on_seed).open()
def restore_xpub(self):
def on_xpub(_dlg, btn):
_dlg.close()
if btn is _dlg.ids.back:
self.run('new')
return
text = _dlg.get_text()
self.run('create_wallet', (text, None))
title = "MASTER PUBLIC KEY"
message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.')
AddXpubDialog(title=title, message=message, test=Wallet.is_mpk, on_release=on_xpub).open()
def create_standard(self):
self.wallet_type = 'standard'
self.run('add_seed_or_xpub')
def create_multisig(self):
self.wallet_type = 'multisig'
self.run('choose_cosigners')
def create_wallet(self, text, password):
if self.wallet_type == 'standard':
self.wallet = Wallet.from_text(text, password, self.storage)
self.run('create_addresses')
else:
self.storage.put('wallet_type', self.wallet_type)
self.wallet = Multisig_Wallet(self.storage)
self.wallet.add_seed(text, password)
self.wallet.create_master_keys(password)
action = self.wallet.get_action()
self.run(action)
def add_cosigners(self):
def on_release(_dlg, btn):
_dlg.close()
self.run('add_cosigner')
xpub = self.wallet.master_public_keys.get('x1/')
ShowXpubDialog(xpub=xpub, test=Wallet.is_xpub, on_release=on_release).open()
def add_cosigner(self):
def on_xpub(_dlg, btn):
xpub = _dlg.get_text()
_dlg.close()
if btn is _dlg.ids.back:
self.run('add_cosigners')
return
if xpub:
self.wallet.add_master_public_key("x%d/" % 2, xpub)
action = self.wallet.get_action()
if action == 'add_cosigners': action = 'add_cosigner'
self.run(action)
title = "ADD COSIGNERS"
message = _('Please paste your cosigners master public key, or scan it using the camera button.')
AddXpubDialog(title=title, message=message, test=Wallet.is_xpub, on_release=on_xpub).open()
def create_main_account(self):
self.wallet.create_main_account()
self.run('create_addresses')
def create_addresses(self):
def task():
self.wallet.create_main_account()
self.wallet.synchronize()
msg= _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg, on_complete=self.terminate)
def create_seed(self):
from electrum.wallet import BIP32_Wallet
seed = BIP32_Wallet.make_seed()
msg = _("If you forget your PIN or lose your device, your seed phrase will be the "
"only way to recover your funds.")
def on_ok(_dlg, _btn):
_dlg.close()
if _btn == _dlg.ids.next:
self.run('confirm_seed', (seed,))
else:
self.run('new')
ShowSeedDialog(message=msg, seed_text=seed, on_release=on_ok).open()
def confirm_seed(self, seed):
assert Wallet.is_seed(seed)
def on_seed(_dlg, btn):
if btn is _dlg.ids.back:
_dlg.close()
self.run('create_seed')
return
_dlg.close()
self.run('enter_pin', (seed,))
msg = _('Please retype your seed phrase, to confirm that you properly saved it')
RestoreSeedDialog(test=lambda x: x==seed, message=msg, on_release=on_seed).open()
def enter_pin(self, seed):
def callback(pin):
action = 'confirm_pin' if pin else 'create_wallet'
self.run(action, (seed, pin))
def password_dialog(self, message, callback):
popup = PasswordDialog()
popup.init('Choose a PIN code', callback)
popup.init(message, callback)
popup.open()
def confirm_pin(self, seed, pin):
def callback(conf):
if conf == pin:
self.run('create_wallet', (seed, pin))
else:
app.show_error(_('PIN mismatch'), duration=.5)
self.run('enter_pin', (seed,))
popup = PasswordDialog()
popup.init('Confirm your PIN code', callback)
popup.open()
def terminate(self):
self.wallet.start_threads(self.network)
self.dispatch('on_wizard_complete', self.wallet)
def on_wizard_complete(self, wallet):
"""overriden by main_window"""
pass
def show_error(self, msg):
app.show_error(msg, duration=0.5)

167
lib/base_wizard.py Normal file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2016 Thomas Voegtlin
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
from electrum.wallet import Wallet, Multisig_Wallet
from electrum_gui.kivy.i18n import _
class BaseWizard(object):
def __init__(self, config, network, storage):
super(BaseWizard, self).__init__()
self.config = config
self.network = network
self.storage = storage
self.wallet = None
def run(self, action, *args):
'''Entry point of our Installation wizard'''
if not action:
return
if hasattr(self, action):
f = getattr(self, action)
apply(f, *args)
else:
raise BaseException("unknown action", action)
def new(self):
name = os.path.basename(self.storage.path)
msg = "\n".join([
_("Welcome to the Electrum installation wizard."),
_("The wallet '%s' does not exist.") % name,
_("What kind of wallet do you want to create?")
])
choices = [
(_('Standard wallet'), 'create_standard'),
(_('Multi-signature wallet'), 'create_multisig'),
]
self.choice_dialog(msg=msg, choices=choices, run_prev=self.cancel, run_next=self.run)
def choose_seed(self):
msg = ' '.join([
_("Do you want to create a new seed, or to restore a wallet using an existing seed?")
])
choices = [
(_('Create a new seed'), 'create_seed'),
(_('I already have a seed'), 'restore_seed'),
(_('Watching-only wallet'), 'restore_xpub')
]
self.choice_dialog(msg=msg, choices=choices, run_prev=self.new, run_next=self.run)
def create_multisig(self):
def f(m, n):
self.wallet_type = "%dof%d"%(m, n)
self.run('choose_seed')
name = os.path.basename(self.storage.path)
self.multisig_dialog(run_prev=self.new, run_next=f)
def restore_seed(self):
msg = _('Please type your seed phrase using the virtual keyboard.')
self.restore_seed_dialog(run_prev=self.new, run_next=self.enter_pin, test=Wallet.is_seed, message=msg)
def restore_xpub(self):
title = "MASTER PUBLIC KEY"
message = _('To create a watching-only wallet, paste your master public key, or scan it using the camera button.')
self.add_xpub_dialog(run_prev=self.new, run_next=lambda xpub: self.create_wallet(xpub, None), title=title, message=message, test=Wallet.is_mpk)
def create_standard(self):
self.wallet_type = 'standard'
self.run('choose_seed')
def create_wallet(self, text, password):
if self.wallet_type == 'standard':
self.wallet = Wallet.from_text(text, password, self.storage)
self.run('create_addresses')
else:
self.storage.put('wallet_type', self.wallet_type)
self.wallet = Multisig_Wallet(self.storage)
self.wallet.add_seed(text, password)
self.wallet.create_master_keys(password)
action = self.wallet.get_action()
self.run(action)
def add_cosigners(self):
xpub = self.wallet.master_public_keys.get('x1/')
self.show_xpub_dialog(run_prev=self.create_multisig, run_next=self.add_cosigner, xpub=xpub, test=Wallet.is_xpub)
def add_cosigner(self):
def on_xpub(xpub):
self.wallet.add_cosigner(xpub)
i = self.wallet.get_missing_cosigner()
action = 'add_cosigner' if i else 'create_main_account'
self.run(action)
title = "ADD COSIGNER"
message = _('Please paste your cosigners master public key, or scan it using the camera button.')
self.add_xpub_dialog(run_prev=self.add_cosigners, run_next=on_xpub, title=title, message=message, test=Wallet.is_xpub)
def create_main_account(self):
self.wallet.create_main_account()
self.run('create_addresses')
def create_addresses(self):
def task():
self.wallet.create_main_account()
self.wallet.synchronize()
msg= _("Electrum is generating your addresses, please wait.")
self.waiting_dialog(task, msg, on_complete=self.terminate)
def create_seed(self):
from electrum.wallet import BIP32_Wallet
seed = BIP32_Wallet.make_seed()
msg = _("If you forget your PIN or lose your device, your seed phrase will be the "
"only way to recover your funds.")
self.show_seed_dialog(run_prev=self.new, run_next=self.confirm_seed, message=msg, seed_text=seed)
def confirm_seed(self, seed):
assert Wallet.is_seed(seed)
msg = _('Please retype your seed phrase, to confirm that you properly saved it')
self.restore_seed_dialog(run_prev=self.create_seed, run_next=self.enter_pin, test=lambda x: x==seed, message=msg)
def enter_pin(self, seed):
def callback(pin):
action = 'confirm_pin' if pin else 'create_wallet'
self.run(action, (seed, pin))
self.password_dialog('Choose a PIN code', callback)
def confirm_pin(self, seed, pin):
def callback(conf):
if conf == pin:
self.run('create_wallet', (seed, pin))
else:
self.show_error(_('PIN mismatch'))
self.run('enter_pin', (seed,))
self.password_dialog('Confirm your PIN code', callback)
def terminate(self):
self.wallet.start_threads(self.network)
self.dispatch('on_wizard_complete', self.wallet)
def cancel(self):
self.dispatch('on_wizard_complete', None)
return True

View File

@ -1915,10 +1915,19 @@ class Multisig_Wallet(BIP32_RD_Wallet, Mnemonic):
def get_master_public_keys(self):
return self.master_public_keys
def get_action(self):
def get_missing_cosigner(self):
for i in range(self.n):
if self.master_public_keys.get("x%d/"%(i+1)) is None:
return 'create_seed' if i == 0 else 'add_cosigners'
return i+1
def add_cosigner(self, xpub):
i = self.get_missing_cosigner()
self.add_master_public_key("x%d/" % i, xpub)
def get_action(self):
i = self.get_missing_cosigner()
if i is not None:
return 'create_seed' if i == 1 else 'add_cosigners'
if not self.accounts:
return 'create_main_account'