tnew combined tablet&mobile design on top of 1.9.x branch WIP - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
DIR commit 30126c544b81bcb8ac6a510da9c9d42acf82ee28
DIR parent 2a889d318be1e2d50945f49b565ccd09cebd69ff
HTML Author: qua-non <akshayaurora@gmail.com>
Date: Tue, 18 Feb 2014 12:42:57 +0530
new combined tablet&mobile design on top of 1.9.x branch WIP
Diffstat:
A gui/kivy/Makefile | 29 +++++++++++++++++++++++++++++
A gui/kivy/Readme.txt | 5 +++++
A gui/kivy/__init__.py | 87 +++++++++++++++++++++++++++++++
A gui/kivy/carousel.py | 41 +++++++++++++++++++++++++++++++
A gui/kivy/combobox.py | 93 +++++++++++++++++++++++++++++++
A gui/kivy/console.py | 319 +++++++++++++++++++++++++++++++
A gui/kivy/dialog.py | 611 +++++++++++++++++++++++++++++++
A gui/kivy/drawer.py | 174 +++++++++++++++++++++++++++++++
A gui/kivy/gridview.py | 203 +++++++++++++++++++++++++++++++
A gui/kivy/installwizard.py | 224 +++++++++++++++++++++++++++++++
A gui/kivy/main.kv | 402 ++++++++++++++++++++++++++++++
A gui/kivy/main_window.py | 294 +++++++++++++++++++++++++++++++
A gui/kivy/menus.py | 95 ++++++++++++++++++++++++++++++
A gui/kivy/nfc_scanner/__init__.py | 43 ++++++++++++++++++++++++++++++
A gui/kivy/nfc_scanner/scanner_andro… | 86 ++++++++++++++++++++++++++++++
A gui/kivy/nfc_scanner/scanner_dummy… | 37 +++++++++++++++++++++++++++++++
A gui/kivy/qr_scanner/__init__.py | 105 +++++++++++++++++++++++++++++++
A gui/kivy/qr_scanner/scanner_androi… | 354 +++++++++++++++++++++++++++++++
A gui/kivy/qr_scanner/scanner_camera… | 89 +++++++++++++++++++++++++++++++
A gui/kivy/qrcodewidget.py | 179 +++++++++++++++++++++++++++++++
A gui/kivy/screens.py | 1095 +++++++++++++++++++++++++++++++
A gui/kivy/statusbar.py | 7 +++++++
A gui/kivy/textinput.py | 14 ++++++++++++++
A gui/kivy/theming/light-0.png | 0
A gui/kivy/theming/light-1.png | 0
A gui/kivy/theming/light.atlas | 2 ++
A gui/kivy/theming/light/action_bar.… | 0
A gui/kivy/theming/light/action_grou… | 0
A gui/kivy/theming/light/add_contact… | 0
A gui/kivy/theming/light/arrow_back.… | 0
A gui/kivy/theming/light/blue_bg_rou… | 0
A gui/kivy/theming/light/btn_create_… | 0
A gui/kivy/theming/light/btn_create_… | 0
A gui/kivy/theming/light/btn_nfc.png | 0
A gui/kivy/theming/light/btn_send_ad… | 0
A gui/kivy/theming/light/btn_send_nf… | 0
A gui/kivy/theming/light/card.png | 0
A gui/kivy/theming/light/card_bottom… | 0
A gui/kivy/theming/light/card_btn.png | 0
A gui/kivy/theming/light/card_top.png | 0
A gui/kivy/theming/light/carousel_de… | 0
A gui/kivy/theming/light/carousel_se… | 0
A gui/kivy/theming/light/clock1.png | 0
A gui/kivy/theming/light/clock2.png | 0
A gui/kivy/theming/light/clock3.png | 0
A gui/kivy/theming/light/clock4.png | 0
A gui/kivy/theming/light/clock5.png | 0
A gui/kivy/theming/light/close.png | 0
A gui/kivy/theming/light/closebutton… | 0
A gui/kivy/theming/light/confirmed.p… | 0
A gui/kivy/theming/light/contact.png | 0
A gui/kivy/theming/light/create_act_… | 0
A gui/kivy/theming/light/create_act_… | 0
A gui/kivy/theming/light/dialog.png | 0
A gui/kivy/theming/light/electrum_ic… | 0
A gui/kivy/theming/light/error.png | 0
A gui/kivy/theming/light/gear.png | 0
A gui/kivy/theming/light/globe.png | 0
A gui/kivy/theming/light/icon_border… | 0
A gui/kivy/theming/light/important.p… | 0
A gui/kivy/theming/light/info.png | 0
A gui/kivy/theming/light/lightblue_b… | 0
A gui/kivy/theming/light/logo.png | 0
A gui/kivy/theming/light/logo_atom_d… | 0
A gui/kivy/theming/light/mail_icon.p… | 0
A gui/kivy/theming/light/manualentry… | 0
A gui/kivy/theming/light/network.png | 0
A gui/kivy/theming/light/nfc.png | 0
A gui/kivy/theming/light/nfc_clock.p… | 0
A gui/kivy/theming/light/nfc_phone.p… | 0
A gui/kivy/theming/light/nfc_stage_o… | 0
A gui/kivy/theming/light/paste_icon.… | 0
A gui/kivy/theming/light/pen.png | 0
A gui/kivy/theming/light/qrcode.png | 0
A gui/kivy/theming/light/settings.png | 0
A gui/kivy/theming/light/shadow.png | 0
A gui/kivy/theming/light/shadow_righ… | 0
A gui/kivy/theming/light/star_big_in… | 0
A gui/kivy/theming/light/stepper_ful… | 0
A gui/kivy/theming/light/stepper_lef… | 0
A gui/kivy/theming/light/tab.png | 0
A gui/kivy/theming/light/tab_btn.png | 0
A gui/kivy/theming/light/tab_btn_dis… | 0
A gui/kivy/theming/light/tab_btn_pre… | 0
A gui/kivy/theming/light/tab_disable… | 0
A gui/kivy/theming/light/tab_strip.p… | 0
A gui/kivy/theming/light/textinput_a… | 0
A gui/kivy/theming/light/unconfirmed… | 0
A gui/kivy/theming/light/wallet.png | 0
A gui/kivy/theming/light/wallets.png | 0
A gui/kivy/theming/light/white_bg_ro… | 0
A gui/kivy/theming/loading.gif | 0
A gui/kivy/theming/splash.png | 0
A gui/kivy/utils.py | 2 ++
94 files changed, 4590 insertions(+), 0 deletions(-)
---
DIR diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile
t@@ -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
DIR diff --git a/gui/kivy/Readme.txt b/gui/kivy/Readme.txt
t@@ -0,0 +1,5 @@
+Commands::
+
+ `make theming` to make a atlas out of a list of pngs
+
+ `make apk` to make a apk
DIR diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py
t@@ -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()
DIR diff --git a/gui/kivy/carousel.py b/gui/kivy/carousel.py
t@@ -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)
+\ No newline at end of file
DIR diff --git a/gui/kivy/combobox.py b/gui/kivy/combobox.py
t@@ -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()
DIR diff --git a/gui/kivy/console.py b/gui/kivy/console.py
t@@ -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)
DIR diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py
t@@ -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")
DIR diff --git a/gui/kivy/drawer.py b/gui/kivy/drawer.py
t@@ -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
+\ No newline at end of file
DIR diff --git a/gui/kivy/gridview.py b/gui/kivy/gridview.py
t@@ -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()
DIR diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py
t@@ -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
DIR diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv
t@@ -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
+\ No newline at end of file
DIR diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
t@@ -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)
DIR diff --git a/gui/kivy/menus.py b/gui/kivy/menus.py
t@@ -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)
DIR diff --git a/gui/kivy/nfc_scanner/__init__.py b/gui/kivy/nfc_scanner/__init__.py
t@@ -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')
DIR diff --git a/gui/kivy/nfc_scanner/scanner_android.py b/gui/kivy/nfc_scanner/scanner_android.py
t@@ -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()
DIR diff --git a/gui/kivy/nfc_scanner/scanner_dummy.py b/gui/kivy/nfc_scanner/scanner_dummy.py
t@@ -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')
DIR diff --git a/gui/kivy/qr_scanner/__init__.py b/gui/kivy/qr_scanner/__init__.py
t@@ -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)
DIR diff --git a/gui/kivy/qr_scanner/scanner_android.py b/gui/kivy/qr_scanner/scanner_android.py
t@@ -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()
DIR diff --git a/gui/kivy/qr_scanner/scanner_camera.py b/gui/kivy/qr_scanner/scanner_camera.py
t@@ -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)
DIR diff --git a/gui/kivy/qrcodewidget.py b/gui/kivy/qrcodewidget.py
t@@ -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))
+
+
DIR diff --git a/gui/kivy/screens.py b/gui/kivy/screens.py
t@@ -0,0 +1,1095 @@
+from functools import partial
+import os, datetime, json, csv
+
+from kivy.app import App
+from kivy.animation import Animation
+from kivy.core.clipboard import Clipboard
+from kivy.clock import Clock
+from kivy.factory import Factory
+from kivy.metrics import dp
+from kivy.properties import (ObjectProperty, StringProperty, ListProperty,
+ DictProperty)
+
+from kivy.uix.button import Button
+from kivy.uix.bubble import Bubble, BubbleButton
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.label import Label
+from kivy.uix.textinput import TextInput
+from kivy.uix.screenmanager import Screen as Screen, ScreenManager
+from kivy.uix.tabbedpanel import TabbedPanel
+
+
+from electrum_gui.kivy.dialog import (NewContactDialog, TakeInputDialog,
+ PrivateKeyDialog, SignVerifyDialog, MessageBox, MessageBoxError,
+ SaveDialog, LoadDialog, InfoDialog, ImportPrivateKeysDialog, Dialog,
+ EditLabelDialog, EditDescriptionDialog, ShowMasterPublicKeyDialog,
+ RecentActivityDialog)
+
+from electrum_gui.i18n import _, languages
+from electrum_gui.kivy.menus import ContextMenu
+from electrum.interface import DEFAULT_PORTS
+from electrum.verifier import WalletVerifier
+from electrum.wallet import Wallet, WalletSynchronizer
+from electrum.bitcoin import is_valid
+
+DEFAULT_PATH = '/tmp/'
+
+# Delayed imports
+encode_uri = None
+
+
+class CScreen(Screen):
+
+ __events__ = ('on_activate', 'on_deactivate')
+
+ action_view = ObjectProperty(None)
+
+ def _change_action_view(self):
+ app = App.get_running_app()
+ action_bar = app.root.main_screen.ids.action_bar
+ _action_view = self.action_view
+
+ if (not _action_view) or _action_view.parent:
+ return
+ action_bar.clear_widgets()
+ action_bar.add_widget(_action_view)
+
+ def on_activate(self):
+ Clock.schedule_once(lambda dt: self._change_action_view())
+
+ def on_deactivate(self):
+ Clock.schedule_once(lambda dt: self._change_action_view())
+
+
+class RootManager(ScreenManager):
+ '''Main Root Widget of the app'''
+
+ # initialize properties that will be updted in kv
+ main_screen = ObjectProperty(None)
+ '''Object holding the reference to main screen'''
+
+ screen_preferences = ObjectProperty(None)
+ '''Object holding the reference to preferences screen'''
+
+ screen_seed = ObjectProperty(None)
+ ''''''
+
+ screen_network = ObjectProperty(None)
+ '''Object holding the Network screen'''
+
+
+class MainScreen(Screen):
+
+ pass
+
+
+class ScreenSend(CScreen):
+
+ pass
+
+
+class ScreenDashboard(CScreen):
+
+ tab = ObjectProperty(None)
+
+ def show_tx_details(
+ self, date, address, amount, amount_color, balance,
+ tx_hash, conf, quote_text):
+
+ ra_dialog = RecentActivityDialog()
+
+ ra_dialog.address = address
+ ra_dialog.amount = amount
+ ra_dialog.amount_color = amount_color
+ ra_dialog.confirmations = conf
+ ra_dialog.quote_text = quote_text
+ date_time = date.split()
+ if len(date_time) == 2:
+ ra_dialog.date = date_time[0]
+ ra_dialog.time = date_time[1]
+ ra_dialog.status = 'Validated'
+ else:
+ ra_dialog.date = date_time
+ ra_dialog.status = 'Pending'
+ ra_dialog.tx_hash = tx_hash
+
+ app = App.get_running_app()
+ main_gui = app.gui.main_gui
+ tx_hash = tx_hash
+ tx = app.wallet.transactions.get(tx_hash)
+
+ if tx_hash in app.wallet.transactions.keys():
+ is_relevant, is_mine, v, fee = app.wallet.get_tx_value(tx)
+ conf, timestamp = app.wallet.verifier.get_confirmations(tx_hash)
+ #if timestamp:
+ # time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ #else:
+ # time_str = 'pending'
+ else:
+ is_mine = False
+
+ ra_dialog.is_mine = is_mine
+
+ if is_mine:
+ if fee is not None:
+ ra_dialog.fee = main_gui.format_amount(fee)
+ else:
+ ra_dialog.fee = 'unknown'
+
+ ra_dialog.open()
+
+
+class ScreenPassword(Screen):
+
+ __events__ = ('on_release', 'on_deactivate', 'on_activate')
+
+ def on_activate(self):
+ app = App.get_running_app()
+ action_bar = app.root.main_screen.ids.action_bar
+ action_bar.add_widget(self._action_view)
+
+ def on_deactivate(self):
+ self.ids.password.text = ''
+
+ def on_release(self, *args):
+ pass
+
+
+class SettingsScreen(Screen):
+
+ def __init__(self, **kwargs):
+ super(SettingsScreen, self).__init__(**kwargs)
+ Clock.schedule_once(self.delayed_init)
+ self.app = App.get_running_app()
+
+ def on_enter(self, *args):
+ self.delayed_init()
+
+ def delayed_init(self, *dt):
+ app = self.app
+ try:
+ main_gui = app.gui.main_gui
+ except AttributeError:
+ # wait for main gui to start
+ Clock.schedule_once(self.delayed_init, 1)
+ return
+ ids = self.ids
+
+ ids.st_unit_combo.key = main_gui.base_unit()
+ ids.st_fee_e.text = main_gui.format_amount(app.wallet.fee).strip()
+ ids.st_expert_cb.active = main_gui.expert_mode
+
+ currencies = main_gui.exchanger.get_currencies()
+ currencies.insert(0, "None")
+ currencies = zip(currencies, currencies)
+ key = app.conf.get('currency', 'None')
+ ids.st_cur_combo.text = ids.st_cur_combo.key = key
+ ids.st_cur_combo.items = currencies
+
+ ids.st_lang_combo.key = key = app.conf.get("language", '')
+ ids.st_lang_combo.items = languages.items()
+ x, y = zip(*ids.st_lang_combo.items)
+ ids.st_lang_combo.text = y[x.index(key)]
+
+ def do_callback(self, instance):
+ ids = self.ids
+ app = self.app
+ wallet = app.wallet
+ main_gui = app.gui.main_gui
+
+ if instance == ids.export_labels:
+ title = _("Select file to save your labels")
+ path = DEFAULT_PATH
+ filename = "electrum_labels.dat"
+ filters = ["*.dat"]
+
+ def save(instance):
+ path = dialog.file_chooser.path
+ filename = dialog.text_input.text.strip()
+ labels = wallet.labels
+ try:
+ with open(os.path.join(path, filename), 'w+') as stream:
+ json.dump(labels, stream)
+ MessageBox(title="Labels exported",
+ message=_("Your labels were exported to")\
+ + " '%s'" % str(filename),
+ size=('320dp', '320dp')).open()
+ except (IOError, os.error), reason:
+ MessageBoxError(
+ title="Unable to export labels",
+ message=_("Electrum was unable to export your labels.")+
+ "\n" + str(reason), size=('320dp', '320dp')).open()
+ dialog.close()
+
+ dialog = SaveDialog(title=title,
+ path=path,
+ filename=filename,
+ filters=filters)
+ dialog.save_button.bind(on_release=save)
+ dialog.open()
+
+ elif instance == ids.import_labels:
+ title = _("Open labels file")
+ path = DEFAULT_PATH
+ filename = ""
+ filters = ["*.dat"]
+
+ def load(instance):
+ path = dialog.file_chooser.path
+ filename = dialog.text_input.text.strip()
+
+ labels = wallet.labels
+ try:
+ with open(os.path.join(path, filename), 'r') as stream:
+ for key, value in json.loads(stream.read()).items():
+ wallet.labels[key] = value
+ wallet.save()
+ MessageBox(title="Labels imported",
+ message=_("Your labels were imported from") + " '%s'" % str(filename),
+ size=('320dp', '320dp')).open()
+ except (IOError, os.error), reason:
+ MessageBoxError(title="Unable to import labels",
+ message=_("Electrum was unable to import your labels.") + "\n" + str(reason),
+ size=('320dp', '320dp')).open()
+
+ dialog.close()
+
+ dialog = LoadDialog(title=title, path=path, filename=filename, filters=filters)
+ dialog.load_button.bind(on_press=load)
+ dialog.open()
+
+ elif instance == ids.export_history:
+ title = _("Select file to export your wallet transactions to")
+ path = os.path.expanduser('~')
+ filename = "electrum-history.csv"
+ filters = ["*.csv"]
+
+ def save(instance):
+ path = dialog.file_chooser.path
+ filename = dialog.text_input.text.strip()
+ # extracted from gui_lite.csv_transaction
+ wallet = wallet
+ try:
+ with open(os.path.join(path, filename), "w+") as stream:
+ transaction = csv.writer(stream)
+ transaction.writerow(["transaction_hash", "label", "confirmations", "value", "fee", "balance", "timestamp"])
+ for item in wallet.get_tx_history():
+ tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
+ if confirmations:
+ if timestamp is not None:
+ try:
+ time_string = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
+ except [RuntimeError, TypeError, NameError] as reason:
+ time_string = "unknown"
+ pass
+ else:
+ time_string = "unknown"
+ else:
+ time_string = "pending"
+
+ if value is not None:
+ value_string = format_satoshis(value, True, wallet.num_zeros)
+ else:
+ value_string = '--'
+
+ if fee is not None:
+ fee_string = format_satoshis(fee, True, wallet.num_zeros)
+ else:
+ fee_string = '0'
+
+ if tx_hash:
+ label, is_default_label = wallet.get_label(tx_hash)
+ else:
+ label = ""
+
+ balance_string = format_satoshis(balance, False, wallet.num_zeros)
+ transaction.writerow([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
+ MessageBox(title="CSV Export created",
+ message="Your CSV export has been successfully created.",
+ size=('320dp', '320dp')).open()
+ except (IOError, os.error), reason:
+ export_error_label = _("Electrum was unable to produce a transaction export.")
+ MessageBoxError(title="Unable to create csv",
+ message=export_error_label + "\n" + str(reason),
+ size=('320dp', '320dp')).open()
+ dialog.close()
+
+ dialog = SaveDialog(title=title, path=path, filename=filename, filters=filters)
+ dialog.save_button.bind(on_press=save)
+ dialog.open()
+
+ elif instance == ids.export_privkey:
+ # NOTE: equivalent to @protected
+ def protected_save_dialog(instance=None, password=None):
+ def show_save_dialog(_dlg, instance):
+ _dlg.close()
+ title = _("Select file to export your private keys to")
+ path = DEFAULT_PATH
+ filename = "electrum-private-keys.csv"
+ filters = ["*.csv"]
+
+ def save(instance):
+ path = dialog.file_chooser.path
+ filename = dialog.text_input.text.strip()
+ try:
+ with open(os.path.join(path, filename), "w+") as csvfile:
+ transaction = csv.writer(csvfile)
+ transaction.writerow(["address", "private_key"])
+ for addr, pk in wallet.get_private_keys(wallet.addresses(True), password).items():
+ transaction.writerow(["%34s" % addr, pk])
+ MesageBox(message=_("Private keys exported."),
+ size=('320dp', '320dp')).open()
+ except (IOError, os.error), reason:
+ export_error_label = _("Electrum was unable to produce a private key-export.")
+ return MessageBoxError(message="Unable to create csv", content_text=export_error_label + "\n" + str(reason),
+ size=('320dp', '320dp')).open()
+ except BaseException, e:
+ return app.show_info_bubble(text=str(e))
+
+ dialog.close()
+
+ dialog = SaveDialog(title=title, path=path, filename=filename, filters=filters)
+ dialog.save_button.bind(on_press=save)
+ dialog.open()
+
+ mb = MessageBox(message="%s\n%s\n%s" % (
+ _("[color=ff0000ff][b]WARNING[/b][/color]: ALL your private keys are secret."),
+ _("Exposing a single private key can compromise your entire wallet!") + '\n\n',
+ _("In particular, [color=ff0000ff]DO NOT[/color] use 'redeem private key' services proposed by third parties.")),
+ on_release=show_save_dialog,
+ size = ('350dp', '320dp')).open()
+
+ if wallet.use_encryption:
+ return main_gui.password_required_dialog(post_ok=protected_save_dialog)
+ return protected_save_dialog()
+
+ elif instance == ids.import_privkey:
+ # NOTE: equivalent to @protected
+ def protected_load_dialog(_instance=None, password=None):
+ def show_privkey_dialog(__instance=None):
+
+ def on_release(_dlg, _btn):
+ if _btn.text == _('Cancel'):
+ _dlg.close()
+ confirm_dialog.close()
+ return
+
+ text = _dlg.ids.ti.text.split()
+ badkeys = []
+ addrlist = []
+ for key in text:
+ try:
+ addr = wallet.import_key(key, password)
+ except BaseException as e:
+ badkeys.append(key)
+ continue
+ if not addr:
+ badkeys.append(key)
+ else:
+ addrlist.append(addr)
+ if addrlist:
+ MessageBox(title=_('Information'),
+ message=_("The following addresses were added") + ':\n' + '\n'.join(addrlist),
+ size=('320dp', '320dp')).open()
+ if badkeys:
+ MessageBoxError(title=_('Error'),
+ message=_("The following inputs could not be imported") + ':\n' + '\n'.join(badkeys),
+ size=('320dp', '320dp')).open()
+ main_gui.update_receive_tab()
+ main_gui.update_history_tab()
+
+ if _instance is not None: # called via callback
+ _dlg.close()
+
+ ImportPrivateKeysDialog(on_release=on_release).open()
+
+ if not wallet.imported_keys:
+
+ def on_release(_dlg, _btn):
+ _dlg.close
+ if _btn.text == _('No'):
+ return
+ show_privkey_dialog()
+
+ confirm_dialog = MessageBoxError(title=_('Warning'),
+ message=_('Imported keys are not recoverable from seed.') + ' ' \
+ + _('If you ever need to restore your wallet from its seed, these keys will be lost.') + '\n\n' \
+ + _('Are you sure you understand what you are doing?'),
+ size=('320dp', '320dp'),
+ on_release=on_release)
+ confirm_dialog.buttons = [_('No'), _('Yes')]
+ confirm_dialog.open()
+ else:
+ show_privkey_dialog()
+
+ if wallet.use_encryption:
+ return main_gui.password_required_dialog(
+ post_ok=protected_load_dialog)
+ return protected_load_dialog()
+
+ elif instance == ids.show_pubkey:
+ # NOTE: Kivy TextInput doesn't wrap long text. So must handle it manually
+ pub_key = wallet.get_master_public_key()
+ pub_key = '%s\n%s\n%s\n%s' % (pub_key[0:31], pub_key[32:63], pub_key[64:95], pub_key[96:127])
+ ShowMasterPublicKeyDialog(text=pub_key).open()
+
+ elif instance == ids.from_file:
+ title = _("Select your transaction file")
+ path = DEFAULT_PATH
+ filename = ""
+ filters = ["*.txn"]
+
+ def load(instance):
+ path = dialog.file_chooser.path
+ filename = dialog.text_input.text.strip()
+
+ if not filename:
+ return
+ try:
+ with open(os.path.join(path, filename), "r") as f:
+ file_content = f.read()
+ except (ValueError, IOError, os.error), reason:
+ MessageBoxError(title="Unable to read file or no transaction found",
+ message=_("Electrum was unable to open your transaction file") + "\n" + str(reason),
+ size=('320dp', '320dp')).open()
+
+ tx_dict = main_gui.tx_dict_from_text(file_content)
+ if tx_dict:
+ main_gui.create_process_transaction_window(tx_dict)
+
+ dialog.close()
+
+ dialog = LoadDialog(title=title, path=path, filename=filename, filters=filters)
+ dialog.load_button.bind(on_press=load)
+ dialog.open()
+
+ elif instance == ids.from_text:
+ def load_transaction(_dlg, _btn):
+ if _btn.text == _('Cancel'):
+ _dlg.close
+ return
+ text = _dlg.ids.ti.text
+ if not text:
+ return
+ tx_dict = main_gui.tx_dict_from_text(text)
+ if tx_dict:
+ main_gui.create_process_transaction_window(tx_dict)
+ _dlg.close()
+
+ dialog = TakeInputDialog(on_release=load_transaction)
+ dialog.title = title=_("Input raw transaction")
+ dialog.open()
+
+ # End of do_callback() #
+
+ def on_ok(self, instance):
+ ##########
+ app = self.app
+ main_gui = app.gui.main_gui
+
+ fee = unicode(self.ids.st_fee_e.text)
+ try:
+ fee = main_gui.read_amount(fee)
+ except:
+ return MessageBoxError(message=_('Invalid value') + ': %s' % fee).open()
+
+ app.wallet.set_fee(fee)
+
+ ##########
+ nz = unicode(self.ids.st_nz_e.text)
+ try:
+ nz = int(nz)
+ if nz > 8: nz = 8
+ except:
+ return MessageBoxError(message=_('Invalid value') + ':%s' % nz).open()
+
+ if app.wallet.num_zeros != nz:
+ app.wallet.num_zeros = nz
+ app.conf.set_key('num_zeros', nz, True)
+ main_gui.update_history_tab()
+ main_gui.update_receive_tab()
+
+ usechange_result = self.ids.st_usechange_cb.active
+ if app.wallet.use_change != usechange_result:
+ app.wallet.use_change = usechange_result
+ app.conf.set_key('use_change', app.wallet.use_change, True)
+
+ unit_result = self.ids.st_unit_combo.text
+ if main_gui.base_unit() != unit_result:
+ main_gui.decimal_point = 8 if unit_result == 'BTC' else 5
+ app.conf.set_key('decimal_point', main_gui.decimal_point, True)
+ main_gui.update_history_tab()
+ main_gui.update_status()
+
+ try:
+ n = int(self.ids.st_gap_e.text)
+ except:
+ return MessageBoxError(message=_('Invalid value')).open()
+
+ if app.wallet.gap_limit != n:
+ if app.wallet.change_gap_limit(n):
+ main_gui.update_receive_tab()
+ app.conf.set_key('gap_limit', app.wallet.gap_limit, True)
+ else:
+ MessageBoxError(Message=_('Invalid value')).open()
+ # TODO: no return???
+
+ need_restart = False
+
+ lang_request = str(self.ids.st_lang_combo.key)
+ if lang_request != app.conf.get('language'):
+ app.conf.set_key("language", lang_request, True) # TODO: why can't save unicode
+ need_restart = True
+
+ cur_request = str(self.ids.st_cur_combo.text)
+ if cur_request != app.conf.get('currency', "None"):
+ app.conf.set_key('currency', cur_request, True) # TODO: why can't save unicode
+ main_gui.update_wallet()
+
+ main_gui.run_hook('close_settings_dialog')
+
+ if need_restart:
+ MessageBox(message=_('Please restart Electrum to activate the new GUI settings')).open()
+
+ # from receive_tab_set_mode()
+ main_gui.save_column_widths()
+ main_gui.expert_mode = self.ids.st_expert_cb.active
+ app.conf.set_key('classic_expert_mode', main_gui.expert_mode, True)
+ main_gui.update_receive_tab()
+
+ # close
+ app.root.current = 'main_screen'
+
+
+class NetworkScreen(Screen):
+
+ status = StringProperty(_('Uninitialized'))
+ '''status message displayed on top of screen'''
+
+ server = StringProperty('')
+
+ #servers = ListProperty([])
+
+ servers_view = ObjectProperty(None)
+
+ server_host = ObjectProperty(None)
+
+ server_port = ObjectProperty(None)
+
+ server_protocol = ObjectProperty(None)
+
+ proxy_host = ObjectProperty(None)
+
+ proxy_port = ObjectProperty(None)
+
+ proxy_mode = ObjectProperty(None)
+
+ protocol_names = ListProperty(['TCP', 'HTTP', 'SSL', 'HTTPS'])
+
+ protocol_letters = StringProperty('thsg')
+
+ proxy_names = ListProperty(['NONE', 'SOCKS4', 'SOCKS5', 'HTTP'])
+
+ proxy_keys = ListProperty(['none', 'socks4', 'socks5', 'http'])
+
+ autocycle_cb = ObjectProperty(None)
+
+ interface = ObjectProperty(None)
+
+ def __init__(self, **kwargs):
+ self.initialized = True
+ super(NetworkScreen, self).__init__(**kwargs)
+ Clock.schedule_once(self._delayed_init)
+
+ def _delayed_init(self, dt):
+ self.protocol = None
+ self.app = app = App.get_running_app()
+ self.conf = conf = app.conf
+ self.wallet = wallet = app.wallet
+ self.interface = interface = wallet.interface
+
+ if not self.initialized:
+ if interface.is_connected:
+ self.status = _("Connected to") + " %s" % (interface.host) + "\n%d " % (wallet.verifier.height) + _("blocks")
+ else:
+ self.status = _("Not connected")
+ else:
+ self.status = _("Please choose a server.") + "\n" + _("Select 'Cancel' if you are offline.")
+ self.server = server = interface.server
+
+ self.servers = interface.get_servers()
+
+ self.servers_view.content_adapter.bind(on_selection_change=self.server_changed)
+
+ ########################
+ if server:
+ host, port, protocol = server.split(':')
+ self.set_protocol(protocol)
+ self.change_server(host, protocol)
+ else:
+ self.set_protocol('s')
+
+ ########################
+ # TODO: review it
+ # if not config.is_modifiable('server'):
+ # for w in [self.server_host, self.server_port, self.server_protocol, self.servers_list_widget]: w.setEnabled(False)
+
+ self.check_for_disable(None, 'none')
+
+ # if not wallet.config.is_modifiable('proxy'):
+ # for w in [proxy_host, proxy_port, proxy_mode]: w.setEnabled(False)
+
+ proxy_config = interface.proxy\
+ if interface.proxy else\
+ { "mode":"none", "host":"localhost", "port":"8080"}
+ self.proxy_mode.key = proxy_config.get("mode")
+ self.proxy_host.text = proxy_config.get("host")
+ self.proxy_port.text = proxy_config.get("port")
+
+ # server = unicode( server_host.text ) + ':' + unicode( server_port.text ) + ':' + (protocol_letters[server_protocol.currentIndex()])
+ # if proxy_mode.currentText() != 'NONE':
+ # proxy = { u'mode':unicode(proxy_mode.currenttext).lower(), u'host':unicode(proxy_host.text), u'port':unicode(proxy_port.text) }
+ # else:
+ # proxy = None
+
+ self.autocycle_cb.active = conf.get('auto_cycle', True)
+ if not conf.is_modifiable('auto_cycle'):
+ self.autocycle_cb.active = False
+
+ def check_for_disable(self, instance, proxy_mode_key):
+ if proxy_mode_key != 'none':
+ self.proxy_host.disabled = False
+ self.proxy_port.disabled = False
+ else:
+ self.proxy_host.disabled = True
+ self.proxy_port.disabled = True
+
+ def on_cancel(self, *args):
+ self.manager.current = 'main_screen'
+
+ # TODO: RuntimeError: threads can only be started once
+ # interface.start(wait=False)
+ # interface.send([('server.peers.subscribe', [])])
+
+ # generate the first addresses, in case we are offline
+ self.wallet.synchronize()
+
+ verifier = WalletVerifier(self.interface, self.conf)
+ verifier.start()
+ self.wallet.set_verifier(verifier)
+ synchronizer = WalletSynchronizer(self.wallet, self.conf)
+ synchronizer.start()
+
+ if not self.initialized:
+ self.app.gui.main_gui.change_password_dialog()
+
+ def on_ok(self, *args):
+ self.manager.current = 'main_screen'
+
+ ################
+ server = ':'.join([str(self.server_host.text),
+ str(self.server_port.text),
+ str(self.server_protocol.key)])
+
+ if self.proxy_mode.key != 'none':
+ proxy = { 'mode':str(self.proxy_mode.key),
+ 'host':str(self.proxy_host.text),
+ 'port':str(self.proxy_port.text) }
+ else:
+ proxy = None
+
+ app = self.app
+ conf = self.conf
+ wallet = self.wallet
+ interface = self.interface
+ conf.set_key("proxy", proxy, True)
+ conf.set_key("server", server, True)
+ interface.set_server(server, proxy)
+ conf.set_key('auto_cycle', self.autocycle_cb.active, True)
+
+ # generate the first addresses, in case we are offline
+ if app.gui.action == 'create':
+ app.wallet.synchronize()
+ app.gui.change_password_dialog()
+
+ verifier = WalletVerifier(interface, conf)
+ verifier.start()
+ wallet.set_verifier(verifier)
+ synchronizer = WalletSynchronizer(wallet, conf)
+ synchronizer.start()
+
+ if app.gui.action == 'restore':
+ initialized = self.initialized
+ try:
+ def on_complete(keep_it=False):
+ wallet.fill_addressbook()
+ #if not keep_it:
+ # app.stop()
+ # return
+ if not initialized:
+ app.gui.change_password_dialog()
+
+ app.gui.restore_wallet(on_complete=on_complete)
+ except:
+ import traceback, sys
+ traceback.print_exc(file=sys.stdout)
+ app.stop()
+ if not interface.isAlive():
+ interface.start(wait=False)
+ interface.send([('server.peers.subscribe', [])])
+
+
+ def init_servers_list(self):
+ data = []
+ for _host, d in self.servers.items():
+ if d.get(self.protocol):
+ pruning_level = d.get('pruning', '')
+ data.append((_host, pruning_level))
+ self.servers_view.content_adapter.data = data
+
+ def set_protocol(self, protocol):
+ if protocol != self.protocol:
+ self.protocol = protocol
+ self.init_servers_list()
+
+ def on_change_protocol(self, instance, protocol_key):
+ p = protocol_key
+ host = unicode(self.server_host.text)
+ pp = self.servers.get(host)
+ if not pp:
+ return
+ if p not in pp.keys():
+ p = pp.keys()[0]
+ port = pp[p]
+ self.server_host.text = host
+ self.server_port.text = port
+ self.set_protocol(p)
+
+ def server_changed(self, instance):
+ try:
+ index = instance.selection[0].index
+ except (AttributeError, IndexError):
+ return
+ item = instance.get_data_item(index)
+ self.change_server(item[0], self.protocol)
+
+ def change_server(self, host, protocol):
+ pp = self.servers.get(host, DEFAULT_PORTS)
+ if protocol:
+ port = pp.get(protocol)
+ if not port: protocol = None
+
+ if not protocol:
+ if 's' in pp.keys():
+ protocol = 's'
+ port = pp.get(protocol)
+ else:
+ protocol = pp.keys()[0]
+ port = pp.get(protocol)
+
+ self.server_host.text = host
+ self.server_port.text = port
+ self.server_protocol.text = self.protocol_names[self.protocol_letters.index(protocol)]
+
+ if not self.servers: return
+ # TODO: what's this?
+ # for p in protocol_letters:
+ # i = protocol_letters.index(p)
+ # j = self.server_protocol.model().index(i,0)
+ # #if p not in pp.keys(): # and self.interface.is_connected:
+ # # self.server_protocol.model().setData(j, QVariant(0), Qt.UserRole-1)
+ # #else:
+ # # self.server_protocol.model().setData(j, QVariant(33), Qt.UserRole-1)
+
+class ScreenAddress(CScreen):
+
+ labels = DictProperty({})
+ '''
+ '''
+
+ tab = ObjectProperty(None)
+ ''' The tab associated With this Carousel
+ '''
+
+class ScreenConsole(CScreen):
+
+ pass
+
+
+class ScreenReceive(CScreen):
+
+ pass
+
+#TODO: move to wallet management
+class ScreenReceive2(CScreen):
+
+ receive_view = ObjectProperty(None)
+
+ def __init__(self, **kwargs):
+ self.context_menu = None
+ super(ScreenReceive, self).__init__(**kwargs)
+ self.app = App.get_running_app()
+
+ def on_receive_view(self, instance, value):
+ if not value:
+ return
+ value.on_context_menu = self.on_context_menu
+
+ def on_menu_item_selected(self, instance, _menu, _btn):
+ '''Called when any one of the bubble menu items is selected
+ '''
+ app = self.app
+ main_gui = app.gui.main_gui
+
+ def delete_imported_key():
+ def on_release(_dlg, _dlg_btn):
+ if _dlg_btn.text == _('Cancel'):
+ _dlg.close()
+ return
+ app.wallet.delete_imported_key(address)
+ main_gui.update_receive_tab()
+ main_gui.update_history_tab()
+
+ MessageBox(title=_('Delete imported key'),
+ message=_("Do you want to remove")
+ +" %s "%addr +_("from your wallet?"),
+ buttons=[_('Cancel'), _('OK')],
+ on_release=on_release).open()
+
+ def edit_label_dialog():
+ # Show dialog to edit the label
+ def save_label(_dlg, _dlg_btn):
+ if _dlg_btn.text != _('Ok'):
+ return
+ txt = _dlg.ids.ti.text
+ if txt:
+ instance.parent.children[2].text = txt
+ _dlg.close()
+
+ text = instance.parent.children[2].text
+ dialog = EditLabelDialog(text=text,
+ on_release=save_label).open()
+
+ def show_private_key_dialog():
+ # NOTE: equivalent to @protected
+ def protected_show_private_key(_instance=None, password=None):
+ try:
+ pk = app.wallet.get_private_key(address, password)
+ except BaseException, e:
+ app.show_info_bubble(text=str(e))
+ return
+
+ PrivateKeyDialog(address=address,
+ private_key=pk).open()
+
+ if app.wallet.use_encryption:
+ return main_gui.password_required_dialog(
+ post_ok=protected_show_private_key)
+ protected_show_private_key()
+
+ def show_sign_verify_dialog():
+ def on_release(_dlg, _dlg_btn):
+ if _dlg_btn.text != _('Ok'):
+ return
+ if _dlg.ids.tabs.current_tab.text == _('Sign'):
+ # NOTE: equivalent to @protected
+ def protected_do_sign_message(instance=None, password=None):
+ try:
+ sig = app.wallet.sign_message(
+ _dlg.ids.sign_address.text,
+ _dlg.ids.sign_message.text,
+ password)
+ _dlg.ids.sign_signature.text = sig
+ except BaseException, e:
+ app.show_info_bubble(text=str(e.message))
+
+ if app.wallet.use_encryption:
+ return main_gui.password_required_dialog(
+ post_ok=protected_do_sign_message)
+ return protected_do_sign_message()
+
+ else: # _('Verify')
+ if app.wallet.verify_message(
+ _dlg.ids.verify_address.text,
+ _dlg.ids.verify_signature.text,
+ _dlg.ids.verify_message.text):
+ app.show_info_bubble(text=_("Signature verified"))
+ else:
+ app.show_info_bubble(
+ text=_("Error: wrong signature"))
+ SignVerifyDialog(on_release=on_release, address=address).open()
+
+ def toggle_freeze():
+ if address in app.wallet.frozen_addresses:
+ app.wallet.unfreeze(address)
+ else:
+ app.wallet.freeze(address)
+ main_gui.update_receive_tab()
+
+ def toggle_priority(_dlg, _dlg_btn):
+ if address in app.wallet.prioritized_addresses:
+ app.wallet.unprioritize(address)
+ else:
+ app.wallet.prioritize(address)
+ main_gui.update_receive_tab()
+
+ _menu.hide()
+ address = instance.parent.children[3].text
+
+ if _btn.text == _('Copy to clipboard'):
+ # copy data to clipboard
+ Clipboard.put(instance.parent.children[3].text, 'UTF8_STRING')
+ elif _btn.text == _('Edit label'):
+ edit_label_dialog()
+ elif _btn.text == _('Private key'):
+ show_private_key_dialog()
+ elif _btn.text == _('Sign message'):
+ # sign message
+ show_sign_verify_dialog()
+ elif _btn.text == _('Remove_from_wallet'):
+ delete_imported_key()
+ elif _btn.text in (_('Freeze'), _('Unfreeze')):
+ toggle_freeze()
+ elif _btn.text in (_('Prioritize'), _('Unprioritize')):
+ toggle_priority(_menu, _btn)
+
+
+ def on_context_menu(self, instance):
+ '''Called when list item is clicked.
+ Objective: show bubble menu
+ '''
+ app = self.app
+ address = instance.parent.children[3].text
+ if not address or not is_valid(address): return
+
+ context_menu = ContextMenu(size_hint=(None, None),
+ size=('160dp', '160dp'),
+ orientation='vertical',
+ arrow_pos='left_mid',
+ buttons=[_('Copy to clipboard'),
+ _('Edit label'),
+ _('Private key'),
+ _('Sign message')],
+ on_release=partial(self.on_menu_item_selected,
+ instance))
+ if address in app.wallet.imported_keys:
+ context_menu.buttons = context_menu.buttons +\
+ [_('Remove from wallet')]
+ # TODO: test more this feature
+
+ if app.gui.main_gui.expert_mode:
+ # TODO: show frozen, prioritized rows in different color
+ # as original code
+
+ t = _("Unfreeze")\
+ if address in app.wallet.frozen_addresses else\
+ _("Freeze")
+ context_menu.buttons = context_menu.buttons + [t]
+ t = _("Unprioritize")\
+ if address in app.wallet.prioritized_addresses else\
+ _("Prioritize")
+ context_menu.buttons = context_menu.buttons + [t]
+ context_menu.show(pos=(instance.right, instance.top))
+
+
+class ScreenContacts(CScreen):
+
+ def add_new_contact(self):
+ NewContactDialog().open()
+
+
+
+class TabbedCarousel(TabbedPanel):
+
+ carousel = ObjectProperty(None)
+
+ def animate_tab_to_center(self, value):
+ scrlv = self._tab_strip.parent
+ if not scrlv:
+ return
+ self_center_x = scrlv.center_x
+ vcenter_x = value.center_x
+ diff_x = (self_center_x - vcenter_x)
+ try:
+ scroll_x = scrlv.scroll_x - (diff_x / scrlv.width)
+ except ZeroDivisionError:
+ pass
+ mation = Animation(scroll_x=max(0, min(scroll_x, 1)), d=.25)
+ mation.cancel_all(scrlv)
+ mation.start(scrlv)
+
+ def on_current_tab(self, instance, value):
+ if value.text == 'default_tab':
+ return
+ self.animate_tab_to_center(value)
+
+ def on_index(self, instance, value):
+ current_slide = instance.current_slide
+ if not hasattr(current_slide, 'tab'):
+ return
+ tab = current_slide.tab
+ ct = self.current_tab
+ try:
+ if ct.text != tab.text:
+ carousel = self.carousel
+ carousel.slides[ct.slide].dispatch('on_deactivate')
+ self.switch_to(tab)
+ carousel.slides[tab.slide].dispatch('on_activate')
+ except AttributeError:
+ current_slide.dispatch('on_activate')
+
+ def switch_to(self, header):
+ # we have to replace the functionality of the original switch_to
+ if not header:
+ return
+ if not hasattr(header, 'slide'):
+ header.content = self.carousel
+ super(TabbedCarousel, self).switch_to(header)
+ tab = self.tab_list[-1]
+ self._current_tab = tab
+ tab.state = 'down'
+ return
+
+ carousel = self.carousel
+ self.current_tab.state = "normal"
+ header.state = 'down'
+ self._current_tab = header
+ # set the carousel to load the appropriate slide
+ # saved in the screen attribute of the tab head
+ slide = carousel.slides[header.slide]
+ if carousel.current_slide != slide:
+ carousel.current_slide.dispatch('on_deactivate')
+ carousel.load_slide(slide)
+ slide.dispatch('on_activate')
+
+ def add_widget(self, widget, index=0):
+ if isinstance(widget, Screen):
+ self.carousel.add_widget(widget)
+ return
+ super(TabbedCarousel, self).add_widget(widget, index=index)
+
+
+class TabbedScreens(TabbedPanel):
+
+ manager = ObjectProperty(None)
+ '''Linked to the screen manager in kv'''
+
+ def switch_to(self, header):
+ # we don't use default tab so skip
+ if not hasattr(header, 'screen'):
+ header.content = self.manager
+ super(TabbedScreens, self).switch_to(header)
+ return
+ if not header.screen:
+ return
+ panel = self
+ panel.current_tab.state = "normal"
+ header.state = 'down'
+ panel._current_tab = header
+ self.manager.current = header.screen
+
+ def add_widget(self, widget, index=0):
+ if isinstance(widget, Screen):
+ self.manager.add_widget(widget)
+ return
+ super(TabbedScreens, self).add_widget(widget, index=index)
DIR diff --git a/gui/kivy/statusbar.py b/gui/kivy/statusbar.py
t@@ -0,0 +1,7 @@
+from kivy.uix.boxlayout import BoxLayout
+from kivy.properties import StringProperty
+
+
+class StatusBar(BoxLayout):
+
+ text = StringProperty('')
DIR diff --git a/gui/kivy/textinput.py b/gui/kivy/textinput.py
t@@ -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)
DIR diff --git a/gui/kivy/theming/light-0.png b/gui/kivy/theming/light-0.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light-1.png b/gui/kivy/theming/light-1.png
Binary files differ.
DIR diff --git a/gui/kivy/theming/light.atlas b/gui/kivy/theming/light.atlas
t@@ -0,0 +1 @@
parazyd.org:70 /git/electrum/commit/30126c544b81bcb8ac6a510da9c9d42acf82ee28.gph:4751: line too long