diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile
new file mode 100644
index 000000000..fb11c3194
--- /dev/null
+++ 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
new file mode 100644
index 000000000..57746d25f
--- /dev/null
+++ 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
new file mode 100644
index 000000000..cdaf3bcc3
--- /dev/null
+++ 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 .
+#
+# 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
new file mode 100644
index 000000000..ed8f2e00c
--- /dev/null
+++ 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
new file mode 100644
index 000000000..26e9f1f63
--- /dev/null
+++ 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('''
+:
+ size_hint_y: None
+ height: 44
+
+:
+ 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
new file mode 100644
index 000000000..a553d979c
--- /dev/null
+++ 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
new file mode 100644
index 000000000..6dc9aec60
--- /dev/null
+++ 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
new file mode 100644
index 000000000..6fc15426e
--- /dev/null
+++ 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
new file mode 100644
index 000000000..567177bc2
--- /dev/null
+++ 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('''
+
+ 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]
+
+