electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 30126c544b81bcb8ac6a510da9c9d42acf82ee28
parent 2a889d318be1e2d50945f49b565ccd09cebd69ff
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:
Agui/kivy/Makefile | 29+++++++++++++++++++++++++++++
Agui/kivy/Readme.txt | 5+++++
Agui/kivy/__init__.py | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/carousel.py | 41+++++++++++++++++++++++++++++++++++++++++
Agui/kivy/combobox.py | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/console.py | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/dialog.py | 611+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/drawer.py | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/gridview.py | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/installwizard.py | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/main.kv | 402+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/main_window.py | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/menus.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/nfc_scanner/__init__.py | 43+++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/nfc_scanner/scanner_android.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/nfc_scanner/scanner_dummy.py | 37+++++++++++++++++++++++++++++++++++++
Agui/kivy/qr_scanner/__init__.py | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/qr_scanner/scanner_android.py | 354+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/qr_scanner/scanner_camera.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/qrcodewidget.py | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/screens.py | 1095+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/statusbar.py | 7+++++++
Agui/kivy/textinput.py | 14++++++++++++++
Agui/kivy/theming/light-0.png | 0
Agui/kivy/theming/light-1.png | 0
Agui/kivy/theming/light.atlas | 2++
Agui/kivy/theming/light/action_bar.png | 0
Agui/kivy/theming/light/action_group_dark.png | 0
Agui/kivy/theming/light/add_contact.png | 0
Agui/kivy/theming/light/arrow_back.png | 0
Agui/kivy/theming/light/blue_bg_round_rb.png | 0
Agui/kivy/theming/light/btn_create_account.png | 0
Agui/kivy/theming/light/btn_create_act_disabled.png | 0
Agui/kivy/theming/light/btn_nfc.png | 0
Agui/kivy/theming/light/btn_send_address.png | 0
Agui/kivy/theming/light/btn_send_nfc.png | 0
Agui/kivy/theming/light/card.png | 0
Agui/kivy/theming/light/card_bottom.png | 0
Agui/kivy/theming/light/card_btn.png | 0
Agui/kivy/theming/light/card_top.png | 0
Agui/kivy/theming/light/carousel_deselected.png | 0
Agui/kivy/theming/light/carousel_selected.png | 0
Agui/kivy/theming/light/clock1.png | 0
Agui/kivy/theming/light/clock2.png | 0
Agui/kivy/theming/light/clock3.png | 0
Agui/kivy/theming/light/clock4.png | 0
Agui/kivy/theming/light/clock5.png | 0
Agui/kivy/theming/light/close.png | 0
Agui/kivy/theming/light/closebutton.png | 0
Agui/kivy/theming/light/confirmed.png | 0
Agui/kivy/theming/light/contact.png | 0
Agui/kivy/theming/light/create_act_text.png | 0
Agui/kivy/theming/light/create_act_text_active.png | 0
Agui/kivy/theming/light/dialog.png | 0
Agui/kivy/theming/light/electrum_icon640.png | 0
Agui/kivy/theming/light/error.png | 0
Agui/kivy/theming/light/gear.png | 0
Agui/kivy/theming/light/globe.png | 0
Agui/kivy/theming/light/icon_border.png | 0
Agui/kivy/theming/light/important.png | 0
Agui/kivy/theming/light/info.png | 0
Agui/kivy/theming/light/lightblue_bg_round_lb.png | 0
Agui/kivy/theming/light/logo.png | 0
Agui/kivy/theming/light/logo_atom_dull.png | 0
Agui/kivy/theming/light/mail_icon.png | 0
Agui/kivy/theming/light/manualentry.png | 0
Agui/kivy/theming/light/network.png | 0
Agui/kivy/theming/light/nfc.png | 0
Agui/kivy/theming/light/nfc_clock.png | 0
Agui/kivy/theming/light/nfc_phone.png | 0
Agui/kivy/theming/light/nfc_stage_one.png | 0
Agui/kivy/theming/light/paste_icon.png | 0
Agui/kivy/theming/light/pen.png | 0
Agui/kivy/theming/light/qrcode.png | 0
Agui/kivy/theming/light/settings.png | 0
Agui/kivy/theming/light/shadow.png | 0
Agui/kivy/theming/light/shadow_right.png | 0
Agui/kivy/theming/light/star_big_inactive.png | 0
Agui/kivy/theming/light/stepper_full.png | 0
Agui/kivy/theming/light/stepper_left.png | 0
Agui/kivy/theming/light/tab.png | 0
Agui/kivy/theming/light/tab_btn.png | 0
Agui/kivy/theming/light/tab_btn_disabled.png | 0
Agui/kivy/theming/light/tab_btn_pressed.png | 0
Agui/kivy/theming/light/tab_disabled.png | 0
Agui/kivy/theming/light/tab_strip.png | 0
Agui/kivy/theming/light/textinput_active.png | 0
Agui/kivy/theming/light/unconfirmed.png | 0
Agui/kivy/theming/light/wallet.png | 0
Agui/kivy/theming/light/wallets.png | 0
Agui/kivy/theming/light/white_bg_round_top.png | 0
Agui/kivy/theming/loading.gif | 0
Agui/kivy/theming/splash.png | 0
Agui/kivy/utils.py | 2++
94 files changed, 4590 insertions(+), 0 deletions(-)

diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile @@ -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 diff --git a/gui/kivy/Readme.txt b/gui/kivy/Readme.txt @@ -0,0 +1,5 @@ +Commands:: + + `make theming` to make a atlas out of a list of pngs + + `make apk` to make a apk diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py @@ -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() diff --git a/gui/kivy/carousel.py b/gui/kivy/carousel.py @@ -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 diff --git a/gui/kivy/combobox.py b/gui/kivy/combobox.py @@ -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() diff --git a/gui/kivy/console.py b/gui/kivy/console.py @@ -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) diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py @@ -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") diff --git a/gui/kivy/drawer.py b/gui/kivy/drawer.py @@ -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 diff --git a/gui/kivy/gridview.py b/gui/kivy/gridview.py @@ -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() diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py @@ -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 diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -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 diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -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) diff --git a/gui/kivy/menus.py b/gui/kivy/menus.py @@ -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) diff --git a/gui/kivy/nfc_scanner/__init__.py b/gui/kivy/nfc_scanner/__init__.py @@ -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') diff --git a/gui/kivy/nfc_scanner/scanner_android.py b/gui/kivy/nfc_scanner/scanner_android.py @@ -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() diff --git a/gui/kivy/nfc_scanner/scanner_dummy.py b/gui/kivy/nfc_scanner/scanner_dummy.py @@ -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') diff --git a/gui/kivy/qr_scanner/__init__.py b/gui/kivy/qr_scanner/__init__.py @@ -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) diff --git a/gui/kivy/qr_scanner/scanner_android.py b/gui/kivy/qr_scanner/scanner_android.py @@ -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() diff --git a/gui/kivy/qr_scanner/scanner_camera.py b/gui/kivy/qr_scanner/scanner_camera.py @@ -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) diff --git a/gui/kivy/qrcodewidget.py b/gui/kivy/qrcodewidget.py @@ -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)) + + diff --git a/gui/kivy/screens.py b/gui/kivy/screens.py @@ -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) diff --git a/gui/kivy/statusbar.py b/gui/kivy/statusbar.py @@ -0,0 +1,7 @@ +from kivy.uix.boxlayout import BoxLayout +from kivy.properties import StringProperty + + +class StatusBar(BoxLayout): + + text = StringProperty('') diff --git a/gui/kivy/textinput.py b/gui/kivy/textinput.py @@ -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) diff --git a/gui/kivy/theming/light-0.png b/gui/kivy/theming/light-0.png Binary files differ. diff --git a/gui/kivy/theming/light-1.png b/gui/kivy/theming/light-1.png Binary files differ. diff --git a/gui/kivy/theming/light.atlas b/gui/kivy/theming/light.atlas @@ -0,0 +1 @@ +{"light-1.png": {"icon_border": [475, 958, 64, 64], "tab_btn_disabled": [625, 924, 32, 32], "tab_btn_pressed": [693, 924, 32, 32], "btn_send_nfc": [1003, 968, 18, 15], "logo_atom_dull": [607, 958, 64, 64], "tab": [871, 958, 64, 64], "logo": [149, 894, 128, 128], "confirmed": [409, 958, 64, 64], "pen": [739, 958, 64, 64], "star_big_inactive": [279, 894, 128, 128], "action_group_dark": [2, 787, 33, 48], "mail_icon": [409, 902, 65, 54], "tab_btn": [659, 924, 32, 32], "btn_send_address": [1003, 985, 18, 15], "add_contact": [538, 913, 51, 43], "manualentry": [2, 888, 145, 134], "wallets": [727, 924, 32, 32], "shadow": [805, 958, 64, 64], "unconfirmed": [937, 958, 64, 64], "info": [541, 958, 64, 64], "nfc": [673, 958, 64, 64], "settings": [591, 924, 32, 32], "closebutton": [476, 913, 60, 43], "wallet": [53, 842, 49, 44], "contact": [2, 837, 49, 49], "dialog": [1003, 1002, 18, 20]}, "light-0.png": {"globe": [937, 65, 72, 72], "btn_create_account": [840, 394, 64, 32], "card_top": [770, 31, 32, 16], "qrcode": [805, 508, 145, 145], "close": [886, 168, 88, 88], "btn_create_act_disabled": [946, 394, 32, 32], "create_act_text": [998, 430, 22, 10], "card_bottom": [985, 150, 32, 16], "carousel_deselected": [952, 589, 64, 64], "network": [976, 208, 48, 48], "blue_bg_round_rb": [886, 146, 31, 20], "action_bar": [976, 170, 36, 36], "arrow_back": [974, 442, 50, 50], "card_btn": [906, 394, 38, 32], "tab_disabled": [644, 394, 96, 32], "lightblue_bg_round_lb": [919, 146, 31, 20], "white_bg_round_top": [952, 146, 31, 20], "tab_strip": [742, 394, 96, 32], "important": [770, 49, 88, 88], "gear": [644, 494, 159, 159], "stepper_left": [376, 20, 392, 117], "nfc_stage_one": [376, 258, 489, 122], "nfc_clock": [644, 655, 372, 367], "clock1": [644, 428, 64, 64], "clock2": [710, 428, 64, 64], "clock3": [776, 428, 64, 64], "clock4": [842, 428, 64, 64], "paste_icon": [860, 60, 75, 77], "carousel_selected": [952, 523, 64, 64], "card": [980, 394, 32, 32], "electrum_icon640": [2, 382, 640, 640], "btn_nfc": [1011, 125, 13, 12], "create_act_text_active": [974, 430, 22, 10], "stepper_full": [376, 139, 392, 117], "nfc_phone": [2, 13, 372, 367], "error": [867, 266, 128, 114], "textinput_active": [770, 142, 114, 114], "shadow_right": [952, 516, 32, 5], "clock5": [908, 428, 64, 64]}}+ \ No newline at end of file diff --git a/gui/kivy/theming/light/action_bar.png b/gui/kivy/theming/light/action_bar.png Binary files differ. diff --git a/gui/kivy/theming/light/action_group_dark.png b/gui/kivy/theming/light/action_group_dark.png Binary files differ. diff --git a/gui/kivy/theming/light/add_contact.png b/gui/kivy/theming/light/add_contact.png Binary files differ. diff --git a/gui/kivy/theming/light/arrow_back.png b/gui/kivy/theming/light/arrow_back.png Binary files differ. diff --git a/gui/kivy/theming/light/blue_bg_round_rb.png b/gui/kivy/theming/light/blue_bg_round_rb.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_create_account.png b/gui/kivy/theming/light/btn_create_account.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_create_act_disabled.png b/gui/kivy/theming/light/btn_create_act_disabled.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_nfc.png b/gui/kivy/theming/light/btn_nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_send_address.png b/gui/kivy/theming/light/btn_send_address.png Binary files differ. diff --git a/gui/kivy/theming/light/btn_send_nfc.png b/gui/kivy/theming/light/btn_send_nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/card.png b/gui/kivy/theming/light/card.png Binary files differ. diff --git a/gui/kivy/theming/light/card_bottom.png b/gui/kivy/theming/light/card_bottom.png Binary files differ. diff --git a/gui/kivy/theming/light/card_btn.png b/gui/kivy/theming/light/card_btn.png Binary files differ. diff --git a/gui/kivy/theming/light/card_top.png b/gui/kivy/theming/light/card_top.png Binary files differ. diff --git a/gui/kivy/theming/light/carousel_deselected.png b/gui/kivy/theming/light/carousel_deselected.png Binary files differ. diff --git a/gui/kivy/theming/light/carousel_selected.png b/gui/kivy/theming/light/carousel_selected.png Binary files differ. diff --git a/gui/kivy/theming/light/clock1.png b/gui/kivy/theming/light/clock1.png Binary files differ. diff --git a/gui/kivy/theming/light/clock2.png b/gui/kivy/theming/light/clock2.png Binary files differ. diff --git a/gui/kivy/theming/light/clock3.png b/gui/kivy/theming/light/clock3.png Binary files differ. diff --git a/gui/kivy/theming/light/clock4.png b/gui/kivy/theming/light/clock4.png Binary files differ. diff --git a/gui/kivy/theming/light/clock5.png b/gui/kivy/theming/light/clock5.png Binary files differ. diff --git a/gui/kivy/theming/light/close.png b/gui/kivy/theming/light/close.png Binary files differ. diff --git a/gui/kivy/theming/light/closebutton.png b/gui/kivy/theming/light/closebutton.png Binary files differ. diff --git a/gui/kivy/theming/light/confirmed.png b/gui/kivy/theming/light/confirmed.png Binary files differ. diff --git a/gui/kivy/theming/light/contact.png b/gui/kivy/theming/light/contact.png Binary files differ. diff --git a/gui/kivy/theming/light/create_act_text.png b/gui/kivy/theming/light/create_act_text.png Binary files differ. diff --git a/gui/kivy/theming/light/create_act_text_active.png b/gui/kivy/theming/light/create_act_text_active.png Binary files differ. diff --git a/gui/kivy/theming/light/dialog.png b/gui/kivy/theming/light/dialog.png Binary files differ. diff --git a/gui/kivy/theming/light/electrum_icon640.png b/gui/kivy/theming/light/electrum_icon640.png Binary files differ. diff --git a/gui/kivy/theming/light/error.png b/gui/kivy/theming/light/error.png Binary files differ. diff --git a/gui/kivy/theming/light/gear.png b/gui/kivy/theming/light/gear.png Binary files differ. diff --git a/gui/kivy/theming/light/globe.png b/gui/kivy/theming/light/globe.png Binary files differ. diff --git a/gui/kivy/theming/light/icon_border.png b/gui/kivy/theming/light/icon_border.png Binary files differ. diff --git a/gui/kivy/theming/light/important.png b/gui/kivy/theming/light/important.png Binary files differ. diff --git a/gui/kivy/theming/light/info.png b/gui/kivy/theming/light/info.png Binary files differ. diff --git a/gui/kivy/theming/light/lightblue_bg_round_lb.png b/gui/kivy/theming/light/lightblue_bg_round_lb.png Binary files differ. diff --git a/gui/kivy/theming/light/logo.png b/gui/kivy/theming/light/logo.png Binary files differ. diff --git a/gui/kivy/theming/light/logo_atom_dull.png b/gui/kivy/theming/light/logo_atom_dull.png Binary files differ. diff --git a/gui/kivy/theming/light/mail_icon.png b/gui/kivy/theming/light/mail_icon.png Binary files differ. diff --git a/gui/kivy/theming/light/manualentry.png b/gui/kivy/theming/light/manualentry.png Binary files differ. diff --git a/gui/kivy/theming/light/network.png b/gui/kivy/theming/light/network.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc.png b/gui/kivy/theming/light/nfc.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_clock.png b/gui/kivy/theming/light/nfc_clock.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_phone.png b/gui/kivy/theming/light/nfc_phone.png Binary files differ. diff --git a/gui/kivy/theming/light/nfc_stage_one.png b/gui/kivy/theming/light/nfc_stage_one.png Binary files differ. diff --git a/gui/kivy/theming/light/paste_icon.png b/gui/kivy/theming/light/paste_icon.png Binary files differ. diff --git a/gui/kivy/theming/light/pen.png b/gui/kivy/theming/light/pen.png Binary files differ. diff --git a/gui/kivy/theming/light/qrcode.png b/gui/kivy/theming/light/qrcode.png Binary files differ. diff --git a/gui/kivy/theming/light/settings.png b/gui/kivy/theming/light/settings.png Binary files differ. diff --git a/gui/kivy/theming/light/shadow.png b/gui/kivy/theming/light/shadow.png Binary files differ. diff --git a/gui/kivy/theming/light/shadow_right.png b/gui/kivy/theming/light/shadow_right.png Binary files differ. diff --git a/gui/kivy/theming/light/star_big_inactive.png b/gui/kivy/theming/light/star_big_inactive.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_full.png b/gui/kivy/theming/light/stepper_full.png Binary files differ. diff --git a/gui/kivy/theming/light/stepper_left.png b/gui/kivy/theming/light/stepper_left.png Binary files differ. diff --git a/gui/kivy/theming/light/tab.png b/gui/kivy/theming/light/tab.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn.png b/gui/kivy/theming/light/tab_btn.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn_disabled.png b/gui/kivy/theming/light/tab_btn_disabled.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_btn_pressed.png b/gui/kivy/theming/light/tab_btn_pressed.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_disabled.png b/gui/kivy/theming/light/tab_disabled.png Binary files differ. diff --git a/gui/kivy/theming/light/tab_strip.png b/gui/kivy/theming/light/tab_strip.png Binary files differ. diff --git a/gui/kivy/theming/light/textinput_active.png b/gui/kivy/theming/light/textinput_active.png Binary files differ. diff --git a/gui/kivy/theming/light/unconfirmed.png b/gui/kivy/theming/light/unconfirmed.png Binary files differ. diff --git a/gui/kivy/theming/light/wallet.png b/gui/kivy/theming/light/wallet.png Binary files differ. diff --git a/gui/kivy/theming/light/wallets.png b/gui/kivy/theming/light/wallets.png Binary files differ. diff --git a/gui/kivy/theming/light/white_bg_round_top.png b/gui/kivy/theming/light/white_bg_round_top.png Binary files differ. diff --git a/gui/kivy/theming/loading.gif b/gui/kivy/theming/loading.gif Binary files differ. diff --git a/gui/kivy/theming/splash.png b/gui/kivy/theming/splash.png Binary files differ. diff --git a/gui/kivy/utils.py b/gui/kivy/utils.py @@ -0,0 +1,2 @@ + +