new combined tablet&mobile design on top of 1.9.x branch WIP
29
gui/kivy/Makefile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
PYTHON = python
|
||||||
|
# needs kivy installed or in PYTHONPATH
|
||||||
|
|
||||||
|
.PHONY: theming apk clean
|
||||||
|
|
||||||
|
theming:
|
||||||
|
$(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png
|
||||||
|
apk:
|
||||||
|
# running pre build setup
|
||||||
|
@cp build/buildozer.spec ../../buildozer.spec
|
||||||
|
# get aes.py
|
||||||
|
@cd ../..; wget -4 https://raw.github.com/devrandom/slowaes/master/python/aes.py
|
||||||
|
# rename electrum to main.py
|
||||||
|
@mv ../../electrum ../../main.py
|
||||||
|
@-if [ ! -d "../../.buildozer" ];then \
|
||||||
|
cd ../..; buildozer android debug;\
|
||||||
|
cp -f gui/kivy/build/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\
|
||||||
|
rm -rf ./.buildozer/android/platform/python-for-android/dist;\
|
||||||
|
fi
|
||||||
|
@-cd ../..; buildozer android debug deploy run
|
||||||
|
@make clean
|
||||||
|
clean:
|
||||||
|
# Cleaning up
|
||||||
|
# remove aes
|
||||||
|
@-rm ../../aes.py
|
||||||
|
# rename main.py to electrum
|
||||||
|
@-mv ../../main.py ../../electrum
|
||||||
|
# remove buildozer.spec
|
||||||
|
@-rm ../../buildozer.spec
|
||||||
5
gui/kivy/Readme.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Commands::
|
||||||
|
|
||||||
|
`make theming` to make a atlas out of a list of pngs
|
||||||
|
|
||||||
|
`make apk` to make a apk
|
||||||
87
gui/kivy/__init__.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Electrum - lightweight Bitcoin client
|
||||||
|
# Copyright (C) 2012 thomasv@gitorious
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Kivy GUI
|
||||||
|
|
||||||
|
import sys
|
||||||
|
#, time, datetime, re, threading
|
||||||
|
#from electrum.i18n import _, set_language
|
||||||
|
#from electrum.util import print_error, print_msg, parse_url
|
||||||
|
|
||||||
|
#:TODO: replace this with kivy's own plugin managment
|
||||||
|
#from electrum.plugins import run_hook
|
||||||
|
#import os.path, json, ast, traceback
|
||||||
|
#import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.argv = ['']
|
||||||
|
import kivy
|
||||||
|
except ImportError:
|
||||||
|
# This error ideally shouldn't raised with pre-built packages
|
||||||
|
sys.exit("Error: Could not import kivy. Please install it using the" + \
|
||||||
|
"instructions mentioned here `http://kivy.org/#download` .")
|
||||||
|
|
||||||
|
# minimum required version for kivy
|
||||||
|
kivy.require('1.8.0')
|
||||||
|
from kivy.logger import Logger
|
||||||
|
|
||||||
|
from electrum.bitcoin import MIN_RELAY_TX_FEE
|
||||||
|
|
||||||
|
#:TODO main window
|
||||||
|
from main_window import ElectrumWindow
|
||||||
|
from electrum.plugins import init_plugins
|
||||||
|
|
||||||
|
#:TODO find a equivalent method to register to `bitcoin:` uri
|
||||||
|
#: ref: http://stackoverflow.com/questions/30931/register-file-extensions-mime-types-in-linux
|
||||||
|
#class OpenFileEventFilter(object):
|
||||||
|
# def __init__(self, windows):
|
||||||
|
# self.windows = windows
|
||||||
|
# super(OpenFileEventFilter, self).__init__()
|
||||||
|
#
|
||||||
|
# def eventFilter(self, obj, event):
|
||||||
|
# if event.type() == QtCore.QEvent.FileOpen:
|
||||||
|
# if len(self.windows) >= 1:
|
||||||
|
# self.windows[0].set_url(event.url().toEncoded())
|
||||||
|
# return True
|
||||||
|
# return False
|
||||||
|
|
||||||
|
|
||||||
|
class ElectrumGui:
|
||||||
|
|
||||||
|
def __init__(self, config, network, app=None):
|
||||||
|
Logger.debug('ElectrumGUI: initialising')
|
||||||
|
self.network = network
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
#:TODO
|
||||||
|
# implement kivy plugin mechanism that needs to be more extensible
|
||||||
|
# and integrated into the ui so can't be common with existing plugin
|
||||||
|
# base
|
||||||
|
#init_plugins(self)
|
||||||
|
|
||||||
|
|
||||||
|
def main(self, url):
|
||||||
|
''' The main entry point of the kivy ux
|
||||||
|
:param url: 'bitcoin:' uri as mentioned in bip0021
|
||||||
|
:type url: str
|
||||||
|
:ref: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
|
||||||
|
'''
|
||||||
|
|
||||||
|
self.main_window = w = ElectrumWindow(config=self.config,
|
||||||
|
network=self.network)
|
||||||
|
w.run()
|
||||||
40
gui/kivy/carousel.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from kivy.uix.carousel import Carousel
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
class CCarousel(Carousel):
|
||||||
|
|
||||||
|
def on_touch_move(self, touch):
|
||||||
|
if self._get_uid('cavoid') in touch.ud:
|
||||||
|
return
|
||||||
|
if self._touch is not touch:
|
||||||
|
super(Carousel, self).on_touch_move(touch)
|
||||||
|
return self._get_uid() in touch.ud
|
||||||
|
if touch.grab_current is not self:
|
||||||
|
return True
|
||||||
|
ud = touch.ud[self._get_uid()]
|
||||||
|
direction = self.direction
|
||||||
|
if ud['mode'] == 'unknown':
|
||||||
|
if direction[0] in ('r', 'l'):
|
||||||
|
distance = abs(touch.ox - touch.x)
|
||||||
|
else:
|
||||||
|
distance = abs(touch.oy - touch.y)
|
||||||
|
if distance > self.scroll_distance:
|
||||||
|
Clock.unschedule(self._change_touch_mode)
|
||||||
|
ud['mode'] = 'scroll'
|
||||||
|
else:
|
||||||
|
diff = 0
|
||||||
|
if direction[0] in ('r', 'l'):
|
||||||
|
diff = touch.dx
|
||||||
|
if direction[0] in ('t', 'b'):
|
||||||
|
diff = touch.dy
|
||||||
|
|
||||||
|
self._offset += diff * 1.27
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from kivy.app import runTouchApp
|
||||||
|
from kivy.uix.button import Button
|
||||||
|
cc = CCarousel()
|
||||||
|
for i in range(10):
|
||||||
|
cc.add_widget(Button(text=str(i)))
|
||||||
|
runTouchApp(cc)
|
||||||
93
gui/kivy/combobox.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
'''
|
||||||
|
ComboBox
|
||||||
|
=======
|
||||||
|
|
||||||
|
Based on Spinner
|
||||||
|
'''
|
||||||
|
|
||||||
|
__all__ = ('ComboBox', 'ComboBoxOption')
|
||||||
|
|
||||||
|
from kivy.properties import ListProperty, ObjectProperty, BooleanProperty
|
||||||
|
from kivy.uix.button import Button
|
||||||
|
from kivy.uix.dropdown import DropDown
|
||||||
|
from kivy.lang import Builder
|
||||||
|
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<ComboBoxOption>:
|
||||||
|
size_hint_y: None
|
||||||
|
height: 44
|
||||||
|
|
||||||
|
<ComboBox>:
|
||||||
|
background_normal: 'atlas://data/images/defaulttheme/spinner'
|
||||||
|
background_down: 'atlas://data/images/defaulttheme/spinner_pressed'
|
||||||
|
on_key:
|
||||||
|
if self.items: x, y = zip(*self.items); self.text = y[x.index(args[1])]
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
class ComboBoxOption(Button):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ComboBox(Button):
|
||||||
|
items = ListProperty()
|
||||||
|
key = ObjectProperty()
|
||||||
|
|
||||||
|
option_cls = ObjectProperty(ComboBoxOption)
|
||||||
|
|
||||||
|
dropdown_cls = ObjectProperty(DropDown)
|
||||||
|
|
||||||
|
is_open = BooleanProperty(False)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._dropdown = None
|
||||||
|
super(ComboBox, self).__init__(**kwargs)
|
||||||
|
self.items_dict = dict(self.items)
|
||||||
|
self.bind(
|
||||||
|
on_release=self._toggle_dropdown,
|
||||||
|
dropdown_cls=self._build_dropdown,
|
||||||
|
option_cls=self._build_dropdown,
|
||||||
|
items=self._update_dropdown,
|
||||||
|
key=self._update_text)
|
||||||
|
self._build_dropdown()
|
||||||
|
self._update_text()
|
||||||
|
|
||||||
|
def _update_text(self, *largs):
|
||||||
|
try:
|
||||||
|
self.text = self.items_dict[self.key]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _build_dropdown(self, *largs):
|
||||||
|
if self._dropdown:
|
||||||
|
self._dropdown.unbind(on_select=self._on_dropdown_select)
|
||||||
|
self._dropdown.dismiss()
|
||||||
|
self._dropdown = None
|
||||||
|
self._dropdown = self.dropdown_cls()
|
||||||
|
self._dropdown.bind(on_select=self._on_dropdown_select)
|
||||||
|
self._update_dropdown()
|
||||||
|
|
||||||
|
def _update_dropdown(self, *largs):
|
||||||
|
dp = self._dropdown
|
||||||
|
cls = self.option_cls
|
||||||
|
dp.clear_widgets()
|
||||||
|
for key, value in self.items:
|
||||||
|
item = cls(text=value)
|
||||||
|
# extra attribute
|
||||||
|
item.key = key
|
||||||
|
item.bind(on_release=lambda option: dp.select(option.key))
|
||||||
|
dp.add_widget(item)
|
||||||
|
|
||||||
|
def _toggle_dropdown(self, *largs):
|
||||||
|
self.is_open = not self.is_open
|
||||||
|
|
||||||
|
def _on_dropdown_select(self, instance, data, *largs):
|
||||||
|
self.key = data
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
def on_is_open(self, instance, value):
|
||||||
|
if value:
|
||||||
|
self._dropdown.open(self)
|
||||||
|
else:
|
||||||
|
self._dropdown.dismiss()
|
||||||
319
gui/kivy/console.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget
|
||||||
|
|
||||||
|
import sys, os, re
|
||||||
|
import traceback, platform
|
||||||
|
from kivy.core.window import Keyboard
|
||||||
|
from kivy.uix.textinput import TextInput
|
||||||
|
from kivy.properties import StringProperty, ListProperty, DictProperty
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
from electrum import util
|
||||||
|
|
||||||
|
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
MONOSPACE_FONT = 'Lucida Console'
|
||||||
|
elif platform.system() == 'Darwin':
|
||||||
|
MONOSPACE_FONT = 'Monaco'
|
||||||
|
else:
|
||||||
|
MONOSPACE_FONT = 'monospace'
|
||||||
|
|
||||||
|
|
||||||
|
class Console(TextInput):
|
||||||
|
|
||||||
|
prompt = StringProperty('>> ')
|
||||||
|
'''String representing the Prompt message'''
|
||||||
|
|
||||||
|
startup_message = StringProperty('')
|
||||||
|
'''Startup Message to be displayed in the Console if any'''
|
||||||
|
|
||||||
|
history = ListProperty([])
|
||||||
|
'''History of the console'''
|
||||||
|
|
||||||
|
namespace = DictProperty({})
|
||||||
|
'''Dict representing the current namespace of the console'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Console, self).__init__(**kwargs)
|
||||||
|
self.construct = []
|
||||||
|
self.showMessage(self.startup_message)
|
||||||
|
self.updateNamespace({'run':self.run_script})
|
||||||
|
self.set_json(False)
|
||||||
|
|
||||||
|
def set_json(self, b):
|
||||||
|
self.is_json = b
|
||||||
|
|
||||||
|
def run_script(self, filename):
|
||||||
|
with open(filename) as f:
|
||||||
|
script = f.read()
|
||||||
|
result = eval(script, self.namespace, self.namespace)
|
||||||
|
|
||||||
|
def updateNamespace(self, namespace):
|
||||||
|
self.namespace.update(namespace)
|
||||||
|
|
||||||
|
def showMessage(self, message):
|
||||||
|
self.appendPlainText(message)
|
||||||
|
self.newPrompt()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.setPlainText('')
|
||||||
|
self.newPrompt()
|
||||||
|
|
||||||
|
def newPrompt(self):
|
||||||
|
if self.construct:
|
||||||
|
prompt = '.' * len(self.prompt)
|
||||||
|
else:
|
||||||
|
prompt = self.prompt
|
||||||
|
|
||||||
|
self.completions_pos = self.cursor_index()
|
||||||
|
self.completions_visible = False
|
||||||
|
|
||||||
|
self.appendPlainText(prompt)
|
||||||
|
self.move_cursor_to('end')
|
||||||
|
|
||||||
|
def getCommand(self):
|
||||||
|
curr_line = self._lines[-1]
|
||||||
|
curr_line = curr_line.rstrip()
|
||||||
|
curr_line = curr_line[len(self.prompt):]
|
||||||
|
return curr_line
|
||||||
|
|
||||||
|
def setCommand(self, command):
|
||||||
|
if self.getCommand() == command:
|
||||||
|
return
|
||||||
|
curr_line = self._lines[-1]
|
||||||
|
last_pos = len(self.text)
|
||||||
|
self.select_text(last_pos - len(curr_line) + len(self.prompt), last_pos)
|
||||||
|
self.delete_selection()
|
||||||
|
self.insert_text(command)
|
||||||
|
|
||||||
|
def show_completions(self, completions):
|
||||||
|
if self.completions_visible:
|
||||||
|
self.hide_completions()
|
||||||
|
|
||||||
|
self.move_cursor_to(self.completions_pos)
|
||||||
|
|
||||||
|
completions = map(lambda x: x.split('.')[-1], completions)
|
||||||
|
t = '\n' + ' '.join(completions)
|
||||||
|
if len(t) > 500:
|
||||||
|
t = t[:500] + '...'
|
||||||
|
self.insert_text(t)
|
||||||
|
self.completions_end = self.cursor_index()
|
||||||
|
|
||||||
|
self.move_cursor_to('end')
|
||||||
|
self.completions_visible = True
|
||||||
|
|
||||||
|
|
||||||
|
def hide_completions(self):
|
||||||
|
if not self.completions_visible:
|
||||||
|
return
|
||||||
|
self.move_cursor_to(self.completions_pos)
|
||||||
|
l = self.completions_end - self.completions_pos
|
||||||
|
for x in range(l):
|
||||||
|
self.move_cursor_to('cursor_right')
|
||||||
|
self.do_backspace()
|
||||||
|
|
||||||
|
self.move_cursor_to('end')
|
||||||
|
self.completions_visible = False
|
||||||
|
|
||||||
|
def getConstruct(self, command):
|
||||||
|
if self.construct:
|
||||||
|
prev_command = self.construct[-1]
|
||||||
|
self.construct.append(command)
|
||||||
|
if not prev_command and not command:
|
||||||
|
ret_val = '\n'.join(self.construct)
|
||||||
|
self.construct = []
|
||||||
|
return ret_val
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
if command and command[-1] == (':'):
|
||||||
|
self.construct.append(command)
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
return command
|
||||||
|
|
||||||
|
def getHistory(self):
|
||||||
|
return self.history
|
||||||
|
|
||||||
|
def setHisory(self, history):
|
||||||
|
self.history = history
|
||||||
|
|
||||||
|
def addToHistory(self, command):
|
||||||
|
if command and (not self.history or self.history[-1] != command):
|
||||||
|
self.history.append(command)
|
||||||
|
self.history_index = len(self.history)
|
||||||
|
|
||||||
|
def getPrevHistoryEntry(self):
|
||||||
|
if self.history:
|
||||||
|
self.history_index = max(0, self.history_index - 1)
|
||||||
|
return self.history[self.history_index]
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def getNextHistoryEntry(self):
|
||||||
|
if self.history:
|
||||||
|
hist_len = len(self.history)
|
||||||
|
self.history_index = min(hist_len, self.history_index + 1)
|
||||||
|
if self.history_index < hist_len:
|
||||||
|
return self.history[self.history_index]
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def getCursorPosition(self):
|
||||||
|
return self.cursor[0] - len(self.prompt)
|
||||||
|
|
||||||
|
def setCursorPosition(self, position):
|
||||||
|
self.cursor = (len(self.prompt) + position, self.cursor[1])
|
||||||
|
|
||||||
|
def register_command(self, c, func):
|
||||||
|
methods = { c: func}
|
||||||
|
self.updateNamespace(methods)
|
||||||
|
|
||||||
|
|
||||||
|
def runCommand(self):
|
||||||
|
command = self.getCommand()
|
||||||
|
self.addToHistory(command)
|
||||||
|
|
||||||
|
command = self.getConstruct(command)
|
||||||
|
|
||||||
|
if command:
|
||||||
|
tmp_stdout = sys.stdout
|
||||||
|
|
||||||
|
class stdoutProxy():
|
||||||
|
def __init__(self, write_func):
|
||||||
|
self.write_func = write_func
|
||||||
|
self.skip = False
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
if not self.skip:
|
||||||
|
stripped_text = text.rstrip('\n')
|
||||||
|
self.write_func(stripped_text)
|
||||||
|
self.skip = not self.skip
|
||||||
|
|
||||||
|
if type(self.namespace.get(command)) == type(lambda:None):
|
||||||
|
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command))
|
||||||
|
self.newPrompt()
|
||||||
|
return
|
||||||
|
|
||||||
|
sys.stdout = stdoutProxy(self.appendPlainText)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
result = eval(command, self.namespace, self.namespace)
|
||||||
|
if result != None:
|
||||||
|
if self.is_json:
|
||||||
|
util.print_json(result)
|
||||||
|
else:
|
||||||
|
self.appendPlainText(repr(result))
|
||||||
|
except SyntaxError:
|
||||||
|
exec command in self.namespace
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
traceback_lines = traceback.format_exc().split('\n')
|
||||||
|
# Remove traceback mentioning this file, and a linebreak
|
||||||
|
for i in (3,2,1,-1):
|
||||||
|
traceback_lines.pop(i)
|
||||||
|
self.appendPlainText('\n'.join(traceback_lines))
|
||||||
|
sys.stdout = tmp_stdout
|
||||||
|
self.newPrompt()
|
||||||
|
self.set_json(False)
|
||||||
|
|
||||||
|
def _keyboard_on_key_down(self, window, keycode, text, modifiers):
|
||||||
|
self._hide_cut_copy_paste()
|
||||||
|
is_osx = sys.platform == 'darwin'
|
||||||
|
# Keycodes on OSX:
|
||||||
|
ctrl, cmd = 64, 1024
|
||||||
|
key, key_str = keycode
|
||||||
|
|
||||||
|
if key == Keyboard.keycodes['tab']:
|
||||||
|
self.completions()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.hide_completions()
|
||||||
|
|
||||||
|
if key == Keyboard.keycodes['enter']:
|
||||||
|
self.runCommand()
|
||||||
|
return
|
||||||
|
if key == Keyboard.keycodes['home']:
|
||||||
|
self.setCursorPosition(0)
|
||||||
|
return
|
||||||
|
if key == Keyboard.keycodes['pageup']:
|
||||||
|
return
|
||||||
|
elif key in (Keyboard.keycodes['left'], Keyboard.keycodes['backspace']):
|
||||||
|
if self.getCursorPosition() == 0:
|
||||||
|
return
|
||||||
|
elif key == Keyboard.keycodes['up']:
|
||||||
|
self.setCommand(self.getPrevHistoryEntry())
|
||||||
|
return
|
||||||
|
elif key == Keyboard.keycodes['down']:
|
||||||
|
self.setCommand(self.getNextHistoryEntry())
|
||||||
|
return
|
||||||
|
elif key == Keyboard.keycodes['l'] and modifiers == ['ctrl']:
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
super(Console, self)._keyboard_on_key_down(window, keycode, text, modifiers)
|
||||||
|
|
||||||
|
def completions(self):
|
||||||
|
cmd = self.getCommand()
|
||||||
|
lastword = re.split(' |\(|\)',cmd)[-1]
|
||||||
|
beginning = cmd[0:-len(lastword)]
|
||||||
|
|
||||||
|
path = lastword.split('.')
|
||||||
|
ns = self.namespace.keys()
|
||||||
|
|
||||||
|
if len(path) == 1:
|
||||||
|
ns = ns
|
||||||
|
prefix = ''
|
||||||
|
else:
|
||||||
|
obj = self.namespace.get(path[0])
|
||||||
|
prefix = path[0] + '.'
|
||||||
|
ns = dir(obj)
|
||||||
|
|
||||||
|
|
||||||
|
completions = []
|
||||||
|
for x in ns:
|
||||||
|
if x[0] == '_':continue
|
||||||
|
xx = prefix + x
|
||||||
|
if xx.startswith(lastword):
|
||||||
|
completions.append(xx)
|
||||||
|
completions.sort()
|
||||||
|
|
||||||
|
if not completions:
|
||||||
|
self.hide_completions()
|
||||||
|
elif len(completions) == 1:
|
||||||
|
self.hide_completions()
|
||||||
|
self.setCommand(beginning + completions[0])
|
||||||
|
else:
|
||||||
|
# find common prefix
|
||||||
|
p = os.path.commonprefix(completions)
|
||||||
|
if len(p)>len(lastword):
|
||||||
|
self.hide_completions()
|
||||||
|
self.setCommand(beginning + p)
|
||||||
|
else:
|
||||||
|
self.show_completions(completions)
|
||||||
|
|
||||||
|
# NEW
|
||||||
|
def setPlainText(self, message):
|
||||||
|
"""Equivalent to QT version"""
|
||||||
|
self.text = message
|
||||||
|
|
||||||
|
# NEW
|
||||||
|
def appendPlainText(self, message):
|
||||||
|
"""Equivalent to QT version"""
|
||||||
|
if len(self.text) == 0:
|
||||||
|
self.text = message
|
||||||
|
else:
|
||||||
|
if message:
|
||||||
|
self.text += '\n' + message
|
||||||
|
|
||||||
|
# NEW
|
||||||
|
def move_cursor_to(self, pos):
|
||||||
|
"""Aggregate all cursor moving functions"""
|
||||||
|
if isinstance(pos, int):
|
||||||
|
self.cursor = self.get_cursor_from_index(pos)
|
||||||
|
elif pos in ('end', 'pgend', 'pageend'):
|
||||||
|
def updt_cursor(*l):
|
||||||
|
self.cursor = self.get_cursor_from_index(self.text)
|
||||||
|
Clock.schedule_once(updt_cursor)
|
||||||
|
else: # cursor_home, cursor_end, ... (see docs)
|
||||||
|
self.do_cursor_movement(pos)
|
||||||
611
gui/kivy/dialog.py
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
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 ''
|
||||||
|
'''
|
||||||
|
|
||||||
|
fs = BooleanProperty(False)
|
||||||
|
''' Show Bubble in half screen mode
|
||||||
|
'''
|
||||||
|
|
||||||
|
modal = BooleanProperty(False)
|
||||||
|
''' Allow bubble to be hidden on touch.
|
||||||
|
'''
|
||||||
|
|
||||||
|
dim_background = BooleanProperty(False)
|
||||||
|
''' Whether to draw a background on the windows behind the bubble
|
||||||
|
'''
|
||||||
|
|
||||||
|
def on_touch_down(self, touch):
|
||||||
|
if self.modal:
|
||||||
|
return
|
||||||
|
self.hide()
|
||||||
|
if self.collide_point(*touch.pos):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def show(self, pos, duration, width=None, modal=False):
|
||||||
|
'''Animate the bubble into position'''
|
||||||
|
self.modal = modal
|
||||||
|
if width:
|
||||||
|
self.width = width
|
||||||
|
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, *dt):
|
||||||
|
''' Auto fade out the Bubble
|
||||||
|
'''
|
||||||
|
def on_stop(*l):
|
||||||
|
Window.remove_widget(self)
|
||||||
|
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 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 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 VerifySeedDialog(CreateAccountDialog):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RestoreSeedDialog(CreateAccountDialog):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
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'))
|
||||||
|
''' Defines the mode of the password dialog.'''
|
||||||
|
|
||||||
|
def validate_new_password(self):
|
||||||
|
self.ids.confirm.dispatch('on_release')
|
||||||
|
|
||||||
|
def on_parent(self, instance, value):
|
||||||
|
if value:
|
||||||
|
stepper = self.ids.stepper
|
||||||
|
stepper.opacity = 1
|
||||||
|
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")
|
||||||
173
gui/kivy/drawer.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
|
||||||
|
from kivy.uix.stencilview import StencilView
|
||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
from kivy.uix.image import Image
|
||||||
|
|
||||||
|
from kivy.animation import Animation
|
||||||
|
from kivy.clock import Clock
|
||||||
|
from kivy.properties import OptionProperty, NumericProperty, ObjectProperty
|
||||||
|
|
||||||
|
# delayed import
|
||||||
|
app = None
|
||||||
|
|
||||||
|
|
||||||
|
class Drawer(StencilView):
|
||||||
|
|
||||||
|
state = OptionProperty('closed',
|
||||||
|
options=('closed', 'open', 'opening', 'closing'))
|
||||||
|
'''This indicates the current state the drawer is in.
|
||||||
|
|
||||||
|
:attr:`state` is a `OptionProperty` defaults to `closed`. Can be one of
|
||||||
|
`closed`, `open`, `opening`, `closing`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
scroll_timeout = NumericProperty(200)
|
||||||
|
'''Timeout allowed to trigger the :data:`scroll_distance`,
|
||||||
|
in milliseconds. If the user has not moved :data:`scroll_distance`
|
||||||
|
within the timeout, the scrolling will be disabled and the touch event
|
||||||
|
will go to the children.
|
||||||
|
|
||||||
|
:data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty`
|
||||||
|
and defaults to 200 (milliseconds)
|
||||||
|
'''
|
||||||
|
|
||||||
|
scroll_distance = NumericProperty('4dp')
|
||||||
|
'''Distance to move before scrolling the :class:`Drawer` in pixels.
|
||||||
|
As soon as the distance has been traveled, the :class:`Drawer` will
|
||||||
|
start to scroll, and no touch event will go to children.
|
||||||
|
It is advisable that you base this value on the dpi of your target
|
||||||
|
device's screen.
|
||||||
|
|
||||||
|
:data:`scroll_distance` is a :class:`~kivy.properties.NumericProperty`
|
||||||
|
and defaults to 20dp.
|
||||||
|
'''
|
||||||
|
|
||||||
|
drag_area = NumericProperty(.1)
|
||||||
|
'''The percentage of area on the left edge that triggers the opening of
|
||||||
|
the drawer. from 0-1
|
||||||
|
|
||||||
|
:attr:`drag_area` is a `NumericProperty` defaults to 2
|
||||||
|
'''
|
||||||
|
|
||||||
|
_hidden_widget = ObjectProperty(None)
|
||||||
|
_overlay_widget = ObjectProperty(None)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(Drawer, self).__init__(**kwargs)
|
||||||
|
self.bind(pos=self._do_layout,
|
||||||
|
size=self._do_layout,
|
||||||
|
children=self._do_layout)
|
||||||
|
|
||||||
|
def _do_layout(self, instance, value):
|
||||||
|
if not self._hidden_widget or not self._overlay_widget:
|
||||||
|
return
|
||||||
|
self._overlay_widget.height = self._hidden_widget.height =\
|
||||||
|
self.height
|
||||||
|
|
||||||
|
def on_touch_down(self, touch):
|
||||||
|
if self.disabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
global app
|
||||||
|
if not app:
|
||||||
|
from kivy.app import App
|
||||||
|
app = App.get_running_app()
|
||||||
|
|
||||||
|
# skip on tablet mode
|
||||||
|
if app.ui_mode[0] == 't':
|
||||||
|
return super(Drawer, self).on_touch_down(touch)
|
||||||
|
|
||||||
|
touch.ud['send_touch_down'] = False
|
||||||
|
drag_area = ((self.width * self.drag_area)
|
||||||
|
if self.state[0] == 'c' else
|
||||||
|
self._hidden_widget.width)
|
||||||
|
if touch.x > drag_area:
|
||||||
|
return super(Drawer, self).on_touch_down(touch)
|
||||||
|
self._touch = touch
|
||||||
|
Clock.schedule_once(self._change_touch_mode,
|
||||||
|
self.scroll_timeout/1000.)
|
||||||
|
touch.ud['in_drag_area'] = True
|
||||||
|
touch.ud['send_touch_down'] = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_touch_move(self, touch):
|
||||||
|
global app
|
||||||
|
if not app:
|
||||||
|
from kivy.app import App
|
||||||
|
app = App.get_running_app()
|
||||||
|
# skip on tablet mode
|
||||||
|
if app.ui_mode[0] == 't':
|
||||||
|
return super(Drawer, self).on_touch_move(touch)
|
||||||
|
|
||||||
|
if not touch.ud.get('in_drag_area', None):
|
||||||
|
return super(Drawer, self).on_touch_move(touch)
|
||||||
|
|
||||||
|
self._overlay_widget.x = min(self._hidden_widget.width,
|
||||||
|
max(self._overlay_widget.x + touch.dx*2, 0))
|
||||||
|
if abs(touch.x - touch.ox) < self.scroll_distance:
|
||||||
|
return
|
||||||
|
touch.ud['send_touch_down'] = False
|
||||||
|
Clock.unschedule(self._change_touch_mode)
|
||||||
|
self._touch = None
|
||||||
|
self.state = 'opening' if touch.dx > 0 else 'closing'
|
||||||
|
touch.ox = touch.x
|
||||||
|
return
|
||||||
|
|
||||||
|
def _change_touch_mode(self, *args):
|
||||||
|
if not self._touch:
|
||||||
|
return
|
||||||
|
touch = self._touch
|
||||||
|
touch.ud['in_drag_area'] = False
|
||||||
|
touch.ud['send_touch_down'] = False
|
||||||
|
self._touch = None
|
||||||
|
super(Drawer, self).on_touch_down(touch)
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_touch_up(self, touch):
|
||||||
|
# skip on tablet mode
|
||||||
|
if app.ui_mode[0] == 't':
|
||||||
|
return super(Drawer, self).on_touch_down(touch)
|
||||||
|
|
||||||
|
if touch.ud.get('send_touch_down', None):
|
||||||
|
Clock.unschedule(self._change_touch_mode)
|
||||||
|
Clock.schedule_once(
|
||||||
|
lambda dt: super(Drawer, self).on_touch_down(touch), -1)
|
||||||
|
if touch.ud.get('in_drag_area', None):
|
||||||
|
touch.ud['in_drag_area'] = False
|
||||||
|
Animation.cancel_all(self._overlay_widget)
|
||||||
|
anim = Animation(x=self._hidden_widget.width
|
||||||
|
if self.state[0] == 'o' else 0,
|
||||||
|
d=.1, t='linear')
|
||||||
|
anim.bind(on_complete = self._complete_drawer_animation)
|
||||||
|
anim.start(self._overlay_widget)
|
||||||
|
Clock.schedule_once(
|
||||||
|
lambda dt: super(Drawer, self).on_touch_up(touch), 0)
|
||||||
|
|
||||||
|
def _complete_drawer_animation(self, *args):
|
||||||
|
self.state = 'open' if self.state[0] == 'o' else 'closed'
|
||||||
|
|
||||||
|
def add_widget(self, widget, index=1):
|
||||||
|
if not widget:
|
||||||
|
return
|
||||||
|
children = self.children
|
||||||
|
len_children = len(children)
|
||||||
|
if len_children == 2:
|
||||||
|
Logger.debug('Drawer: No more than two widgets allowed')
|
||||||
|
return
|
||||||
|
|
||||||
|
super(Drawer, self).add_widget(widget)
|
||||||
|
if len_children == 0:
|
||||||
|
# first widget add it to the hidden/drawer section
|
||||||
|
self._hidden_widget = widget
|
||||||
|
return
|
||||||
|
# Second Widget
|
||||||
|
self._overlay_widget = widget
|
||||||
|
|
||||||
|
def remove_widget(self, widget):
|
||||||
|
super(Drawer, self).remove_widget(self)
|
||||||
|
if widget == self._hidden_widget:
|
||||||
|
self._hidden_widget = None
|
||||||
|
return
|
||||||
|
if widget == self._overlay_widget:
|
||||||
|
self._overlay_widget = None
|
||||||
|
return
|
||||||
203
gui/kivy/gridview.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
from kivy.adapters.dictadapter import DictAdapter
|
||||||
|
from kivy.adapters.listadapter import ListAdapter
|
||||||
|
from kivy.properties import ObjectProperty, ListProperty, AliasProperty
|
||||||
|
from kivy.uix.listview import (ListItemButton, ListItemLabel, CompositeListItem,
|
||||||
|
ListView)
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.metrics import dp, sp
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<GridView>
|
||||||
|
header_view: header_view
|
||||||
|
content_view: content_view
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
padding: '0dp', '2dp'
|
||||||
|
BoxLayout:
|
||||||
|
id: header_box
|
||||||
|
orientation: 'vertical'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '30dp'
|
||||||
|
ListView:
|
||||||
|
id: header_view
|
||||||
|
BoxLayout:
|
||||||
|
id: content_box
|
||||||
|
orientation: 'vertical'
|
||||||
|
ListView:
|
||||||
|
id: content_view
|
||||||
|
|
||||||
|
<-HorizVertGrid>
|
||||||
|
header_view: header_view
|
||||||
|
content_view: content_view
|
||||||
|
ScrollView:
|
||||||
|
id: scrl
|
||||||
|
do_scroll_y: False
|
||||||
|
RelativeLayout:
|
||||||
|
size_hint_x: None
|
||||||
|
width: max(scrl.width, dp(sum(root.widths)))
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
padding: '0dp', '2dp'
|
||||||
|
BoxLayout:
|
||||||
|
id: header_box
|
||||||
|
orientation: 'vertical'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '30dp'
|
||||||
|
ListView:
|
||||||
|
id: header_view
|
||||||
|
BoxLayout:
|
||||||
|
id: content_box
|
||||||
|
orientation: 'vertical'
|
||||||
|
ListView:
|
||||||
|
id: content_view
|
||||||
|
|
||||||
|
''')
|
||||||
|
|
||||||
|
class GridView(BoxLayout):
|
||||||
|
"""Workaround solution for grid view by using 2 list view.
|
||||||
|
Sometimes the height of lines is shown properly."""
|
||||||
|
|
||||||
|
def _get_hd_adpt(self):
|
||||||
|
return self.ids.header_view.adapter
|
||||||
|
|
||||||
|
header_adapter = AliasProperty(_get_hd_adpt, None)
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
|
||||||
|
def _get_cnt_adpt(self):
|
||||||
|
return self.ids.content_view.adapter
|
||||||
|
|
||||||
|
content_adapter = AliasProperty(_get_cnt_adpt, None)
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
|
||||||
|
headers = ListProperty([])
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
|
||||||
|
widths = ListProperty([])
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
|
||||||
|
data = ListProperty([])
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
|
||||||
|
getter = ObjectProperty(lambda item, i: item[i])
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
on_context_menu = ObjectProperty(None)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(GridView, self).__init__(**kwargs)
|
||||||
|
self._from_widths = False
|
||||||
|
#self.on_headers(self, self.headers)
|
||||||
|
|
||||||
|
def on_widths(self, instance, value):
|
||||||
|
self._from_widths = True
|
||||||
|
self.on_headers(instance, self.headers)
|
||||||
|
self._from_widths = False
|
||||||
|
|
||||||
|
def on_headers(self, instance, value):
|
||||||
|
if not self._from_widths:
|
||||||
|
return
|
||||||
|
if not (value and self.canvas and self.headers):
|
||||||
|
return
|
||||||
|
widths = self.widths
|
||||||
|
if len(self.widths) != len(value):
|
||||||
|
return
|
||||||
|
#if widths is not None:
|
||||||
|
# widths = ['%sdp' % i for i in widths]
|
||||||
|
|
||||||
|
def generic_args_converter(row_index,
|
||||||
|
item,
|
||||||
|
is_header=True,
|
||||||
|
getter=self.getter):
|
||||||
|
cls_dicts = []
|
||||||
|
_widths = self.widths
|
||||||
|
getter = self.getter
|
||||||
|
on_context_menu = self.on_context_menu
|
||||||
|
|
||||||
|
for i, header in enumerate(self.headers):
|
||||||
|
kwargs = {
|
||||||
|
'padding': ('2dp','2dp'),
|
||||||
|
'halign': 'center',
|
||||||
|
'valign': 'middle',
|
||||||
|
'size_hint_y': None,
|
||||||
|
'shorten': True,
|
||||||
|
'height': '30dp',
|
||||||
|
'text_size': (_widths[i], dp(30)),
|
||||||
|
'text': getter(item, i),
|
||||||
|
}
|
||||||
|
|
||||||
|
kwargs['font_size'] = '9sp'
|
||||||
|
if is_header:
|
||||||
|
kwargs['deselected_color'] = kwargs['selected_color'] =\
|
||||||
|
[0, 1, 1, 1]
|
||||||
|
else: # this is content
|
||||||
|
kwargs['deselected_color'] = 1, 1, 1, 1
|
||||||
|
if on_context_menu is not None:
|
||||||
|
kwargs['on_press'] = on_context_menu
|
||||||
|
|
||||||
|
if widths is not None: # set width manually
|
||||||
|
kwargs['size_hint_x'] = None
|
||||||
|
kwargs['width'] = widths[i]
|
||||||
|
|
||||||
|
cls_dicts.append({
|
||||||
|
'cls': ListItemButton,
|
||||||
|
'kwargs': kwargs,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': item[-1],
|
||||||
|
'size_hint_y': None,
|
||||||
|
'height': '30dp',
|
||||||
|
'cls_dicts': cls_dicts,
|
||||||
|
}
|
||||||
|
|
||||||
|
def header_args_converter(row_index, item):
|
||||||
|
return generic_args_converter(row_index, item)
|
||||||
|
|
||||||
|
def content_args_converter(row_index, item):
|
||||||
|
return generic_args_converter(row_index, item, is_header=False)
|
||||||
|
|
||||||
|
|
||||||
|
self.ids.header_view.adapter = ListAdapter(data=[self.headers],
|
||||||
|
args_converter=header_args_converter,
|
||||||
|
selection_mode='single',
|
||||||
|
allow_empty_selection=False,
|
||||||
|
cls=CompositeListItem)
|
||||||
|
|
||||||
|
self.ids.content_view.adapter = ListAdapter(data=self.data,
|
||||||
|
args_converter=content_args_converter,
|
||||||
|
selection_mode='single',
|
||||||
|
allow_empty_selection=False,
|
||||||
|
cls=CompositeListItem)
|
||||||
|
self.content_adapter.bind_triggers_to_view(self.ids.content_view._trigger_reset_populate)
|
||||||
|
|
||||||
|
class HorizVertGrid(GridView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from kivy.app import App
|
||||||
|
class MainApp(App):
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
data = []
|
||||||
|
for i in range(90):
|
||||||
|
data.append((str(i), str(i)))
|
||||||
|
self.data = data
|
||||||
|
return Builder.load_string('''
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
HorizVertGrid:
|
||||||
|
on_parent: if args[1]: self.content_adapter.data = app.data
|
||||||
|
headers:['Address', 'Previous output']
|
||||||
|
widths: [400, 500]
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
font_size: '16sp'
|
||||||
|
''')
|
||||||
|
MainApp().run()
|
||||||
224
gui/kivy/installwizard.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
from electrum import Wallet
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum_gui.kivy.dialog import (CreateRestoreDialog, InitSeedDialog,
|
||||||
|
ChangePasswordDialog)
|
||||||
|
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
from kivy.core.window import Window
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
#from seed_dialog import SeedDialog
|
||||||
|
#from network_dialog import NetworkDialog
|
||||||
|
#from util import *
|
||||||
|
#from amountedit import AmountEdit
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
# global Variables
|
||||||
|
app = App.get_running_app()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def waiting_dialog(self, task,
|
||||||
|
msg= _("Electrum is generating your addresses,"
|
||||||
|
" please wait.")):
|
||||||
|
def target():
|
||||||
|
task()
|
||||||
|
Clock.schedule_once(lambda dt:
|
||||||
|
app.show_info_bubble(text="Complete", duration=.5,
|
||||||
|
icon='atlas://gui/kivy/theming/light/important',
|
||||||
|
pos=Window.center, width='200dp', arrow_pos=None))
|
||||||
|
|
||||||
|
app.show_info_bubble(
|
||||||
|
text=msg, icon='atlas://gui/kivy/theming/light/important',
|
||||||
|
pos=Window.center, width='200sp', arrow_pos=None, modal=True)
|
||||||
|
t = threading.Thread(target = target)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
CreateRestoreDialog(on_release=self.on_creatrestore_complete).open()
|
||||||
|
|
||||||
|
def on_creatrestore_complete(self, dialog, button):
|
||||||
|
if not button:
|
||||||
|
self.dispatch('on_wizard_complete', None)
|
||||||
|
return
|
||||||
|
wallet = Wallet(self.storage)
|
||||||
|
gap = self.config.get('gap_limit', 5)
|
||||||
|
if gap !=5:
|
||||||
|
wallet.gap_limit = gap_limit
|
||||||
|
wallet.storage.put('gap_limit', gap, True)
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
if button == dialog.ids.create:
|
||||||
|
# create
|
||||||
|
self.change_password_dialog(wallet=wallet)
|
||||||
|
elif button == dialog.ids.restore:
|
||||||
|
# restore
|
||||||
|
wallet.init_seed(None)
|
||||||
|
self.restore_seed_dialog()
|
||||||
|
#elif button == dialog.ids.watching:
|
||||||
|
# self.action = 'watching'
|
||||||
|
else:
|
||||||
|
self.dispatch('on_wizard_complete', None)
|
||||||
|
|
||||||
|
def init_seed_dialog(self, wallet=None, instance=None, password=None,
|
||||||
|
wallet_name=None):
|
||||||
|
# renamed from show_seed()
|
||||||
|
'''Can be called directly (password is None)
|
||||||
|
or from a password-protected callback (password is not None)'''
|
||||||
|
|
||||||
|
if not wallet or not wallet.seed:
|
||||||
|
if instance == None:
|
||||||
|
wallet.init_seed(None)
|
||||||
|
else:
|
||||||
|
return MessageBoxError(message=_('No seed')).open()
|
||||||
|
|
||||||
|
if password is None or not instance:
|
||||||
|
seed = wallet.get_mnemonic(None)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
seed = self.wallet.get_seed(password)
|
||||||
|
except Exception:
|
||||||
|
return MessageBoxError(message=_('Incorrect Password'))
|
||||||
|
|
||||||
|
brainwallet = seed
|
||||||
|
|
||||||
|
msg2 = _("[color=#414141][b]"+\
|
||||||
|
"[b]PLEASE WRITE DOWN YOUR SEED PASS[/b][/color]"+\
|
||||||
|
"[size=9]\n\n[/size]" +\
|
||||||
|
"[color=#929292]If you ever forget your pincode, your seed" +\
|
||||||
|
" phrase will be the [color=#EB984E]"+\
|
||||||
|
"[b]only way to recover[/b][/color] your wallet. Your " +\
|
||||||
|
" [color=#EB984E][b]Bitcoins[/b][/color] will otherwise be" +\
|
||||||
|
" [color=#EB984E]lost forever![/color]")
|
||||||
|
|
||||||
|
if wallet.imported_keys:
|
||||||
|
msg2 += "[b][color=#ff0000ff]" + _("WARNING") + "[/color]:[/b] " +\
|
||||||
|
_("Your wallet contains imported keys. These keys cannot" +\
|
||||||
|
" be recovered from seed.")
|
||||||
|
|
||||||
|
def on_ok_press(_dlg, _btn):
|
||||||
|
_dlg.close()
|
||||||
|
if _btn != _dlg.ids.confirm:
|
||||||
|
self.change_password_dialog(wallet)
|
||||||
|
return
|
||||||
|
if instance is None:
|
||||||
|
# in initial phase
|
||||||
|
def create(password):
|
||||||
|
try:
|
||||||
|
password = None if not password else password
|
||||||
|
wallet.save_seed(password)
|
||||||
|
except Exception as err:
|
||||||
|
Logger.Info('Wallet: {}'.format(err))
|
||||||
|
Clock.schedule_once(lambda dt:
|
||||||
|
app.show_error(err))
|
||||||
|
wallet.synchronize() # generate first addresses offline
|
||||||
|
self.waiting_dialog(partial(create, password))
|
||||||
|
|
||||||
|
|
||||||
|
InitSeedDialog(message=msg2,
|
||||||
|
seed_msg=brainwallet,
|
||||||
|
seed=seed,
|
||||||
|
on_release=on_ok_press).open()
|
||||||
|
|
||||||
|
def change_password_dialog(self, wallet=None, instance=None):
|
||||||
|
"""Can be called directly (instance is None)
|
||||||
|
or from a callback (instance is not None)"""
|
||||||
|
|
||||||
|
if instance and not wallet.seed:
|
||||||
|
return MessageBoxExit(message=_('No seed !!')).open()
|
||||||
|
|
||||||
|
if instance is not None:
|
||||||
|
if wallet.use_encryption:
|
||||||
|
msg = (
|
||||||
|
_('Your wallet is encrypted. Use this dialog to change" + \
|
||||||
|
" your password.') + '\n' + _('To disable wallet" + \
|
||||||
|
" encryption, enter an empty new password.'))
|
||||||
|
mode = 'confirm'
|
||||||
|
else:
|
||||||
|
msg = _('Your wallet keys are not encrypted')
|
||||||
|
mode = 'new'
|
||||||
|
else:
|
||||||
|
msg = _("Please choose a password to encrypt your wallet keys.") +\
|
||||||
|
'\n' + _("Leave these fields empty if you want to disable" + \
|
||||||
|
" encryption.")
|
||||||
|
mode = 'create'
|
||||||
|
|
||||||
|
def on_release(_dlg, _btn):
|
||||||
|
ti_password = _dlg.ids.ti_password
|
||||||
|
ti_new_password = _dlg.ids.ti_new_password
|
||||||
|
ti_confirm_password = _dlg.ids.ti_confirm_password
|
||||||
|
if _btn != _dlg.ids.next:
|
||||||
|
_dlg.close()
|
||||||
|
if not instance:
|
||||||
|
CreateRestoreDialog(
|
||||||
|
on_release=self.on_creatrestore_complete).open()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirm
|
||||||
|
wallet_name = _dlg.ids.ti_wallet_name.text
|
||||||
|
password = (unicode(ti_password.text)
|
||||||
|
if wallet.use_encryption else
|
||||||
|
None)
|
||||||
|
new_password = unicode(ti_new_password.text)
|
||||||
|
new_password2 = unicode(ti_confirm_password.text)
|
||||||
|
|
||||||
|
if new_password != new_password2:
|
||||||
|
ti_password.text = ""
|
||||||
|
ti_new_password.text = ""
|
||||||
|
ti_confirm_password.text = ""
|
||||||
|
if ti_password.disabled:
|
||||||
|
ti_new_password.focus = True
|
||||||
|
else:
|
||||||
|
ti_password.focus = True
|
||||||
|
return app.show_error(_('Passwords do not match'))
|
||||||
|
|
||||||
|
if not instance:
|
||||||
|
_dlg.close()
|
||||||
|
self.init_seed_dialog(password=new_password,
|
||||||
|
wallet=wallet,
|
||||||
|
wallet_name=wallet_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
seed = wallet.decode_seed(password)
|
||||||
|
except BaseException:
|
||||||
|
return MessageBoxError(
|
||||||
|
message=_('Incorrect Password')).open()
|
||||||
|
|
||||||
|
# test carefully
|
||||||
|
try:
|
||||||
|
wallet.update_password(seed, password, new_password)
|
||||||
|
except BaseException:
|
||||||
|
return MessageBoxExit(
|
||||||
|
message=_('Failed to update password')).open()
|
||||||
|
else:
|
||||||
|
app.show_info_bubble(
|
||||||
|
text=_('Password successfully updated'), duration=1,
|
||||||
|
pos=_btn.pos)
|
||||||
|
_dlg.close()
|
||||||
|
|
||||||
|
|
||||||
|
if instance is None: # in initial phase
|
||||||
|
self.load_wallet()
|
||||||
|
self.app.gui.main_gui.update_wallet()
|
||||||
|
|
||||||
|
cpd = ChangePasswordDialog(
|
||||||
|
message=msg,
|
||||||
|
mode=mode,
|
||||||
|
on_release=on_release).open()
|
||||||
|
|
||||||
|
def on_wizard_complete(self, instance, wallet):
|
||||||
|
pass
|
||||||
401
gui/kivy/main.kv
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
#:import Window kivy.core.window.Window
|
||||||
|
#:import _ electrum.i18n._
|
||||||
|
#:import partial functools.partial
|
||||||
|
|
||||||
|
# Custom Global Widgets
|
||||||
|
|
||||||
|
<VGridLayout@GridLayout>:
|
||||||
|
rows: 1
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
|
||||||
|
<IconButton@ButtonBehavior+Image>
|
||||||
|
allow_stretch: True
|
||||||
|
size_hint_x: None
|
||||||
|
width: self.height
|
||||||
|
canvas:
|
||||||
|
BorderImage:
|
||||||
|
border: (10, 10, 10, 10)
|
||||||
|
source:
|
||||||
|
'atlas://gui/kivy/theming/light/' + ('tab_btn'\
|
||||||
|
if root.state == 'normal' else 'icon_border')
|
||||||
|
size: root.size
|
||||||
|
pos: root.pos
|
||||||
|
###########################
|
||||||
|
## Gloabal Defaults
|
||||||
|
###########################
|
||||||
|
|
||||||
|
<Label>
|
||||||
|
markup: True
|
||||||
|
font_name: 'data/fonts/Roboto.ttf'
|
||||||
|
font_size: '16sp'
|
||||||
|
|
||||||
|
<ListItemButton>
|
||||||
|
font_size: '12sp'
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Dialogs
|
||||||
|
#########################
|
||||||
|
|
||||||
|
################################################
|
||||||
|
## Create Dialogs
|
||||||
|
################################################
|
||||||
|
|
||||||
|
<CreateAccountTextInput@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'
|
||||||
|
|
||||||
|
<CreateAccountButtonBlue@Button>
|
||||||
|
canvas.after:
|
||||||
|
Color
|
||||||
|
rgba: 1, 1, 1, 1 if self.disabled else 0
|
||||||
|
Rectangle:
|
||||||
|
texture: self.texture
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
Color
|
||||||
|
rgba: .5, .5, .5, .5 if self.disabled else 0
|
||||||
|
Rectangle:
|
||||||
|
texture: self.texture
|
||||||
|
size: self.size
|
||||||
|
pos: self.x - dp(1), self.y + dp(1)
|
||||||
|
border: 15, 5, 5, 5
|
||||||
|
background_color: (1, 1, 1, 1) if self.disabled else (.203, .490, .741, 1 if self.state == 'normal' else .75)
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48sp'
|
||||||
|
text_size: self.size
|
||||||
|
halign: 'center'
|
||||||
|
valign: 'middle'
|
||||||
|
background_normal: 'atlas://gui/kivy/theming/light/btn_create_account'
|
||||||
|
background_down: 'atlas://gui/kivy/theming/light/btn_create_account'
|
||||||
|
background_disabled_normal: 'atlas://gui/kivy/theming/light/btn_create_act_disabled'
|
||||||
|
on_release: self.root.dispatch('on_press', self)
|
||||||
|
on_release: self.root.dispatch('on_release', self)
|
||||||
|
|
||||||
|
<CreateAccountButtonGreen@CreateAccountButtonBlue>
|
||||||
|
background_color: (1, 1, 1, 1) if self.disabled else (.415, .717, 0, 1 if self.state == 'normal' else .75)
|
||||||
|
|
||||||
|
<InfoBubble>
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: 0, 0, 0, .7 if root.dim_background else 0
|
||||||
|
Rectangle:
|
||||||
|
size: Window.size
|
||||||
|
size_hint: None, None
|
||||||
|
width: '270dp' if root.fs else min(self.width, dp(270))
|
||||||
|
height: self.width if self.fs else (lbl.texture_size[1] + dp(27))
|
||||||
|
on_touch_down: self.hide()
|
||||||
|
BoxLayout:
|
||||||
|
padding: '5dp'
|
||||||
|
Widget:
|
||||||
|
size_hint: None, 1
|
||||||
|
width: '4dp' if root.fs else '2dp'
|
||||||
|
Image:
|
||||||
|
id: img
|
||||||
|
source: root.icon
|
||||||
|
mipmap: True
|
||||||
|
size_hint: None, 1
|
||||||
|
width: (root.width - dp(20)) if root.fs else (0 if not root.icon else '32dp')
|
||||||
|
Label:
|
||||||
|
id: lbl
|
||||||
|
markup: True
|
||||||
|
font_size: '12sp'
|
||||||
|
text: root.message
|
||||||
|
text_size: self.width, None
|
||||||
|
size_hint: None, 1
|
||||||
|
width: 0 if root.fs else (root.width - img.width)
|
||||||
|
|
||||||
|
<-CreateAccountDialog>
|
||||||
|
text_color: .854, .925, .984, 1
|
||||||
|
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
|
||||||
|
FloatLayout:
|
||||||
|
size_hint: None, None
|
||||||
|
size: 0, 0
|
||||||
|
IconButton:
|
||||||
|
id: but_close
|
||||||
|
size_hint: None, None
|
||||||
|
size: '27dp', '27dp'
|
||||||
|
top: Window.height - dp(10)
|
||||||
|
right: Window.width - dp(10)
|
||||||
|
source: 'atlas://gui/kivy/theming/light/closebutton'
|
||||||
|
on_release: root.dispatch('on_press', self)
|
||||||
|
on_release: root.dispatch('on_release', self)
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical' if self.width < self.height else 'horizontal'
|
||||||
|
padding:
|
||||||
|
min(dp(42), self.width/8), min(dp(60), self.height/9.7),\
|
||||||
|
min(dp(42), self.width/8), min(dp(72), self.height/8)
|
||||||
|
spacing: '27dp'
|
||||||
|
GridLayout:
|
||||||
|
id: grid_logo
|
||||||
|
cols: 1
|
||||||
|
pos_hint: {'center_y': .5}
|
||||||
|
size_hint: 1, .62
|
||||||
|
#height: self.minimum_height
|
||||||
|
Image:
|
||||||
|
id: logo_img
|
||||||
|
mipmap: True
|
||||||
|
allow_stretch: True
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '110dp'
|
||||||
|
source: 'atlas://gui/kivy/theming/light/electrum_icon640'
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: 0 if stepper.opacity else dp(15)
|
||||||
|
Label:
|
||||||
|
color: root.text_color
|
||||||
|
opacity: 0 if stepper.opacity else 1
|
||||||
|
text: 'ELECTRUM'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.texture_size[1] if self.opacity else 0
|
||||||
|
font_size: '33sp'
|
||||||
|
font_name: 'data/fonts/tron/Tr2n.ttf'
|
||||||
|
Image:
|
||||||
|
id: stepper
|
||||||
|
allow_stretch: True
|
||||||
|
opacity: 0
|
||||||
|
source: 'atlas://gui/kivy/theming/light/stepper_left'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: grid_logo.height/2.5 if self.opacity else 0
|
||||||
|
Widget:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '5dp'
|
||||||
|
GridLayout:
|
||||||
|
cols: 1
|
||||||
|
id: crcontent
|
||||||
|
spacing: '13dp'
|
||||||
|
|
||||||
|
<CreateRestoreDialog>
|
||||||
|
Label:
|
||||||
|
color: root.text_color
|
||||||
|
size_hint: 1, None
|
||||||
|
text_size: self.width, None
|
||||||
|
height: self.texture_size[1]
|
||||||
|
text:
|
||||||
|
_("Wallet file not found!!")+\
|
||||||
|
"\n\n" + _("Do you want to create a new wallet ")+\
|
||||||
|
_("or restore an existing one?")
|
||||||
|
Widget
|
||||||
|
size_hint: 1, None
|
||||||
|
height: dp(15)
|
||||||
|
GridLayout:
|
||||||
|
id: grid
|
||||||
|
orientation: 'vertical'
|
||||||
|
cols: 1
|
||||||
|
spacing: '14dp'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
CreateAccountButtonGreen:
|
||||||
|
id: create
|
||||||
|
text: _('Create a Wallet')
|
||||||
|
root: root
|
||||||
|
CreateAccountButtonBlue:
|
||||||
|
id: restore
|
||||||
|
text: _('I already have a wallet')
|
||||||
|
root: root
|
||||||
|
#CreateAccountButtonBlue:
|
||||||
|
# id: watching
|
||||||
|
# text: _('Create a Watching only wallet')
|
||||||
|
# root: root
|
||||||
|
|
||||||
|
<InitSeedDialog>
|
||||||
|
spacing: '12dp'
|
||||||
|
GridLayout:
|
||||||
|
id: grid
|
||||||
|
cols: 1
|
||||||
|
pos_hint: {'center_y': .5}
|
||||||
|
size_hint_y: None
|
||||||
|
height: dp(180)
|
||||||
|
orientation: 'vertical'
|
||||||
|
Button:
|
||||||
|
border: 4, 4, 4, 4
|
||||||
|
halign: 'justify'
|
||||||
|
valign: 'middle'
|
||||||
|
font_size: self.width/21
|
||||||
|
text_size: self.width - dp(24), self.height - dp(12)
|
||||||
|
#size_hint: 1, None
|
||||||
|
#height: self.texture_size[1] + dp(24)
|
||||||
|
background_normal: 'atlas://gui/kivy/theming/light/white_bg_round_top'
|
||||||
|
background_down: self.background_normal
|
||||||
|
text: root.message
|
||||||
|
GridLayout:
|
||||||
|
rows: 1
|
||||||
|
size_hint: 1, .7
|
||||||
|
#size_hint_y: None
|
||||||
|
#height: but_seed.texture_size[1] + dp(24)
|
||||||
|
Button:
|
||||||
|
id: but_seed
|
||||||
|
border: 4, 4, 4, 4
|
||||||
|
halign: 'justify'
|
||||||
|
valign: 'middle'
|
||||||
|
font_size: self.width/15
|
||||||
|
text: root.seed_msg
|
||||||
|
text_size: self.width - dp(24), self.height - dp(12)
|
||||||
|
background_normal: 'atlas://gui/kivy/theming/light/lightblue_bg_round_lb'
|
||||||
|
background_down: self.background_normal
|
||||||
|
Button:
|
||||||
|
id: bt
|
||||||
|
size_hint_x: .25
|
||||||
|
background_normal: 'atlas://gui/kivy/theming/light/blue_bg_round_rb'
|
||||||
|
background_down: self.background_normal
|
||||||
|
Image:
|
||||||
|
mipmap: True
|
||||||
|
source: 'atlas://gui/kivy/theming/light/qrcode'
|
||||||
|
size: bt.size
|
||||||
|
center: bt.center
|
||||||
|
#on_release:
|
||||||
|
GridLayout:
|
||||||
|
rows: 1
|
||||||
|
spacing: '12dp'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
CreateAccountButtonBlue:
|
||||||
|
id: back
|
||||||
|
text: _('Back')
|
||||||
|
root: root
|
||||||
|
CreateAccountButtonGreen:
|
||||||
|
id: confirm
|
||||||
|
text: _('Confirm')
|
||||||
|
root: root
|
||||||
|
|
||||||
|
<ChangePasswordDialog>
|
||||||
|
padding: '7dp'
|
||||||
|
CreateAccountTextInput:
|
||||||
|
id: ti_wallet_name
|
||||||
|
hint_text: 'Your Wallet Name'
|
||||||
|
multiline: False
|
||||||
|
on_text_validate:
|
||||||
|
next = ti_new_password if ti_password.disabled else ti_password
|
||||||
|
next.focus = True
|
||||||
|
CreateAccountTextInput:
|
||||||
|
id: ti_password
|
||||||
|
hint_text: 'Enter old pincode'
|
||||||
|
size_hint_y: None
|
||||||
|
height: 0 if self.disabled else '38sp'
|
||||||
|
password: True
|
||||||
|
disabled: True if root.mode in ('new', 'create') else False
|
||||||
|
opacity: 0 if self.disabled else 1
|
||||||
|
multiline: False
|
||||||
|
on_text_validate:
|
||||||
|
#root.validate_old_password()
|
||||||
|
ti_new_password.focus = True
|
||||||
|
CreateAccountTextInput:
|
||||||
|
id: ti_new_password
|
||||||
|
hint_text: 'Enter new pincode'
|
||||||
|
multiline: False
|
||||||
|
password: True
|
||||||
|
on_text_validate: ti_confirm_password.focus = True
|
||||||
|
CreateAccountTextInput:
|
||||||
|
id: ti_confirm_password
|
||||||
|
hint_text: 'Confirm pincode'
|
||||||
|
password: True
|
||||||
|
multiline: False
|
||||||
|
on_text_validate: root.validate_new_passowrd()
|
||||||
|
Widget
|
||||||
|
GridLayout:
|
||||||
|
rows: 1
|
||||||
|
spacing: '12dp'
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
CreateAccountButtonBlue:
|
||||||
|
id: back
|
||||||
|
text: _('Back')
|
||||||
|
root: root
|
||||||
|
CreateAccountButtonGreen:
|
||||||
|
id: next
|
||||||
|
text: _('Next')
|
||||||
|
root: root
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
## Wallet Management
|
||||||
|
###############################################
|
||||||
|
|
||||||
|
<WalletManagement@ScrollView>
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: .145, .145, .145, 1
|
||||||
|
Rectangle:
|
||||||
|
size: root.size
|
||||||
|
pos: root.pos
|
||||||
|
VGridLayout:
|
||||||
|
Wallets:
|
||||||
|
id: wallets_section
|
||||||
|
Plugins:
|
||||||
|
id: plugins_section
|
||||||
|
Commands:
|
||||||
|
id: commands_section
|
||||||
|
|
||||||
|
<WalletManagementItem@BoxLayout>
|
||||||
|
|
||||||
|
<Header@WalletManagementItem>
|
||||||
|
|
||||||
|
<Wallets@VGridLayout>
|
||||||
|
Header
|
||||||
|
|
||||||
|
<Plugins@VGridLayout>
|
||||||
|
Header
|
||||||
|
|
||||||
|
<Commands@VGridLayout>
|
||||||
|
Header
|
||||||
|
|
||||||
|
################################################
|
||||||
|
## This is our Root Widget of the app
|
||||||
|
################################################
|
||||||
|
StencilView
|
||||||
|
manager: manager
|
||||||
|
Drawer
|
||||||
|
id: drawer
|
||||||
|
size: root.size
|
||||||
|
WalletManagement
|
||||||
|
id: wallet_management
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
rgba: .176, .176, .176, 1
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
canvas.after:
|
||||||
|
Color
|
||||||
|
rgba: 1, 1, 1, 1
|
||||||
|
BorderImage
|
||||||
|
border: 0, 32, 0, 0
|
||||||
|
source: 'atlas://gui/kivy/theming/light/shadow_right'
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
width:
|
||||||
|
(root.width * .877) if app.ui_mode[0] == 'p'\
|
||||||
|
else root.width * .35 if app.orientation[0] == 'l'\
|
||||||
|
else root.width * .10
|
||||||
|
height: root.height
|
||||||
|
ScreenManager:
|
||||||
|
id: manager
|
||||||
|
x: wallet_management.width if app.ui_mode[0] == 't' else 0
|
||||||
|
size: root.size
|
||||||
|
canvas.before:
|
||||||
|
Color
|
||||||
|
rgba: 1, 1, 1, 1
|
||||||
|
BorderImage:
|
||||||
|
border: 2, 2, 2, 23
|
||||||
|
size: self.size
|
||||||
|
pos: self.x, self.y
|
||||||
294
gui/kivy/main_window.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from electrum import WalletStorage, Wallet
|
||||||
|
from electrum.i18n import _
|
||||||
|
|
||||||
|
from kivy.app import App
|
||||||
|
from kivy.core.window import Window
|
||||||
|
from kivy.metrics import inch
|
||||||
|
from kivy.logger import Logger
|
||||||
|
from kivy.utils import platform
|
||||||
|
from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty,
|
||||||
|
StringProperty, ListProperty)
|
||||||
|
|
||||||
|
#inclusions for factory so that widgets can be used in kv
|
||||||
|
from gui.kivy.drawer import Drawer
|
||||||
|
from gui.kivy.dialog import InfoBubble
|
||||||
|
|
||||||
|
class ElectrumWindow(App):
|
||||||
|
|
||||||
|
title = _('Electrum App')
|
||||||
|
|
||||||
|
wallet = ObjectProperty(None)
|
||||||
|
'''Holds the electrum wallet
|
||||||
|
|
||||||
|
:attr:`wallet` is a `ObjectProperty` defaults to None.
|
||||||
|
'''
|
||||||
|
|
||||||
|
conf = ObjectProperty(None)
|
||||||
|
'''Holds the electrum config
|
||||||
|
|
||||||
|
:attr:`conf` 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'
|
||||||
|
'''
|
||||||
|
|
||||||
|
base_unit = StringProperty('BTC')
|
||||||
|
'''BTC or UBTC or ...
|
||||||
|
|
||||||
|
:attr:`base_unit` is a `StringProperty` defaults to 'BTC'
|
||||||
|
'''
|
||||||
|
|
||||||
|
_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
|
||||||
|
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
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
from kivy.lang import Builder
|
||||||
|
return Builder.load_file('gui/kivy/main.kv')
|
||||||
|
|
||||||
|
def _pause(self):
|
||||||
|
if platform == 'android':
|
||||||
|
from jnius import autoclass
|
||||||
|
python_act = autoclass('org.renpy.android.PythonActivity')
|
||||||
|
mActivity = python_act.mActivity
|
||||||
|
mActivity.moveTaskToBack(True)
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
Window.bind(size=self.on_size,
|
||||||
|
on_keyboard=self.on_keyboard)
|
||||||
|
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_back(self):
|
||||||
|
''' Manage screen higherarchy
|
||||||
|
'''
|
||||||
|
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_keyboard(self, instance, key, keycode, codepoint, modifiers):
|
||||||
|
# override settings button
|
||||||
|
if key in (319, 282):
|
||||||
|
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...')
|
||||||
|
self.stop()
|
||||||
|
sys.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# plugins that need to change the GUI do it here
|
||||||
|
#run_hook('init')
|
||||||
|
|
||||||
|
self.load_wallet(wallet)
|
||||||
|
|
||||||
|
Clock.schedule_once(update_wallet)
|
||||||
|
|
||||||
|
#self.windows.append(w)
|
||||||
|
#if url: w.set_url(url)
|
||||||
|
#w.app = self.app
|
||||||
|
#w.connect_slots(s)
|
||||||
|
#w.update_wallet()
|
||||||
|
|
||||||
|
#self.app.exec_()
|
||||||
|
|
||||||
|
wallet.stop_threads()
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
def load_screen(self, index=0, direction='left'):
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
screen = Builder.load_file('data/screens/' + self.screens[index])
|
||||||
|
screen.name = self.screens[index]
|
||||||
|
root.manager.switch_to(screen, direction=direction)
|
||||||
|
|
||||||
|
def load_next_screen(self):
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
manager = root.manager
|
||||||
|
try:
|
||||||
|
self.load_screen(self.screens.index(manager.current_screen.name)+1)
|
||||||
|
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,
|
||||||
|
direction='right')
|
||||||
|
except IndexError:
|
||||||
|
self.load_screen(-1, direction='right')
|
||||||
|
|
||||||
|
def show_error(self, error,
|
||||||
|
width='200dp',
|
||||||
|
pos=None,
|
||||||
|
arrow_pos=None):
|
||||||
|
''' Show a error Message Bubble.
|
||||||
|
'''
|
||||||
|
self.show_info_bubble(
|
||||||
|
text=error,
|
||||||
|
icon='atlas://gui/kivy/theming/light/error',
|
||||||
|
width=width,
|
||||||
|
pos=pos or Window.center,
|
||||||
|
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):
|
||||||
|
'''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:
|
||||||
|
info_bubble.hide()
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
95
gui/kivy/menus.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from kivy.animation import Animation
|
||||||
|
from kivy.core.window import Window
|
||||||
|
from kivy.clock import Clock
|
||||||
|
from kivy.uix.bubble import Bubble, BubbleButton
|
||||||
|
from kivy.properties import ListProperty
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
|
||||||
|
from electrum_gui.i18n import _
|
||||||
|
|
||||||
|
class ContextMenuItem(Widget):
|
||||||
|
'''abstract class
|
||||||
|
'''
|
||||||
|
|
||||||
|
class ContextButton(ContextMenuItem, BubbleButton):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ContextMenu(Bubble):
|
||||||
|
|
||||||
|
buttons = ListProperty([_('ok'), _('cancel')])
|
||||||
|
'''List of Buttons to be displayed at the bottom'''
|
||||||
|
|
||||||
|
__events__ = ('on_press', 'on_release')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._old_buttons = self.buttons
|
||||||
|
super(ContextMenu, self).__init__(**kwargs)
|
||||||
|
self.on_buttons(self, self.buttons)
|
||||||
|
|
||||||
|
def on_touch_down(self, touch):
|
||||||
|
if not self.collide_point(*touch.pos):
|
||||||
|
self.hide()
|
||||||
|
return
|
||||||
|
return super(ContextMenu, self).on_touch_down(touch)
|
||||||
|
|
||||||
|
def on_buttons(self, _menu, value):
|
||||||
|
if 'menu_content' not in self.ids.keys():
|
||||||
|
return
|
||||||
|
if value == self._old_buttons:
|
||||||
|
return
|
||||||
|
blayout = self.ids.menu_content
|
||||||
|
blayout.clear_widgets()
|
||||||
|
for btn in value:
|
||||||
|
ib = ContextButton(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
|
||||||
|
|
||||||
|
def on_press(self, instance):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_release(self, instance):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def show(self, pos, duration=0):
|
||||||
|
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, *dt):
|
||||||
|
|
||||||
|
def on_stop(*l):
|
||||||
|
Window.remove_widget(self)
|
||||||
|
anim = Animation(opacity=0, d=.25)
|
||||||
|
anim.bind(on_complete=on_stop)
|
||||||
|
anim.cancel_all(self)
|
||||||
|
anim.start(self)
|
||||||
|
|
||||||
|
def add_widget(self, widget, index=0):
|
||||||
|
if not isinstance(widget, ContextMenuItem):
|
||||||
|
super(ContextMenu, self).add_widget(widget, index)
|
||||||
|
return
|
||||||
|
menu_content.add_widget(widget, index)
|
||||||
43
gui/kivy/nfc_scanner/__init__.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'''
|
||||||
|
'''
|
||||||
|
from kivy.core import core_select_lib
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
|
from kivy.factory import Factory
|
||||||
|
|
||||||
|
__all__ = ('NFCBase', 'NFCScanner')
|
||||||
|
|
||||||
|
class NFCBase(Widget):
|
||||||
|
|
||||||
|
payload = ObjectProperty(None)
|
||||||
|
|
||||||
|
def nfc_init(self):
|
||||||
|
''' Initialize the adapter
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nfc_disable(self):
|
||||||
|
''' Disable scanning
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nfc_enable(self):
|
||||||
|
''' Enable Scanning
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nfc_enable_exchange(self, data):
|
||||||
|
''' Start sending data
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def nfc_disable_exchange(self):
|
||||||
|
''' Disable/Stop ndef exchange
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
# load NFCScanner implementation
|
||||||
|
|
||||||
|
NFCScanner = core_select_lib('nfc_scanner', (
|
||||||
|
('android', 'scanner_android', 'ScannerAndroid'),
|
||||||
|
('dummy', 'scanner_dummy', 'ScannerDummy')), True, 'electrum_gui.kivy')
|
||||||
86
gui/kivy/nfc_scanner/scanner_android.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from kivy.utils import platform
|
||||||
|
if platform != 'android':
|
||||||
|
raise ImportError
|
||||||
|
|
||||||
|
from electrum_gui.kivy.nfc_scanner import NFCBase
|
||||||
|
from jnius import autoclass, cast
|
||||||
|
from android.runnable import run_on_ui_thread
|
||||||
|
from android import activity
|
||||||
|
|
||||||
|
NfcAdapter = autoclass('android.nfc.NfcAdapter')
|
||||||
|
PythonActivity = autoclass('org.renpy.android.PythonActivity')
|
||||||
|
Intent = autoclass('android.content.Intent')
|
||||||
|
IntentFilter = autoclass('android.content.IntentFilter')
|
||||||
|
PendingIntent = autoclass('android.app.PendingIntent')
|
||||||
|
NdefRecord = autoclass('android.nfc.NdefRecord')
|
||||||
|
NdefMessage = autoclass('android.nfc.NdefMessage')
|
||||||
|
|
||||||
|
class ScannerAndroid(NFCBase):
|
||||||
|
|
||||||
|
def nfc_init(self):
|
||||||
|
# print 'nfc_init()'
|
||||||
|
|
||||||
|
# print 'configure nfc'
|
||||||
|
self.j_context = context = PythonActivity.mActivity
|
||||||
|
self.nfc_adapter = NfcAdapter.getDefaultAdapter(context)
|
||||||
|
self.nfc_pending_intent = PendingIntent.getActivity(context, 0,
|
||||||
|
Intent(context, context.getClass()).addFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
|
||||||
|
|
||||||
|
# print 'p2p filter'
|
||||||
|
self.ndef_detected = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
|
||||||
|
self.ndef_detected.addDataType('text/plain')
|
||||||
|
self.ndef_exchange_filters = [self.ndef_detected]
|
||||||
|
|
||||||
|
def on_new_intent(self, intent):
|
||||||
|
# print 'on_new_intent()', intent.getAction()
|
||||||
|
if intent.getAction() != NfcAdapter.ACTION_NDEF_DISCOVERED:
|
||||||
|
# print 'unknow action, avoid.'
|
||||||
|
return
|
||||||
|
|
||||||
|
rawmsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||||
|
# print 'raw messages', rawmsgs
|
||||||
|
if not rawmsgs:
|
||||||
|
return
|
||||||
|
|
||||||
|
for message in rawmsgs:
|
||||||
|
message = cast(NdefMessage, message)
|
||||||
|
# print 'got message', message
|
||||||
|
payload = message.getRecords()[0].getPayload()
|
||||||
|
self.payload = payload
|
||||||
|
print 'payload: {}'.format(''.join(map(chr, payload)))
|
||||||
|
|
||||||
|
def nfc_disable(self):
|
||||||
|
# print 'nfc_enable()'
|
||||||
|
activity.bind(on_new_intent=self.on_new_intent)
|
||||||
|
|
||||||
|
def nfc_enable(self):
|
||||||
|
# print 'nfc_enable()'
|
||||||
|
activity.bind(on_new_intent=self.on_new_intent)
|
||||||
|
|
||||||
|
@run_on_ui_thread
|
||||||
|
def _nfc_enable_ndef_exchange(self, data):
|
||||||
|
# print 'create record'
|
||||||
|
ndef_record = NdefRecord(
|
||||||
|
NdefRecord.TNF_MIME_MEDIA,
|
||||||
|
'text/plain', '', data)
|
||||||
|
# print 'create message'
|
||||||
|
ndef_message = NdefMessage([ndef_record])
|
||||||
|
|
||||||
|
# print 'enable ndef push'
|
||||||
|
self.nfc_adapter.enableForegroundNdefPush(self.j_context, ndef_message)
|
||||||
|
|
||||||
|
# print 'enable dispatch', self.j_context, self.nfc_pending_intent
|
||||||
|
self.nfc_adapter.enableForegroundDispatch(self.j_context,
|
||||||
|
self.nfc_pending_intent, self.ndef_exchange_filters, [])
|
||||||
|
|
||||||
|
@run_on_ui_thread
|
||||||
|
def _nfc_disable_ndef_exchange(self):
|
||||||
|
self.nfc_adapter.disableForegroundNdefPush(self.j_context)
|
||||||
|
self.nfc_adapter.disableForegroundDispatch(self.j_context)
|
||||||
|
|
||||||
|
def nfc_enable_exchange(self, data):
|
||||||
|
self._nfc_enable_ndef_exchange()
|
||||||
|
|
||||||
|
def nfc_disable_exchange(self):
|
||||||
|
self._nfc_disable_ndef_exchange()
|
||||||
37
gui/kivy/nfc_scanner/scanner_dummy.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
''' Dummy NFC Provider to be used on desktops in case no other provider is found
|
||||||
|
'''
|
||||||
|
from electrum_gui.kivy.nfc_scanner import NFCBase
|
||||||
|
from kivy.clock import Clock
|
||||||
|
from kivy.logger import Logger
|
||||||
|
|
||||||
|
class ScannerDummy(NFCBase):
|
||||||
|
|
||||||
|
_initialised = False
|
||||||
|
|
||||||
|
def nfc_init(self):
|
||||||
|
# print 'nfc_init()'
|
||||||
|
|
||||||
|
Logger.debug('NFC: configure nfc')
|
||||||
|
self._initialised = True
|
||||||
|
|
||||||
|
def on_new_intent(self, dt):
|
||||||
|
Logger.debug('NFC: got new dummy tag')
|
||||||
|
|
||||||
|
def nfc_enable(self):
|
||||||
|
Logger.debug('NFC: enable')
|
||||||
|
if self._initialised:
|
||||||
|
Clock.schedule_interval(self.on_new_intent, 22)
|
||||||
|
|
||||||
|
def nfc_disable(self):
|
||||||
|
# print 'nfc_enable()'
|
||||||
|
Clock.unschedule(self.on_new_intent)
|
||||||
|
|
||||||
|
def nfc_enable_exchange(self, data):
|
||||||
|
''' Start sending data
|
||||||
|
'''
|
||||||
|
Logger.debug('NFC: sending data {}'.format(data))
|
||||||
|
|
||||||
|
def nfc_disable_exchange(self):
|
||||||
|
''' Disable/Stop ndef exchange
|
||||||
|
'''
|
||||||
|
Logger.debug('NFC: disable nfc exchange')
|
||||||
105
gui/kivy/qr_scanner/__init__.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
'''QrScanner Base Abstract implementation
|
||||||
|
'''
|
||||||
|
|
||||||
|
__all__ = ('ScannerBase', 'QRScanner')
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from kivy.uix.anchorlayout import AnchorLayout
|
||||||
|
from kivy.core import core_select_lib
|
||||||
|
from kivy.properties import ListProperty, BooleanProperty
|
||||||
|
from kivy.factory import Factory
|
||||||
|
|
||||||
|
|
||||||
|
def encode_uri(addr, amount=0, label='', message='', size='',
|
||||||
|
currency='btc'):
|
||||||
|
''' Convert to BIP0021 compatible URI
|
||||||
|
'''
|
||||||
|
uri = 'bitcoin:{}'.format(addr)
|
||||||
|
first = True
|
||||||
|
if amount:
|
||||||
|
uri += '{}amount={}'.format('?' if first else '&', amount)
|
||||||
|
first = False
|
||||||
|
if label:
|
||||||
|
uri += '{}label={}'.format('?' if first else '&', label)
|
||||||
|
first = False
|
||||||
|
if message:
|
||||||
|
uri += '{}?message={}'.format('?' if first else '&', message)
|
||||||
|
first = False
|
||||||
|
if size:
|
||||||
|
uri += '{}size={}'.format('?' if not first else '&', size)
|
||||||
|
return uri
|
||||||
|
|
||||||
|
def decode_uri(uri):
|
||||||
|
if ':' not in uri:
|
||||||
|
# It's just an address (not BIP21)
|
||||||
|
return {'address': uri}
|
||||||
|
|
||||||
|
if '//' not in uri:
|
||||||
|
# Workaround for urlparse, it don't handle bitcoin: URI properly
|
||||||
|
uri = uri.replace(':', '://')
|
||||||
|
|
||||||
|
try:
|
||||||
|
uri = urlparse(uri)
|
||||||
|
except NameError:
|
||||||
|
# delayed import
|
||||||
|
from urlparse import urlparse, parse_qs
|
||||||
|
uri = urlparse(uri)
|
||||||
|
|
||||||
|
result = {'address': uri.netloc}
|
||||||
|
|
||||||
|
if uri.path.startswith('?'):
|
||||||
|
params = parse_qs(uri.path[1:])
|
||||||
|
else:
|
||||||
|
params = parse_qs(uri.path)
|
||||||
|
|
||||||
|
for k,v in params.items():
|
||||||
|
if k in ('amount', 'label', 'message', 'size'):
|
||||||
|
result[k] = v[0]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ScannerBase(AnchorLayout):
|
||||||
|
''' Base implementation for camera based scanner
|
||||||
|
'''
|
||||||
|
camera_size = ListProperty([320, 240])
|
||||||
|
|
||||||
|
symbols = ListProperty([])
|
||||||
|
|
||||||
|
# XXX can't work now, due to overlay.
|
||||||
|
show_bounds = BooleanProperty(False)
|
||||||
|
|
||||||
|
Qrcode = namedtuple('Qrcode',
|
||||||
|
['type', 'data', 'bounds', 'quality', 'count'])
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_symbols(self, instance, value):
|
||||||
|
#if self.show_bounds:
|
||||||
|
# self.update_bounds()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_bounds(self):
|
||||||
|
self.canvas.after.remove_group('bounds')
|
||||||
|
if not self.symbols:
|
||||||
|
return
|
||||||
|
with self.canvas.after:
|
||||||
|
Color(1, 0, 0, group='bounds')
|
||||||
|
for symbol in self.symbols:
|
||||||
|
x, y, w, h = symbol.bounds
|
||||||
|
x = self._camera.right - x - w
|
||||||
|
y = self._camera.top - y - h
|
||||||
|
Line(rectangle=[x, y, w, h], group='bounds')
|
||||||
|
|
||||||
|
|
||||||
|
# load QRCodeDetector implementation
|
||||||
|
|
||||||
|
QRScanner = core_select_lib('qr_scanner', (
|
||||||
|
('android', 'scanner_android', 'ScannerAndroid'),
|
||||||
|
('camera', 'scanner_camera', 'ScannerCamera')), False, 'electrum_gui.kivy')
|
||||||
|
Factory.register('QRScanner', cls=QRScanner)
|
||||||
354
gui/kivy/qr_scanner/scanner_android.py
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
'''
|
||||||
|
Qrcode example application
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Author: Mathieu Virbel <mat@meltingrocks.com>
|
||||||
|
|
||||||
|
License:
|
||||||
|
Copyright (c) 2013 Mathieu Virbel <mat@meltingrocks.com>
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Featuring:
|
||||||
|
|
||||||
|
- Android camera initialization
|
||||||
|
- Show the android camera into a Android surface that act as an overlay
|
||||||
|
- New AndroidWidgetHolder that control any android view as an overlay
|
||||||
|
- New ZbarQrcodeDetector that use AndroidCamera / PreviewFrame + zbar to
|
||||||
|
detect Qrcode.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
__all__ = ('ScannerAndroid', )
|
||||||
|
|
||||||
|
from kivy.utils import platform
|
||||||
|
if platform != 'android':
|
||||||
|
raise ImportError
|
||||||
|
|
||||||
|
from electrum_gui.kivy.qr_scanner import ScannerBase
|
||||||
|
from kivy.properties import ObjectProperty, NumericProperty
|
||||||
|
from kivy.uix.widget import Widget
|
||||||
|
from kivy.uix.anchorlayout import AnchorLayout
|
||||||
|
from kivy.graphics import Color, Line
|
||||||
|
from jnius import autoclass, PythonJavaClass, java_method, cast
|
||||||
|
from android.runnable import run_on_ui_thread
|
||||||
|
|
||||||
|
# preload java classes
|
||||||
|
System = autoclass('java.lang.System')
|
||||||
|
System.loadLibrary('iconv')
|
||||||
|
PythonActivity = autoclass('org.renpy.android.PythonActivity')
|
||||||
|
Camera = autoclass('android.hardware.Camera')
|
||||||
|
ImageScanner = autoclass('net.sourceforge.zbar.ImageScanner')
|
||||||
|
Image = autoclass('net.sourceforge.zbar.Image')
|
||||||
|
Symbol = autoclass('net.sourceforge.zbar.Symbol')
|
||||||
|
Config = autoclass('net.sourceforge.zbar.Config')
|
||||||
|
SurfaceView = autoclass('android.view.SurfaceView')
|
||||||
|
LayoutParams = autoclass('android.view.ViewGroup$LayoutParams')
|
||||||
|
ImageFormat = autoclass('android.graphics.ImageFormat')
|
||||||
|
LinearLayout = autoclass('android.widget.LinearLayout')
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewCallback(PythonJavaClass):
|
||||||
|
'''Interface used to get back the preview frame of the Android Camera
|
||||||
|
'''
|
||||||
|
__javainterfaces__ = ('android.hardware.Camera$PreviewCallback', )
|
||||||
|
|
||||||
|
def __init__(self, callback):
|
||||||
|
super(PreviewCallback, self).__init__()
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
@java_method('([BLandroid/hardware/Camera;)V')
|
||||||
|
def onPreviewFrame(self, data, camera):
|
||||||
|
self.callback(camera, data)
|
||||||
|
|
||||||
|
|
||||||
|
class SurfaceHolderCallback(PythonJavaClass):
|
||||||
|
'''Interface used to know exactly when the Surface used for the Android
|
||||||
|
Camera will be created and changed.
|
||||||
|
'''
|
||||||
|
|
||||||
|
__javainterfaces__ = ('android.view.SurfaceHolder$Callback', )
|
||||||
|
|
||||||
|
def __init__(self, callback):
|
||||||
|
super(SurfaceHolderCallback, self).__init__()
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
@java_method('(Landroid/view/SurfaceHolder;III)V')
|
||||||
|
def surfaceChanged(self, surface, fmt, width, height):
|
||||||
|
self.callback(fmt, width, height)
|
||||||
|
|
||||||
|
@java_method('(Landroid/view/SurfaceHolder;)V')
|
||||||
|
def surfaceCreated(self, surface):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@java_method('(Landroid/view/SurfaceHolder;)V')
|
||||||
|
def surfaceDestroyed(self, surface):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidWidgetHolder(Widget):
|
||||||
|
'''Act as a placeholder for an Android widget.
|
||||||
|
It will automatically add / remove the android view depending if the widget
|
||||||
|
view is set or not. The android view will act as an overlay, so any graphics
|
||||||
|
instruction in this area will be covered by the overlay.
|
||||||
|
'''
|
||||||
|
|
||||||
|
view = ObjectProperty(allownone=True)
|
||||||
|
'''Must be an Android View
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._old_view = None
|
||||||
|
from kivy.core.window import Window
|
||||||
|
self._window = Window
|
||||||
|
kwargs['size_hint'] = (None, None)
|
||||||
|
super(AndroidWidgetHolder, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def on_view(self, instance, view):
|
||||||
|
if self._old_view is not None:
|
||||||
|
layout = cast(LinearLayout, self._old_view.getParent())
|
||||||
|
layout.removeView(self._old_view)
|
||||||
|
self._old_view = None
|
||||||
|
|
||||||
|
if view is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
activity = PythonActivity.mActivity
|
||||||
|
activity.addContentView(view, LayoutParams(*self.size))
|
||||||
|
view.setZOrderOnTop(True)
|
||||||
|
view.setX(self.x)
|
||||||
|
view.setY(self._window.height - self.y - self.height)
|
||||||
|
self._old_view = view
|
||||||
|
|
||||||
|
def on_size(self, instance, size):
|
||||||
|
if self.view:
|
||||||
|
params = self.view.getLayoutParams()
|
||||||
|
params.width = self.width
|
||||||
|
params.height = self.height
|
||||||
|
self.view.setLayoutParams(params)
|
||||||
|
self.view.setY(self._window.height - self.y - self.height)
|
||||||
|
|
||||||
|
def on_x(self, instance, x):
|
||||||
|
if self.view:
|
||||||
|
self.view.setX(x)
|
||||||
|
|
||||||
|
def on_y(self, instance, y):
|
||||||
|
if self.view:
|
||||||
|
self.view.setY(self._window.height - self.y - self.height)
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidCamera(Widget):
|
||||||
|
'''Widget for controling an Android Camera.
|
||||||
|
'''
|
||||||
|
|
||||||
|
index = NumericProperty(0)
|
||||||
|
|
||||||
|
__events__ = ('on_preview_frame', )
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._holder = None
|
||||||
|
self._android_camera = None
|
||||||
|
super(AndroidCamera, self).__init__(**kwargs)
|
||||||
|
self._holder = AndroidWidgetHolder(size=self.size, pos=self.pos)
|
||||||
|
self.add_widget(self._holder)
|
||||||
|
|
||||||
|
@run_on_ui_thread
|
||||||
|
def stop(self):
|
||||||
|
if self._android_camera is None:
|
||||||
|
return
|
||||||
|
self._android_camera.setPreviewCallback(None)
|
||||||
|
self._android_camera.release()
|
||||||
|
self._android_camera = None
|
||||||
|
self._holder.view = None
|
||||||
|
|
||||||
|
@run_on_ui_thread
|
||||||
|
def start(self):
|
||||||
|
if self._android_camera is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._android_camera = Camera.open(self.index)
|
||||||
|
|
||||||
|
# create a fake surfaceview to get the previewCallback working.
|
||||||
|
self._android_surface = SurfaceView(PythonActivity.mActivity)
|
||||||
|
surface_holder = self._android_surface.getHolder()
|
||||||
|
|
||||||
|
# create our own surface holder to correctly call the next method when
|
||||||
|
# the surface is ready
|
||||||
|
self._android_surface_cb = SurfaceHolderCallback(self._on_surface_changed)
|
||||||
|
surface_holder.addCallback(self._android_surface_cb)
|
||||||
|
|
||||||
|
# attach the android surfaceview to our android widget holder
|
||||||
|
self._holder.view = self._android_surface
|
||||||
|
|
||||||
|
def _on_surface_changed(self, fmt, width, height):
|
||||||
|
# internal, called when the android SurfaceView is ready
|
||||||
|
# FIXME if the size is not handled by the camera, it will failed.
|
||||||
|
params = self._android_camera.getParameters()
|
||||||
|
params.setPreviewSize(width, height)
|
||||||
|
self._android_camera.setParameters(params)
|
||||||
|
|
||||||
|
# now that we know the camera size, we'll create 2 buffers for faster
|
||||||
|
# result (using Callback buffer approach, as described in Camera android
|
||||||
|
# documentation)
|
||||||
|
# it also reduce the GC collection
|
||||||
|
bpp = ImageFormat.getBitsPerPixel(params.getPreviewFormat()) / 8.
|
||||||
|
buf = '\x00' * int(width * height * bpp)
|
||||||
|
self._android_camera.addCallbackBuffer(buf)
|
||||||
|
self._android_camera.addCallbackBuffer(buf)
|
||||||
|
|
||||||
|
# create a PreviewCallback to get back the onPreviewFrame into python
|
||||||
|
self._previewCallback = PreviewCallback(self._on_preview_frame)
|
||||||
|
|
||||||
|
# connect everything and start the preview
|
||||||
|
self._android_camera.setPreviewCallbackWithBuffer(self._previewCallback);
|
||||||
|
self._android_camera.setPreviewDisplay(self._android_surface.getHolder())
|
||||||
|
self._android_camera.startPreview();
|
||||||
|
|
||||||
|
def _on_preview_frame(self, camera, data):
|
||||||
|
# internal, called by the PreviewCallback when onPreviewFrame is
|
||||||
|
# received
|
||||||
|
self.dispatch('on_preview_frame', camera, data)
|
||||||
|
# reintroduce the data buffer into the queue
|
||||||
|
self._android_camera.addCallbackBuffer(data)
|
||||||
|
|
||||||
|
def on_preview_frame(self, camera, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_size(self, instance, size):
|
||||||
|
if self._holder:
|
||||||
|
self._holder.size = size
|
||||||
|
|
||||||
|
def on_pos(self, instance, pos):
|
||||||
|
if self._holder:
|
||||||
|
self._holder.pos = pos
|
||||||
|
|
||||||
|
|
||||||
|
class ScannerAndroid(ScannerBase):
|
||||||
|
'''Widget that use the AndroidCamera and zbar to detect qrcode.
|
||||||
|
When found, the `symbols` will be updated
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(ScannerAndroid, self).__init__(**kwargs)
|
||||||
|
self._camera = AndroidCamera(
|
||||||
|
size=self.camera_size,
|
||||||
|
size_hint=(None, None))
|
||||||
|
self._camera.bind(on_preview_frame=self._detect_qrcode_frame)
|
||||||
|
self.add_widget(self._camera)
|
||||||
|
|
||||||
|
# create a scanner used for detecting qrcode
|
||||||
|
self._scanner = ImageScanner()
|
||||||
|
self._scanner.setConfig(0, Config.ENABLE, 0)
|
||||||
|
self._scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1)
|
||||||
|
self._scanner.setConfig(0, Config.X_DENSITY, 3)
|
||||||
|
self._scanner.setConfig(0, Config.Y_DENSITY, 3)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._camera.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._camera.stop()
|
||||||
|
|
||||||
|
def _detect_qrcode_frame(self, instance, camera, data):
|
||||||
|
# the image we got by default from a camera is using the NV21 format
|
||||||
|
# zbar only allow Y800/GREY image, so we first need to convert,
|
||||||
|
# then start the detection on the image
|
||||||
|
if not self.get_root_window():
|
||||||
|
self.stop()
|
||||||
|
return
|
||||||
|
parameters = camera.getParameters()
|
||||||
|
size = parameters.getPreviewSize()
|
||||||
|
barcode = Image(size.width, size.height, 'NV21')
|
||||||
|
barcode.setData(data)
|
||||||
|
barcode = barcode.convert('Y800')
|
||||||
|
|
||||||
|
result = self._scanner.scanImage(barcode)
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
self.symbols = []
|
||||||
|
return
|
||||||
|
|
||||||
|
# we detected qrcode! extract and dispatch them
|
||||||
|
symbols = []
|
||||||
|
it = barcode.getSymbols().iterator()
|
||||||
|
while it.hasNext():
|
||||||
|
symbol = it.next()
|
||||||
|
qrcode = ScannerAndroid.Qrcode(
|
||||||
|
type=symbol.getType(),
|
||||||
|
data=symbol.getData(),
|
||||||
|
quality=symbol.getQuality(),
|
||||||
|
count=symbol.getCount(),
|
||||||
|
bounds=symbol.getBounds())
|
||||||
|
symbols.append(qrcode)
|
||||||
|
|
||||||
|
self.symbols = symbols
|
||||||
|
|
||||||
|
'''
|
||||||
|
# can't work, due to the overlay.
|
||||||
|
def on_symbols(self, instance, value):
|
||||||
|
if self.show_bounds:
|
||||||
|
self.update_bounds()
|
||||||
|
|
||||||
|
def update_bounds(self):
|
||||||
|
self.canvas.after.remove_group('bounds')
|
||||||
|
if not self.symbols:
|
||||||
|
return
|
||||||
|
with self.canvas.after:
|
||||||
|
Color(1, 0, 0, group='bounds')
|
||||||
|
for symbol in self.symbols:
|
||||||
|
x, y, w, h = symbol.bounds
|
||||||
|
x = self._camera.right - x - w
|
||||||
|
y = self._camera.top - y - h
|
||||||
|
Line(rectangle=[x, y, w, h], group='bounds')
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.app import App
|
||||||
|
|
||||||
|
qrcode_kv = '''
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
|
||||||
|
ZbarQrcodeDetector:
|
||||||
|
id: detector
|
||||||
|
|
||||||
|
Label:
|
||||||
|
text: '\\n'.join(map(repr, detector.symbols))
|
||||||
|
size_hint_y: None
|
||||||
|
height: '100dp'
|
||||||
|
|
||||||
|
BoxLayout:
|
||||||
|
size_hint_y: None
|
||||||
|
height: '48dp'
|
||||||
|
|
||||||
|
Button:
|
||||||
|
text: 'Scan a qrcode'
|
||||||
|
on_release: detector.start()
|
||||||
|
Button:
|
||||||
|
text: 'Stop detection'
|
||||||
|
on_release: detector.stop()
|
||||||
|
'''
|
||||||
|
|
||||||
|
class QrcodeExample(App):
|
||||||
|
def build(self):
|
||||||
|
return Builder.load_string(qrcode_kv)
|
||||||
|
|
||||||
|
QrcodeExample().run()
|
||||||
89
gui/kivy/qr_scanner/scanner_camera.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from kivy.uix.camera import Camera
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
import iconv
|
||||||
|
from electrum_gui.kivy.qr_scanner import ScannerBase
|
||||||
|
try:
|
||||||
|
from zbar import ImageScanner, Config, Image, Symbol
|
||||||
|
except ImportError:
|
||||||
|
raise SystemError('unable to import zbar please make sure you have it installed')
|
||||||
|
try:
|
||||||
|
import Image as PILImage
|
||||||
|
except ImportError:
|
||||||
|
raise SystemError('unable to import Pil/pillow please install one of the two.')
|
||||||
|
|
||||||
|
__all__ = ('ScannerCamera', )
|
||||||
|
|
||||||
|
class ScannerCamera(ScannerBase):
|
||||||
|
'''Widget that use the kivy.uix.camera.Camera and zbar to detect qrcode.
|
||||||
|
When found, the `symbols` will be updated
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(ScannerCamera, self).__init__(**kwargs)
|
||||||
|
self._camera = None
|
||||||
|
# create a scanner used for detecting qrcode
|
||||||
|
self._scanner = ImageScanner()
|
||||||
|
self._scanner.parse_config('enable')
|
||||||
|
#self._scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1)
|
||||||
|
#self._scanner.setConfig(0, Config.X_DENSITY, 3)
|
||||||
|
#self._scanner.setConfig(0, Config.Y_DENSITY, 3)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not self._camera:
|
||||||
|
self._camera = Camera(
|
||||||
|
resolution=self.camera_size,
|
||||||
|
size_hint=(None, None))
|
||||||
|
self.add_widget(self._camera)
|
||||||
|
self.bind(size=self._camera.setter('size'))
|
||||||
|
self.bind(pos=self._camera.setter('pos'))
|
||||||
|
else:
|
||||||
|
self._camera._camera.init_camera()
|
||||||
|
self._camera.play = True
|
||||||
|
Clock.schedule_interval(self._detect_qrcode_frame, 1/15)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if not self._camera:
|
||||||
|
return
|
||||||
|
self._camera.play = False
|
||||||
|
Clock.unschedule(self._detect_qrcode_frame)
|
||||||
|
# TODO: testing for various platforms(windows, mac)
|
||||||
|
self._camera._camera._pipeline.set_state(1)
|
||||||
|
#self._camera = None
|
||||||
|
|
||||||
|
def _detect_qrcode_frame(self, *args):
|
||||||
|
# the image we got by default from a camera is using the rgba format
|
||||||
|
# zbar only allow Y800/GREY image, so we first need to convert,
|
||||||
|
# then start the detection on the image
|
||||||
|
if not self.get_root_window():
|
||||||
|
self.stop()
|
||||||
|
return
|
||||||
|
cam = self._camera
|
||||||
|
tex = cam.texture
|
||||||
|
if not tex:
|
||||||
|
return
|
||||||
|
im = PILImage.fromstring('RGBA', tex.size, tex.pixels)
|
||||||
|
im = im.convert('L')
|
||||||
|
barcode = Image(tex.size[0],
|
||||||
|
tex.size[1], 'Y800', im.tostring())
|
||||||
|
|
||||||
|
result = self._scanner.scan(barcode)
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
self.symbols = []
|
||||||
|
del(barcode)
|
||||||
|
return
|
||||||
|
|
||||||
|
# we detected qrcode! extract and dispatch them
|
||||||
|
symbols = []
|
||||||
|
for symbol in barcode.symbols:
|
||||||
|
qrcode = ScannerCamera.Qrcode(
|
||||||
|
type=symbol.type,
|
||||||
|
data=symbol.data,
|
||||||
|
quality=symbol.quality,
|
||||||
|
count=symbol.count,
|
||||||
|
bounds=symbol.location)
|
||||||
|
symbols.append(qrcode)
|
||||||
|
|
||||||
|
self.symbols = symbols
|
||||||
|
del(barcode)
|
||||||
179
gui/kivy/qrcodewidget.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
''' Kivy Widget that accepts data and displas qrcode
|
||||||
|
'''
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from kivy.uix.floatlayout import FloatLayout
|
||||||
|
|
||||||
|
from kivy.graphics.texture import Texture
|
||||||
|
from kivy.properties import StringProperty
|
||||||
|
from kivy.properties import ObjectProperty, StringProperty, ListProperty,\
|
||||||
|
BooleanProperty
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.clock import Clock
|
||||||
|
|
||||||
|
try:
|
||||||
|
import qrcode
|
||||||
|
except ImportError:
|
||||||
|
sys.exit("Error: qrcode does not seem to be installed. Try 'sudo pip install qrcode'")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<QRCodeWidget>
|
||||||
|
on_parent: if args[1]: qrimage.source = self.loading_image
|
||||||
|
canvas.before:
|
||||||
|
# Draw white Rectangle
|
||||||
|
Color:
|
||||||
|
rgba: root.background_color
|
||||||
|
Rectangle:
|
||||||
|
size: self.size
|
||||||
|
pos: self.pos
|
||||||
|
canvas.after:
|
||||||
|
Color:
|
||||||
|
rgba: .5, .5, .5, 1 if root.show_border else 0
|
||||||
|
Line:
|
||||||
|
width: dp(1.333)
|
||||||
|
points:
|
||||||
|
dp(2), dp(2),\
|
||||||
|
self.width - dp(2), dp(2),\
|
||||||
|
self.width - dp(2), self.height - dp(2),\
|
||||||
|
dp(2), self.height - dp(2),\
|
||||||
|
dp(2), dp(2)
|
||||||
|
Image
|
||||||
|
id: qrimage
|
||||||
|
pos_hint: {'center_x': .5, 'center_y': .5}
|
||||||
|
allow_stretch: True
|
||||||
|
size_hint: None, None
|
||||||
|
size: root.width * .9, root.height * .9
|
||||||
|
''')
|
||||||
|
|
||||||
|
class QRCodeWidget(FloatLayout):
|
||||||
|
|
||||||
|
show_border = BooleanProperty(True)
|
||||||
|
'''Whether to show border around the widget.
|
||||||
|
|
||||||
|
:data:`show_border` is a :class:`~kivy.properties.BooleanProperty`,
|
||||||
|
defaulting to `True`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
data = StringProperty(None, allow_none=True)
|
||||||
|
''' Data using which the qrcode is generated.
|
||||||
|
|
||||||
|
:data:`data` is a :class:`~kivy.properties.StringProperty`, defaulting to
|
||||||
|
`None`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
background_color = ListProperty((1, 1, 1, 1))
|
||||||
|
''' Background color of the background of the widget.
|
||||||
|
|
||||||
|
:data:`background_color` is a :class:`~kivy.properties.ListProperty`,
|
||||||
|
defaulting to `(1, 1, 1, 1)`.
|
||||||
|
'''
|
||||||
|
|
||||||
|
loading_image = StringProperty('gui/kivy/theming/loading.gif')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(QRCodeWidget, self).__init__(**kwargs)
|
||||||
|
self.addr = None
|
||||||
|
self.qr = None
|
||||||
|
self._qrtexture = None
|
||||||
|
|
||||||
|
def on_data(self, instance, value):
|
||||||
|
if not (self.canvas or value):
|
||||||
|
return
|
||||||
|
img = self.ids.get('qrimage', None)
|
||||||
|
|
||||||
|
if not img:
|
||||||
|
# if texture hasn't yet been created delay the texture updation
|
||||||
|
Clock.schedule_once(lambda dt: self.on_data(instance, value))
|
||||||
|
return
|
||||||
|
img.anim_delay = .25
|
||||||
|
img.source = self.loading_image
|
||||||
|
Thread(target=partial(self.generate_qr, value)).start()
|
||||||
|
|
||||||
|
def generate_qr(self, value):
|
||||||
|
self.set_addr(value)
|
||||||
|
self.update_qr()
|
||||||
|
|
||||||
|
def set_addr(self, addr):
|
||||||
|
if self.addr == addr:
|
||||||
|
return
|
||||||
|
MinSize = 210 if len(addr) < 128 else 500
|
||||||
|
self.setMinimumSize((MinSize, MinSize))
|
||||||
|
self.addr = addr
|
||||||
|
self.qr = None
|
||||||
|
|
||||||
|
def update_qr(self):
|
||||||
|
if not self.addr and self.qr:
|
||||||
|
return
|
||||||
|
QRCode = qrcode.QRCode
|
||||||
|
L = qrcode.constants.ERROR_CORRECT_L
|
||||||
|
addr = self.addr
|
||||||
|
try:
|
||||||
|
self.qr = qr = QRCode(
|
||||||
|
version=None,
|
||||||
|
error_correction=L,
|
||||||
|
box_size=10,
|
||||||
|
border=0,
|
||||||
|
)
|
||||||
|
qr.add_data(addr)
|
||||||
|
qr.make(fit=True)
|
||||||
|
except Exception as e:
|
||||||
|
print e
|
||||||
|
self.qr=None
|
||||||
|
self.update_texture()
|
||||||
|
|
||||||
|
def setMinimumSize(self, size):
|
||||||
|
# currently unused, do we need this?
|
||||||
|
self._texture_size = size
|
||||||
|
|
||||||
|
def _create_texture(self, k, dt):
|
||||||
|
self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb')
|
||||||
|
# don't interpolate texture
|
||||||
|
texture.min_filter = 'nearest'
|
||||||
|
texture.mag_filter = 'nearest'
|
||||||
|
|
||||||
|
def update_texture(self):
|
||||||
|
if not self.addr:
|
||||||
|
return
|
||||||
|
|
||||||
|
matrix = self.qr.get_matrix()
|
||||||
|
k = len(matrix)
|
||||||
|
# create the texture in main UI thread otherwise
|
||||||
|
# this will lead to memory corruption
|
||||||
|
Clock.schedule_once(partial(self._create_texture, k), -1)
|
||||||
|
buff = []
|
||||||
|
bext = buff.extend
|
||||||
|
cr, cg, cb, ca = self.background_color[:]
|
||||||
|
cr, cg, cb = cr*255, cg*255, cb*255
|
||||||
|
|
||||||
|
for r in range(k):
|
||||||
|
for c in range(k):
|
||||||
|
bext([0, 0, 0] if matrix[r][c] else [cr, cg, cb])
|
||||||
|
|
||||||
|
# then blit the buffer
|
||||||
|
buff = ''.join(map(chr, buff))
|
||||||
|
# update texture in UI thread.
|
||||||
|
Clock.schedule_once(lambda dt: self._upd_texture(buff))
|
||||||
|
|
||||||
|
def _upd_texture(self, buff):
|
||||||
|
texture = self._qrtexture
|
||||||
|
if not texture:
|
||||||
|
# if texture hasn't yet been created delay the texture updation
|
||||||
|
Clock.schedule_once(lambda dt: self._upd_texture(buff))
|
||||||
|
return
|
||||||
|
texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte')
|
||||||
|
img =self.ids.qrimage
|
||||||
|
img.anim_delay = -1
|
||||||
|
img.texture = texture
|
||||||
|
img.canvas.ask_update()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from kivy.app import runTouchApp
|
||||||
|
import sys
|
||||||
|
data = str(sys.argv[1:])
|
||||||
|
runTouchApp(QRCodeWidget(data=data))
|
||||||
|
|
||||||
|
|
||||||
1095
gui/kivy/screens.py
Normal file
7
gui/kivy/statusbar.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
|
from kivy.properties import StringProperty
|
||||||
|
|
||||||
|
|
||||||
|
class StatusBar(BoxLayout):
|
||||||
|
|
||||||
|
text = StringProperty('')
|
||||||
14
gui/kivy/textinput.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from kivy.uix.textinput import TextInput
|
||||||
|
from kivy.properties import OptionProperty
|
||||||
|
|
||||||
|
class ELTextInput(TextInput):
|
||||||
|
|
||||||
|
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)
|
||||||
BIN
gui/kivy/theming/light-0.png
Normal file
|
After Width: | Height: | Size: 716 KiB |
BIN
gui/kivy/theming/light-1.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
1
gui/kivy/theming/light.atlas
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"light-1.png": {"icon_border": [475, 958, 64, 64], "tab_btn_disabled": [625, 924, 32, 32], "tab_btn_pressed": [693, 924, 32, 32], "btn_send_nfc": [1003, 968, 18, 15], "logo_atom_dull": [607, 958, 64, 64], "tab": [871, 958, 64, 64], "logo": [149, 894, 128, 128], "confirmed": [409, 958, 64, 64], "pen": [739, 958, 64, 64], "star_big_inactive": [279, 894, 128, 128], "action_group_dark": [2, 787, 33, 48], "mail_icon": [409, 902, 65, 54], "tab_btn": [659, 924, 32, 32], "btn_send_address": [1003, 985, 18, 15], "add_contact": [538, 913, 51, 43], "manualentry": [2, 888, 145, 134], "wallets": [727, 924, 32, 32], "shadow": [805, 958, 64, 64], "unconfirmed": [937, 958, 64, 64], "info": [541, 958, 64, 64], "nfc": [673, 958, 64, 64], "settings": [591, 924, 32, 32], "closebutton": [476, 913, 60, 43], "wallet": [53, 842, 49, 44], "contact": [2, 837, 49, 49], "dialog": [1003, 1002, 18, 20]}, "light-0.png": {"globe": [937, 65, 72, 72], "btn_create_account": [840, 394, 64, 32], "card_top": [770, 31, 32, 16], "qrcode": [805, 508, 145, 145], "close": [886, 168, 88, 88], "btn_create_act_disabled": [946, 394, 32, 32], "create_act_text": [998, 430, 22, 10], "card_bottom": [985, 150, 32, 16], "carousel_deselected": [952, 589, 64, 64], "network": [976, 208, 48, 48], "blue_bg_round_rb": [886, 146, 31, 20], "action_bar": [976, 170, 36, 36], "arrow_back": [974, 442, 50, 50], "card_btn": [906, 394, 38, 32], "tab_disabled": [644, 394, 96, 32], "lightblue_bg_round_lb": [919, 146, 31, 20], "white_bg_round_top": [952, 146, 31, 20], "tab_strip": [742, 394, 96, 32], "important": [770, 49, 88, 88], "gear": [644, 494, 159, 159], "stepper_left": [376, 20, 392, 117], "nfc_stage_one": [376, 258, 489, 122], "nfc_clock": [644, 655, 372, 367], "clock1": [644, 428, 64, 64], "clock2": [710, 428, 64, 64], "clock3": [776, 428, 64, 64], "clock4": [842, 428, 64, 64], "paste_icon": [860, 60, 75, 77], "carousel_selected": [952, 523, 64, 64], "card": [980, 394, 32, 32], "electrum_icon640": [2, 382, 640, 640], "btn_nfc": [1011, 125, 13, 12], "create_act_text_active": [974, 430, 22, 10], "stepper_full": [376, 139, 392, 117], "nfc_phone": [2, 13, 372, 367], "error": [867, 266, 128, 114], "textinput_active": [770, 142, 114, 114], "shadow_right": [952, 516, 32, 5], "clock5": [908, 428, 64, 64]}}
|
||||||
BIN
gui/kivy/theming/light/action_bar.png
Normal file
|
After Width: | Height: | Size: 1022 B |
BIN
gui/kivy/theming/light/action_group_dark.png
Normal file
|
After Width: | Height: | Size: 380 B |
BIN
gui/kivy/theming/light/add_contact.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
gui/kivy/theming/light/arrow_back.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
gui/kivy/theming/light/blue_bg_round_rb.png
Normal file
|
After Width: | Height: | Size: 242 B |
BIN
gui/kivy/theming/light/btn_create_account.png
Normal file
|
After Width: | Height: | Size: 311 B |
BIN
gui/kivy/theming/light/btn_create_act_disabled.png
Normal file
|
After Width: | Height: | Size: 427 B |
BIN
gui/kivy/theming/light/btn_nfc.png
Normal file
|
After Width: | Height: | Size: 362 B |
BIN
gui/kivy/theming/light/btn_send_address.png
Normal file
|
After Width: | Height: | Size: 210 B |
BIN
gui/kivy/theming/light/btn_send_nfc.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
gui/kivy/theming/light/card.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
gui/kivy/theming/light/card_bottom.png
Normal file
|
After Width: | Height: | Size: 383 B |
BIN
gui/kivy/theming/light/card_btn.png
Normal file
|
After Width: | Height: | Size: 357 B |
BIN
gui/kivy/theming/light/card_top.png
Normal file
|
After Width: | Height: | Size: 481 B |
BIN
gui/kivy/theming/light/carousel_deselected.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
gui/kivy/theming/light/carousel_selected.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
gui/kivy/theming/light/clock1.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gui/kivy/theming/light/clock2.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
gui/kivy/theming/light/clock3.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
gui/kivy/theming/light/clock4.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
gui/kivy/theming/light/clock5.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gui/kivy/theming/light/close.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
gui/kivy/theming/light/closebutton.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gui/kivy/theming/light/confirmed.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
gui/kivy/theming/light/contact.png
Normal file
|
After Width: | Height: | Size: 838 B |
BIN
gui/kivy/theming/light/create_act_text.png
Normal file
|
After Width: | Height: | Size: 330 B |
BIN
gui/kivy/theming/light/create_act_text_active.png
Normal file
|
After Width: | Height: | Size: 308 B |
BIN
gui/kivy/theming/light/dialog.png
Normal file
|
After Width: | Height: | Size: 393 B |
BIN
gui/kivy/theming/light/electrum_icon640.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
gui/kivy/theming/light/error.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
gui/kivy/theming/light/gear.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
gui/kivy/theming/light/globe.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
gui/kivy/theming/light/icon_border.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
gui/kivy/theming/light/important.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
gui/kivy/theming/light/info.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
gui/kivy/theming/light/lightblue_bg_round_lb.png
Normal file
|
After Width: | Height: | Size: 244 B |
BIN
gui/kivy/theming/light/logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
gui/kivy/theming/light/logo_atom_dull.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
gui/kivy/theming/light/mail_icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
gui/kivy/theming/light/manualentry.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
gui/kivy/theming/light/network.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
gui/kivy/theming/light/nfc.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
gui/kivy/theming/light/nfc_clock.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
gui/kivy/theming/light/nfc_phone.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
gui/kivy/theming/light/nfc_stage_one.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
gui/kivy/theming/light/paste_icon.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
gui/kivy/theming/light/pen.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
gui/kivy/theming/light/qrcode.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
gui/kivy/theming/light/settings.png
Normal file
|
After Width: | Height: | Size: 199 B |
BIN
gui/kivy/theming/light/shadow.png
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
gui/kivy/theming/light/shadow_right.png
Normal file
|
After Width: | Height: | Size: 243 B |
BIN
gui/kivy/theming/light/star_big_inactive.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
gui/kivy/theming/light/stepper_full.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
gui/kivy/theming/light/stepper_left.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
gui/kivy/theming/light/tab.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gui/kivy/theming/light/tab_btn.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gui/kivy/theming/light/tab_btn_disabled.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gui/kivy/theming/light/tab_btn_pressed.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
gui/kivy/theming/light/tab_disabled.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
gui/kivy/theming/light/tab_strip.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gui/kivy/theming/light/textinput_active.png
Normal file
|
After Width: | Height: | Size: 488 B |
BIN
gui/kivy/theming/light/unconfirmed.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
gui/kivy/theming/light/wallet.png
Normal file
|
After Width: | Height: | Size: 824 B |
BIN
gui/kivy/theming/light/wallets.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
gui/kivy/theming/light/white_bg_round_top.png
Normal file
|
After Width: | Height: | Size: 280 B |
BIN
gui/kivy/theming/loading.gif
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
gui/kivy/theming/splash.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
2
gui/kivy/utils.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||