@ -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 |
@ -0,0 +1,5 @@ |
|||
Commands:: |
|||
|
|||
`make theming` to make a atlas out of a list of pngs |
|||
|
|||
`make apk` to make a apk |
@ -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() |
@ -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) |
@ -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() |
@ -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) |
@ -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") |
@ -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 |
@ -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() |
@ -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 |
@ -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 |
@ -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) |
@ -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) |
@ -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') |
@ -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() |
@ -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') |
@ -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) |
@ -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() |
@ -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) |
@ -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)) |
|||
|
|||
|
@ -0,0 +1,7 @@ |
|||
from kivy.uix.boxlayout import BoxLayout |
|||
from kivy.properties import StringProperty |
|||
|
|||
|
|||
class StatusBar(BoxLayout): |
|||
|
|||
text = StringProperty('') |
@ -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) |
After Width: | Height: | Size: 716 KiB |
After Width: | Height: | Size: 79 KiB |
@ -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]}} |
After Width: | Height: | Size: 1022 B |
After Width: | Height: | Size: 380 B |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 242 B |
After Width: | Height: | Size: 311 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 362 B |
After Width: | Height: | Size: 210 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 561 B |
After Width: | Height: | Size: 383 B |
After Width: | Height: | Size: 357 B |
After Width: | Height: | Size: 481 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 838 B |
After Width: | Height: | Size: 330 B |
After Width: | Height: | Size: 308 B |
After Width: | Height: | Size: 393 B |
After Width: | Height: | Size: 526 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 514 B |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 199 B |
After Width: | Height: | Size: 884 B |
After Width: | Height: | Size: 243 B |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 488 B |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 824 B |
After Width: | Height: | Size: 222 B |
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 73 KiB |
@ -0,0 +1,2 @@ |
|||
|
|||
|