electrum

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

commit c121c1aa4e2d20825d4eafc201144e84df3129dd
parent 9938316400ad263ab9bec40e93f7e0b10c2ed9cd
Author: akshayaurora <akshayaurora@gmail.com>
Date:   Thu,  5 Jun 2014 06:12:29 +0530

reorganize files and bring code inline with current master

Conflicts:
	lib/simple_config.py

Diffstat:
Mdata/fonts/Roboto-Bold.ttf | 0
Adata/fonts/Roboto-Condensed.ttf | 0
Adata/fonts/Roboto-Medium.ttf | 0
Mgui/kivy/Makefile | 2+-
Mgui/kivy/__init__.py | 54++++++++++++++++++++++++++++++++++++++++++++++++------
Dgui/kivy/carousel.py | 33---------------------------------
Dgui/kivy/dialog.py | 686-------------------------------------------------------------------------------
Dgui/kivy/drawer.py | 188-------------------------------------------------------------------------------
Dgui/kivy/gridview.py | 203-------------------------------------------------------------------------------
Dgui/kivy/installwizard.py | 328-------------------------------------------------------------------------------
Mgui/kivy/main.kv | 439+++++++++++--------------------------------------------------------------------
Mgui/kivy/main_window.py | 1023++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mgui/kivy/plugins/exchange_rate.py | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mgui/kivy/qr_scanner/__init__.py | 54++++--------------------------------------------------
Mgui/kivy/qr_scanner/scanner_android.py | 9+++++++--
Dgui/kivy/qrcodewidget.py | 179-------------------------------------------------------------------------------
Dgui/kivy/screens.py | 105-------------------------------------------------------------------------------
Dgui/kivy/statusbar.py | 7-------
Dgui/kivy/textinput.py | 14--------------
Mgui/kivy/theming/light-0.png | 0
Mgui/kivy/theming/light.atlas | 4++--
Mgui/kivy/theming/light/action_bar.png | 0
Agui/kivy/theming/light/action_button_group.png | 0
Agui/kivy/theming/light/action_group_light.png | 0
Agui/kivy/theming/light/bit_logo.png | 0
Mgui/kivy/theming/light/card.png | 0
Mgui/kivy/theming/light/card_top.png | 0
Mgui/kivy/theming/light/contact.png | 0
Agui/kivy/theming/light/contact_avatar.png | 0
Agui/kivy/theming/light/contact_overlay.png | 0
Agui/kivy/theming/light/dropdown_background.png | 0
Mgui/kivy/theming/light/gear.png | 0
Mgui/kivy/theming/light/logo.png | 0
Mgui/kivy/theming/light/manualentry.png | 0
Mgui/kivy/theming/light/nfc_phone.png | 0
Agui/kivy/theming/light/overflow_background.png | 0
Agui/kivy/theming/light/overflow_btn_dn.png | 0
Mgui/kivy/theming/light/qrcode.png | 0
Mgui/kivy/theming/light/settings.png | 0
Mgui/kivy/theming/light/tab_btn_pressed.png | 0
Mgui/kivy/theming/light/tab_strip.png | 0
Mgui/kivy/theming/light/wallets.png | 0
Agui/kivy/tools/blacklist.txt | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/tools/buildozer.spec | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgui/kivy/ui_screens/mainscreen.kv | 287-------------------------------------------------------------------------------
Agui/kivy/ui_screens/screenreceive.kv | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/ui_screens/screensend.kv | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/__init__.py | 1+
Rgui/kivy/combobox.py -> gui/kivy/uix/combobox.py | 0
Rgui/kivy/console.py -> gui/kivy/uix/console.py | 0
Agui/kivy/uix/dialogs/__init__.py | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/carousel_dialog.py | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/create_restore.py | 488+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/installwizard.py | 478+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/new_contact.py | 26++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/nfc_transaction.py | 33+++++++++++++++++++++++++++++++++
Agui/kivy/uix/dialogs/qr_scanner.py | 42++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/drawer.py | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/gridview.py | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rgui/kivy/menus.py -> gui/kivy/uix/menus.py | 0
Agui/kivy/uix/qrcodewidget.py | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/screens.py | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/mainscreen.kv | 1406+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/screenreceive.kv | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/screensend.kv | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgui/kivy/utils.py | 2--
Alib/android/libiconv.so | 0
Alib/android/libzbarjni.so | 0
Alib/android/zbar.jar | 0
69 files changed, 5738 insertions(+), 2781 deletions(-)

diff --git a/data/fonts/Roboto-Bold.ttf b/data/fonts/Roboto-Bold.ttf Binary files differ. diff --git a/data/fonts/Roboto-Condensed.ttf b/data/fonts/Roboto-Condensed.ttf Binary files differ. diff --git a/data/fonts/Roboto-Medium.ttf b/data/fonts/Roboto-Medium.ttf Binary files differ. diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile @@ -9,7 +9,7 @@ apk: # running pre build setup @cp tools/buildozer.spec ../../buildozer.spec # get aes.py - @cd ../..; wget -4 https://raw.github.com/devrandom/slowaes/master/python/aes.py + @cd ../..; curl -O https://raw.github.com/devrandom/slowaes/master/python/aes.py # rename electrum to main.py @mv ../../electrum ../../main.py @-if [ ! -d "../../.buildozer" ];then \ diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py @@ -21,7 +21,7 @@ import sys #, time, datetime, re, threading #from electrum.i18n import _, set_language -#from electrum.util import print_error, print_msg, parse_url +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 @@ -42,9 +42,8 @@ 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 +#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 @@ -60,7 +59,6 @@ from electrum.plugins import init_plugins # return True # return False - class ElectrumGui: def __init__(self, config, network, app=None): @@ -74,6 +72,47 @@ class ElectrumGui: # base #init_plugins(self) + def set_url(self, url): + from electrum import util + from decimal import Decimal + + try: + address, amount, label, message,\ + request_url, url = util.parse_url(url) + except Exception: + self.main_window.show_error(_('Invalid bitcoin URL')) + return + + if amount: + try: + if main_window.base_unit == 'mBTC': + amount = str( 1000* Decimal(amount)) + else: + amount = str(Decimal(amount)) + except Exception: + amount = "0.0" + self.main_window.show_error(_('Invalid Amount')) + + if request_url: + try: + from electrum import paymentrequest + except: + self.main_window.show_error("cannot import payment request") + request_url = None + + if not request_url: + self.main_window.set_send(address, amount, label, message) + return + + def payment_request(): + self.payment_request = paymentrequest.PaymentRequest(request_url) + if self.payment_request.verify(): + Clock.schedule_once(self.main_window.payment_request_ok) + else: + Clock.schedule_once(self.main_window.payment_request_error) + + threading.Thread(target=payment_request).start() + self.main_window.prepare_for_payment_request() def main(self, url): ''' The main entry point of the kivy ux @@ -83,5 +122,7 @@ class ElectrumGui: ''' self.main_window = w = ElectrumWindow(config=self.config, - network=self.network) - w.run() + network=self.network, + url=url, + gui_object=self) + w.run()+ \ No newline at end of file diff --git a/gui/kivy/carousel.py b/gui/kivy/carousel.py @@ -1,32 +0,0 @@ -from kivy.uix.carousel import Carousel -from kivy.clock import Clock - -class Carousel(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- \ No newline at end of file diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py @@ -1,686 +0,0 @@ -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 '' - - :attr:`icon` is a `StringProperty` defaults to `''` - ''' - - fs = BooleanProperty(False) - ''' Show Bubble in half screen mode - - :attr:`fs` is a `BooleanProperty` defaults to `False` - ''' - - modal = BooleanProperty(False) - ''' Allow bubble to be hidden on touch. - - :attr:`modal` is a `BooleanProperty` defauult to `False`. - ''' - - exit = BooleanProperty(False) - '''Indicates whether to exit app after bubble is closed. - - :attr:`exit` is a `BooleanProperty` defaults to False. - ''' - - dim_background = BooleanProperty(False) - ''' Indicates Whether to draw a background on the windows behind the bubble. - - :attr:`dim` is a `BooleanProperty` defaults to `False`. - ''' - - def on_touch_down(self, touch): - if self.modal: - return True - self.hide() - if self.collide_point(*touch.pos): - return True - - def show(self, pos, duration, width=None, modal=False, exit=False): - '''Animate the bubble into position''' - self.modal, self.exit = modal, exit - if width: - self.width = width - if self.modal: - from kivy.uix.modalview import ModalView - self._modal_view = m = ModalView() - Window.add_widget(m) - m.add_widget(self) - else: - 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, now=False): - ''' Auto fade out the Bubble - ''' - def on_stop(*l): - if self.modal: - m = self._modal_view - m.remove_widget(self) - Window.remove_widget(m) - Window.remove_widget(self) - if self.exit: - App.get_running_app().stop() - import sys - sys.exit() - if now: - return on_stop() - - 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 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 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 VerifySeedDialog(CreateAccountDialog): - - pass - -class RestoreSeedDialog(CreateAccountDialog): - - def on_parent(self, instance, value): - if value: - tis = self.ids.text_input_seed - tis.focus = True - tis._keyboard.bind(on_key_down=self.on_key_down) - stepper = self.ids.stepper - stepper.opacity = 1 - stepper.source = ('atlas://gui/kivy/theming' - '/light/stepper_restore_seed') - self._back = _back = partial(self.ids.back.dispatch, 'on_release') - app.navigation_higherarchy.append(_back) - - def on_key_down(self, keyboard, keycode, key, modifiers): - if keycode[0] in (13, 271): - self.on_enter() - return True - #super - - def on_enter(self): - #self._remove_keyboard() - # press next - self.ids.next.dispatch('on_release') - - def _remove_keyboard(self): - tis = self.ids.text_input_seed - if tis._keyboard: - tis._keyboard.unbind(on_key_down=self.on_key_down) - tis.focus = False - - def close(self): - self._remove_keyboard() - if self._back in app.navigation_higherarchy: - app.navigation_higherarchy.pop() - self._back = None - super(RestoreSeedDialog, self).close() - -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', 'restore')) - ''' Defines the mode of the password dialog.''' - - def validate_new_password(self): - self.ids.next.dispatch('on_release') - - def on_parent(self, instance, value): - if value: - stepper = self.ids.stepper - stepper.opacity = 1 - t_wallet_name = self.ids.ti_wallet_name - if self.mode in ('create', 'restore'): - t_wallet_name.text = 'Default Wallet' - t_wallet_name.readonly = True - self.ids.ti_new_password.focus = True - else: - t_wallet_name.text = '' - t_wallet_name.readonly = False - t_wallet_name.focus = True - 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 @@ -1,187 +0,0 @@ - -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('9dp') - '''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 - - if not self.collide_point(*touch.pos): - return - - touch.grab(self) - - 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) - - state = self.state - touch.ud['send_touch_down'] = False - start = 0 if state[0] == 'c' else self._hidden_widget.right - drag_area = ((self.width * self.drag_area) - if self.state[0] == 'c' else - self.width) - if touch.x not in range(int(start), int(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): - if not touch.grab_current: - return - - # 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) - - ov = self._overlay_widget - ov.x=min(self._hidden_widget.width, - max(ov.x + touch.dx*2, 0)) - #_anim = Animation(x=x, duration=1/2, t='in_out_quart') - #_anim.cancel_all(ov) - #_anim.start(ov) - - 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): - if not touch.grab_current: - return - - # 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 @@ -1,203 +0,0 @@ -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 @@ -1,328 +0,0 @@ -from electrum import Wallet -from electrum.i18n import _ - -from kivy.app import App -from kivy.uix.widget import Widget -from kivy.core.window import Window -from kivy.clock import Clock - -from electrum_gui.kivy.dialog import CreateRestoreDialog -#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): - '''Installation Wizard. Responsible for instantiating the - creation/restoration of wallets. - - events:: - `on_wizard_complete` Fired when the wizard is done creating/ restoring - wallet/s. - ''' - - __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."), - on_complete=None): - '''Perform a blocking task in the background by running the passed - method in a thread. - ''' - - def target(): - - # run your threaded function - try: - task() - except Exception as err: - Clock.schedule_once(lambda dt: app.show_error(str(err))) - - # on completion hide message - Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) - - # call completion routine - if on_complete: - Clock.schedule_once(lambda dt: on_complete()) - - 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): - '''Entry point of our Installation wizard - ''' - CreateRestoreDialog(on_release=self.on_creatrestore_complete).open() - - def on_creatrestore_complete(self, dialog, button): - if not button: - return self.dispatch('on_wizard_complete', None) - - #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 - wallet = Wallet(self.storage) - self.change_password_dialog(wallet=wallet) - elif button == dialog.ids.restore: - # restore - wallet = None - self.restore_seed_dialog(wallet) - #if button == dialog.ids.watching: - #TODO: not available in the new design - # self.action = 'watching' - else: - self.dispatch('on_wizard_complete', None) - - def restore_seed_dialog(self, wallet): - from electrum_gui.kivy.dialog import RestoreSeedDialog - RestoreSeedDialog( - on_release=partial(self.on_verify_restore_ok, wallet)).open() - - def on_verify_restore_ok(self, wallet, _dlg, btn, restore=False): - - if _dlg.ids.back == btn: - _dlg.close() - CreateRestoreDialog( - on_release=self.on_creatrestore_complete).open() - return - - seed = unicode(_dlg.ids.text_input_seed.text) - if not seed: - app.show_error(_("No seed!"), duration=.5) - return - - try: - wallet = Wallet.from_seed(seed, self.storage) - except Exception as err: - _dlg.close() - return app.show_error(str(err) + '\n App will now exit', - exit=True, modal=True, duration=.5) - _dlg.close() - return self.change_password_dialog(wallet=wallet, mode='restore') - - - def init_seed_dialog(self, wallet=None, instance=None, password=None, - wallet_name=None, mode='create'): - # 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 app.show_error(_('No seed')) - - if password is None or not instance: - seed = wallet.get_mnemonic(None) - else: - try: - seed = self.wallet.get_seed(password) - except Exception: - return app.show_error(_('Incorrect Password')) - - brainwallet = seed - - msg2 = _("[color=#414141]"+\ - "[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][b]lost forever![/b][/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: - if not instance: - self.change_password_dialog(wallet) - return - # confirm - 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), - on_complete=partial(self.load_network, wallet, mode=mode)) - - from electrum_gui.kivy.dialog import InitSeedDialog - InitSeedDialog(message=msg2, - seed_msg=brainwallet, seed=seed, on_release=on_ok_press).open() - - def change_password_dialog(self, wallet=None, instance=None, mode='create'): - """Can be called directly (instance is None) - or from a callback (instance is not None)""" - - if instance and not wallet.seed: - return ShowError(_('No seed !!'), exit=True, modal=True) - - 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.") - - 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: - if mode == 'restore': - # back is disabled cause seed is already set - return - _dlg.close() - if not instance: - # back on create - 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'), duration=.5) - - if mode == 'restore': - def on_complete(*l): - _dlg.close() - self.load_network(wallet, mode='restore') - - self.waiting_dialog(lambda: wallet.save_seed(new_password), - msg=_("saving seed"), - on_complete=on_complete) - return - if not instance: - # create - _dlg.close() - #self.load_network(wallet, mode='create') - return self.init_seed_dialog(password=new_password, - wallet=wallet, wallet_name=wallet_name, mode=mode) - - try: - seed = wallet.decode_seed(password) - except BaseException: - return app.show_error(_('Incorrect Password'), duration=.5) - - # test carefully - try: - wallet.update_password(seed, password, new_password) - except BaseException: - return app.show_error(_('Failed to update password'), exit=True) - 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.update_wallet() - - from electrum_gui.kivy.dialog import ChangePasswordDialog - cpd = ChangePasswordDialog( - message=msg, - mode=mode, - on_release=on_release).open() - - def load_network(self, wallet, mode='create'): - #if not self.config.get('server'): - if self.network: - if self.network.interfaces: - if mode not in ('restore', 'create'): - self.network_dialog() - else: - app.show_error(_('You are offline')) - self.network.stop() - self.network = None - - if mode in ('restore', 'create'): - # auto cycle - self.config.set_key('auto_cycle', True, True) - - # start wallet threads - wallet.start_threads(self.network) - - if not mode == 'restore': - return self.dispatch('on_wizard_complete', wallet) - - def get_text(text): - def set_text(*l): app.info_bubble.ids.lbl.text=text - Clock.schedule_once(set_text) - - def on_complete(*l): - if not self.network: - app.show_info( - _("This wallet was restored offline. It may contain more" - " addresses than displayed."), duration=.5) - return self.dispatch('on_wizard_complete', wallet) - - if wallet.is_found(): - app.show_info(_("Recovery successful"), duration=.5) - else: - app.show_info(_("No transactions found for this seed"), - duration=.5) - return self.dispatch('on_wizard_complete', wallet) - - self.waiting_dialog(lambda: wallet.restore(get_text), - on_complete=on_complete) - - def on_wizard_complete(self, wallet): - pass diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -1,7 +1,4 @@ #:import Window kivy.core.window.Window -#:import _ electrum.i18n._ -#:import partial functools.partial - # Custom Global Widgets @@ -22,37 +19,36 @@ if root.state == 'normal' else 'icon_border') size: root.size pos: root.pos -########################### -## Gloabal Defaults -########################### - -<Label> - markup: True - font_name: 'Roboto' - font_size: '16sp' -<ListItemButton> - font_size: '12sp' - -######################### -# Dialogs -######################### - -################################################ -## Create Dialogs -################################################ +<Butt_star@ActionToggleButton>: + important: True + size_hint_x: None + width: '32dp' + mipmap: True + state: 'down' if app.expert_mode else 'normal' + background_down: self.background_normal + foreground_color: (.466, .466, .466, 1) + color_active: (0.235, .588, .89, 1) + on_release: app.expert_mode = True if self.state == 'down' else False + Image: + source: 'atlas://gui/kivy/theming/light/star_big_inactive' + center: root.center + size: root.width/1.5, self.width + color: + root.foreground_color if root.state == 'normal' else root.color_active + canvas.after: + Color: + rgba: 1, 1, 1, 1 + source: + allow_stretch: True + +<ELTextInput> + padding: '10dp', '4dp' + background_color: (0.238, 0.589, .996, 1) if self.focus else self.foreground_color + foreground_color: 0.531, 0.531, 0.531, 1 + background_active: 'atlas://gui/kivy/theming/light/textinput_active' + background_normal: 'atlas://gui/kivy/theming/light/textinput_active' -<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: @@ -75,26 +71,40 @@ text_size: self.size halign: 'center' valign: 'middle' + root: None 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) + on_press: if self.root: self.root.dispatch('on_press', self) + on_release: if self.root: 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) +########################### +## Gloabal Defaults +########################### +<TextInput> + on_focus: app._focused_widget = root + +<Label> + markup: True + font_name: 'Roboto' + font_size: '16sp' + +<ListItemButton> + font_size: '12sp' + +######################### +# Dialogs +######################### <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)) BoxLayout: - padding: '5dp' + padding: '5dp' if root.fs else 0 Widget: size_hint: None, 1 width: '4dp' if root.fs else '2dp' @@ -117,346 +127,11 @@ size_hint: 1, 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: None, None - size: '5dp', '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 - -<RestoreSeedDialog> - GridLayout - # leave room for future selection of gap through a widget - # removed for mobile - id: text_input_gap - text: '5' - - cols: 1 - padding: 0, '12dp' - orientation: 'vertical' - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - CreateAccountTextInput: - id: text_input_seed - size_hint: 1, None - height: '110dp' - hint_text: - _('Enter your seedphrase') - Label: - font_size: '12sp' - text_size: self.width, None - size_hint: 1, None - height: self.texture_size[1] - halign: 'justify' - valign: 'middle' - text: - _('If you need additional information, please check ' - '[color=#0000ff][ref=1]' - 'https://electrum.org/faq.html#seed[/ref][/color]') - on_ref_press: - import webbrowser - webbrowser.open('https://electrum.org/faq.html#seed') - 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 - -<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' - GridLayout: - size_hint_y: None - height: self.minimum_height - cols: 1 - 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 - Widget: - size_hint_y: None - height: '13dp' - 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', 'restore') else False - opacity: 0 if self.disabled else 1 - multiline: False - on_text_validate: - #root.validate_old_password() - ti_new_password.focus = True - Widget: - size_hint_y: None - height: 0 if ti_password.disabled else '13dp' - CreateAccountTextInput: - id: ti_new_password - hint_text: 'Enter new pincode' - multiline: False - password: True - on_text_validate: ti_confirm_password.focus = True - Widget: - size_hint_y: None - height: '13dp' - CreateAccountTextInput: - id: ti_confirm_password - hint_text: 'Confirm pincode' - password: True - multiline: False - on_text_validate: root.validate_new_password() - Widget - GridLayout: - rows: 1 - spacing: '12dp' - size_hint: 1, None - height: self.minimum_height - CreateAccountButtonBlue: - id: back - text: _('Back') - root: root - disabled: True if root.mode[0] == 'r' else self.disabled - CreateAccountButtonGreen: - id: next - text: _('Confirm') if root.mode[0] == 'r' else _('Next') - root: root - -############################################### -## Wallet Management -############################################### - -<WalletManagement@ScrollView> +StencilView: + manager: None 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 - 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 - BoxLayout: - x: wallet_management.width if app.ui_mode[0] == 't' else 0 - width: (root.width - self.x) if app.ui_mode[0] == 't' else root.width - size_hint: None, None - height: root.height - canvas.before: - Color - rgba: 1, 1, 1, 1 - BorderImage - border: 0, 32, 0, 0 - source: 'atlas://gui/kivy/theming/light/shadow_right' - pos: root.pos - size: self.x, self.height - ScreenManager: - id: manager- \ No newline at end of file + rgba: 1, 1, 1, 1 + Rectangle + size: self.size + pos: self.pos+ \ No newline at end of file diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -1,35 +1,88 @@ import sys -from decimal import Decimal +import datetime from electrum import WalletStorage, Wallet from electrum.i18n import _, set_language -from electrum.wallet import format_satoshis from kivy.app import App from kivy.core.window import Window -from kivy.lang import Builder from kivy.logger import Logger -from kivy.metrics import inch from kivy.utils import platform from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty, - StringProperty, ListProperty) + StringProperty, ListProperty, BooleanProperty) +from kivy.cache import Cache from kivy.clock import Clock +from kivy.factory import Factory -#inclusions for factory so that widgets can be used in kv -from electrum_gui.kivy.drawer import Drawer -from electrum_gui.kivy.dialog import InfoBubble +from electrum_gui.kivy.uix.drawer import Drawer -# delayed imports -notification = None +# lazy imports for factory so that widgets can be used in kv +Factory.register('InstallWizard', + module='electrum_gui.kivy.uix.dialogs.installwizard') +Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs') +Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens') + +# delayed imports: for startup speed on android +notification = app = Decimal = ref = format_satoshis = is_valid = Builder = None +inch = None +util = False +re = None + +# register widget cache for keeping memory down timeout to 4 minutes to cache +# the data +Cache.register('electrum_widgets', timeout=240) class ElectrumWindow(App): - title = _('Electrum App') + def _get_bu(self): + assert self.decimal_point in (5,8) + return "BTC" if self.decimal_point == 8 else "mBTC" - wallet = ObjectProperty(None) - '''Holds the electrum wallet + def _set_bu(self, value): + try: + self.electrum_config.set_key('base_unit', value, True) + except AttributeError: + Logger.error('Electrum: Config not set ' + 'While trying to save value to config') - :attr:`wallet` is a `ObjectProperty` defaults to None. + base_unit = AliasProperty(_get_bu, _set_bu, bind=('decimal_point',)) + '''BTC or UBTC or mBTC... + + :attr:`base_unit` is a `AliasProperty` defaults to the unit set in + electrum config. + ''' + + currencies = ListProperty(['EUR', 'GBP', 'USD']) + '''List of currencies supported by the current exchanger plugin. + + :attr:`currencies` is a `ListProperty` default to ['Eur', 'GBP'. 'USD']. + ''' + + expert_mode = BooleanProperty(False) + '''This defines whether expert mode options are available in the ui. + + :attr:`expert_mode` is a `BooleanProperty` defaults to `False`. + ''' + + def _get_decimal(self): + try: + return self.electrum_config.get('decimal_point', 8) + except AttributeError: + return 8 + + def _set_decimal(self, value): + try: + self.electrum_config.set_key('decimal_point', value, True) + except AttributeError: + Logger.error('Electrum: Config not set ' + 'While trying to save value to config') + + decimal_point = AliasProperty(_get_decimal, _set_decimal) + '''This defines the decimal point to be used determining the + :attr:`decimal_point`. + + :attr:`decimal_point` is a `AliasProperty` defaults to the value gotten + from electrum config. ''' electrum_config = ObjectProperty(None) @@ -61,43 +114,26 @@ class ElectrumWindow(App): '''Number of zeros used while representing the value in base_unit. ''' - def _get_decimal(self): - try: - return self.electrum_config.get('decimal_point', 8) - except AttributeError: - return 8 - - def _set_decimal(self, value): - try: - self.electrum_config.set_key('decimal_point', value, True) - except AttributeError: - Logger.error('Electrum: Config not set ' - 'While trying to save value to config') - - decimal_point = AliasProperty(_get_decimal, _set_decimal) - '''This defines the decimal point to be used determining the - :attr:`base_unit`. + navigation_higherarchy = ListProperty([]) + '''This is a list of the current navigation higherarchy of the app used to + navigate using back button. - :attr:`decimal_point` is a `AliasProperty` defaults to the value gotten - from electrum config. + :attr:`navigation_higherarchy` is s `ListProperty` defaults to [] ''' - def _get_bu(self): - assert self.decimal_point in (5,8) - return "BTC" if self.decimal_point == 8 else "mBTC" + _orientation = OptionProperty('landscape', + options=('landscape', 'portrait')) - def _set_bu(self, value): - try: - self.electrum_config.set_key('base_unit', value, True) - except AttributeError: - Logger.error('Electrum: Config not set ' - 'While trying to save value to config') + def _get_orientation(self): + return self._orientation - base_unit = AliasProperty(_get_bu, _set_bu, bind=('decimal_point',)) - '''BTC or UBTC or mBTC... + 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`. - :attr:`base_unit` is a `AliasProperty` defaults to the unit set in - electrum config. + :data:`orientation` is a read only `AliasProperty` Defaults to 'landscape' ''' _ui_mode = OptionProperty('phone', options=('tablet', 'phone')) @@ -114,51 +150,58 @@ class ElectrumWindow(App): :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' + url = StringProperty('', allownone=True) + ''' ''' - navigation_higherarchy = ListProperty([]) - '''This is a list of the current navigation higherarchy of the app used to - navigate using back button. + wallet = ObjectProperty(None) + '''Holds the electrum wallet - :attr:`navigation_higherarchy` is s `ListProperty` defaults to [] + :attr:`wallet` is a `ObjectProperty` defaults to None. ''' __events__ = ('on_back', ) def __init__(self, **kwargs): # initialize variables - self.info_bubble = None + self._clipboard = None self.console = None self.exchanger = None + self.info_bubble = None + self.qrscanner = None + self.nfcscanner = None + self.tabs = None super(ElectrumWindow, self).__init__(**kwargs) - self.network = network = kwargs.get('network') - self.electrum_config = config = kwargs.get('config') + title = _('Electrum App') + self.network = network = kwargs.get('network', None) + self.electrum_config = config = kwargs.get('config', None) + self.gui_object = kwargs.get('gui_object', None) + + self.bind(url=self.set_url) + # were we sent a url? + url = kwargs.get('url', None) + if url: + self.gui_object.set_url(url) # create triggers so as to minimize updation a max of 2 times a sec + self._trigger_update_wallet =\ + Clock.create_trigger(self.update_wallet, .5) self._trigger_update_status =\ Clock.create_trigger(self.update_status, .5) self._trigger_update_console =\ Clock.create_trigger(self.update_console, .5) self._trigger_notify_transactions = \ - Clock.create_trigger(self.notify_transactions, .5) + Clock.create_trigger(self.notify_transactions, 5) + + def set_url(self, instance, url): + self.gui_object.set_url(url) def build(self): - from kivy.lang import Builder + global Builder + if not Builder: + from kivy.lang import Builder return Builder.load_file('gui/kivy/main.kv') def _pause(self): @@ -172,11 +215,13 @@ class ElectrumWindow(App): def on_start(self): ''' This is the start point of the kivy ui ''' - Window.bind(size=self.on_size, + win = Window + win.bind(size=self.on_size, on_keyboard=self.on_keyboard) - Window.bind(on_key_down=self.on_key_down) + win.bind(on_key_down=self.on_key_down) - # register fonts + # Register fonts without this you won't be able to use bold/italic... + # inside markup. from kivy.core.text import Label Label.register('Roboto', 'data/fonts/Roboto.ttf', @@ -185,23 +230,29 @@ class ElectrumWindow(App): 'data/fonts/Roboto-Bold.ttf') if platform == 'android': - # - Window.bind(keyboard_height=self.on_keyboard_height) - self.on_size(Window, Window.size) + # bind to keyboard height so we can get the window contents to + # behave the way we want when the keyboard appears. + win.bind(keyboard_height=self.on_keyboard_height) + + self.on_size(win, win.size) config = self.electrum_config storage = WalletStorage(config) Logger.info('Electrum: Check for existing wallet') - if not storage.file_exists: + + if storage.file_exists: + wallet = Wallet(storage) + action = wallet.get_action() + else: + action = 'new' + + if action is not None: # start installation wizard Logger.debug('Electrum: Wallet not found. Launching install wizard') - import installwizard - wizard = installwizard.InstallWizard(config, self.network, - storage) + wizard = Factory.InstallWizard(config, self.network, storage) wizard.bind(on_wizard_complete=self.on_wizard_complete) - wizard.run() + wizard.run(action) else: - wallet = Wallet(storage) wallet.start_threads(self.network) self.on_wizard_complete(None, wallet) @@ -220,15 +271,22 @@ class ElectrumWindow(App): # 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_height(self, window, height): + win = window + active_widg = win.children[0] + if not issubclass(active_widg.__class__, Factory.Popup): + try: + active_widg = self.root.children[0] + except IndexError: + return + + try: + fw = self._focused_widget + except AttributeError: + return + if height > 0 and fw.to_window(*fw.pos)[1] > height: + return + Factory.Animation(y=win.keyboard_height, d=.1).start(active_widg) def on_key_down(self, instance, key, keycode, codepoint, modifiers): if 'ctrl' in modifiers: @@ -261,6 +319,7 @@ class ElectrumWindow(App): def on_wizard_complete(self, instance, wallet): if not wallet: Logger.debug('Electrum: No Wallet set/found. Exiting...') + app = App.get_running_app() app.show_error('Electrum: No Wallet set/found. Exiting...', exit=True) @@ -271,21 +330,6 @@ class ElectrumWindow(App): self.load_wallet(wallet) - #TODO: URI handling - #self.windows.append(w) - #if url: w.set_url(url) - - # TODO:remove properties are used instead - #Clock.schedule_interval(self.timer_actions, .5) - - - #TODO: remove not needed properties allow on_property events - #def timer_actions(self): - # if self.need_update.is_set(): - # self.update_wallet() - # self.need_update.clear() - # run_hook('timer_actions') - def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic tasks of setting up the ui. @@ -297,49 +341,70 @@ class ElectrumWindow(App): #self._tray_icon = 'icons/" + (electrum_dark_icon.png'\ # if platform == 'mac' else 'electrum_light_icon.png') - #setup tray + #setup tray TODO: use the systray branch #self.tray = SystemTrayIcon(self.icon, self) #self.tray.setToolTip('Electrum') #self.tray.activated.connect(self.tray_activated) + global ref + if not ref: + from weakref import ref + set_language(self.electrum_config.get('language')) self.funds_error = False self.completions = [] # setup UX - self.screens = ['mainscreen'] - self.load_screen(index=0) - - self.icon = "icons/electrum.png" + self.screens = ['mainscreen',] + + #setup lazy imports for mainscreen + Factory.register('AnimatedPopup', + module='electrum_gui.kivy.uix.dialogs') + Factory.register('TabbedCarousel', + module='electrum_gui.kivy.uix.screens') + Factory.register('ScreenDashboard', + module='electrum_gui.kivy.uix.screens') + Factory.register('EffectWidget', + module='electrum_gui.kivy.uix.effectwidget') # load and focus the ui + #Load mainscreen + + Factory.register('QRCodeWidget', + module='electrum_gui.kivy.uix.qrcodewidget') + Factory.register('MainScreen', + module='electrum_gui.kivy.uix.screens') + Factory.register('CSpinner', + module='electrum_gui.kivy.uix.screens') + + dr = Builder.load_file('gui/kivy/uix/ui_screens/mainscreen.kv') + self.root.add_widget(dr) + self.root.manager = manager = dr.ids.manager + self.root.main_screen = m = manager.screens[0] + self.tabs = m.ids.tabs + + #TODO + # load left_menu + + self.icon = "icons/electrum.png" # connect callbacks if self.network: - self.network.register_callback( - 'updated', self._trigger_update_status) - self.network.register_callback( - 'banner', self._trigger_update_console) - self.network.register_callback( - 'disconnected', self._trigger_update_status) - self.network.register_callback( - 'disconnecting', self._trigger_update_status) - self.network.register_callback('new_transaction', - self._trigger_notify_transactions) + self.network.register_callback('updated', self._trigger_update_wallet) + #self.network.register_callback('banner', self.console.show_message(self.network.banner)) + self.network.register_callback('disconnected', self._trigger_update_status) + self.network.register_callback('disconnecting', self._trigger_update_status) + self.network.register_callback('new_transaction', self._trigger_notify_transactions) # set initial message - self.update_console() + #self.console.show_message(self.network.banner) self.wallet = None def create_quote_text(self, btc_balance, mode='normal'): ''' ''' - if not self.exchanger: - from electrum_gui.kivy.plugins.exchange_rate import Exchanger - self.exchanger = Exchanger(self) - self.exchanger.start() quote_currency = self.exchanger.currency quote_balance = self.exchanger.exchange(btc_balance, quote_currency) @@ -348,19 +413,60 @@ class ElectrumWindow(App): quote_currency) if quote_balance is None: - quote_text = "" + quote_text = u"..." else: - quote_text = " (%.2f %s)" % (quote_balance, quote_currency) + quote_text = u"%s%.2f" % (quote_currency, + quote_balance) return quote_text def set_currencies(self, quote_currencies): - self._trigger_update_status() - print quote_currencies self.currencies = sorted(quote_currencies.keys()) + self._trigger_update_status() + + def get_history_rate(self, item, btc_balance, mintime): + '''Historical rates: currently only using coindesk by default. + ''' + maxtime = datetime.datetime.now().strftime('%Y-%m-%d') + rate = self.exchanger.get_history_rate(item, btc_balance, mintime, + maxtime) + + return self.set_history_rate(item, rate) + + def set_history_rate(self, item, rate): + ''' + ''' + #TODO: fix me allow other currencies to be used for history rates + quote_currency = self.exchanger.symbols.get('USD', 'USD') + + if rate is None: + quote_text = "..." + else: + quote_text = "{0}{1:.3}".format(quote_currency, rate) + + item = item() + if item: + item.quote_text = quote_text + return quote_text def update_console(self, *dt): - if self.console: - self.console.showMessage(self.network.banner) + console = self.console + if console: + console = self.console + console.history = self.config.get("console-history",[]) + console.history_index = len(console.history) + + console.updateNamespace({'wallet' : self.wallet, 'network' : self.network, 'gui':self}) + console.updateNamespace({'util' : util, 'bitcoin':bitcoin}) + + c = commands.Commands(self.wallet, self.network, lambda: self.console.set_json(True)) + methods = {} + def mkfunc(f, method): + return lambda *args: apply( f, (method, args, self.password_dialog )) + for m in dir(c): + if m[0]=='_' or m in ['network','wallet']: continue + methods[m] = mkfunc(c._run, m) + + console.updateNamespace(methods) def load_wallet(self, wallet): self.wallet = wallet @@ -375,25 +481,28 @@ class ElectrumWindow(App): self.update_wallet() # Once GUI has been initialized check if we want to announce something # since the callback has been called before the GUI was initialized + self.update_history_tab() self.notify_transactions() self.update_account_selector() - #TODO - #self.new_account.setEnabled(self.wallet.seed_version>4) - #self.update_lock_icon() - #self.update_buttons_on_seed() #run_hook('load_wallet', wallet) def update_status(self, *dt): if not self.wallet: return + + global Decimal + if not Decimal: + from decimal import Decimal + + unconfirmed = '' + quote_text = '' + if self.network is None or not self.network.is_running(): text = _("Offline") #icon = QIcon(":icons/status_disconnected.png") elif self.network.is_connected(): - unconfirmed = '' - quote_text = '.' if not self.wallet.up_to_date: text = _("Synchronizing...") #icon = QIcon(":icons/status_waiting.png") @@ -406,7 +515,8 @@ class ElectrumWindow(App): if u: unconfirmed = " [%s unconfirmed]"\ %( self.format_amount(u, True).strip()) - quote_text = self.create_quote_text(Decimal(c+u)/100000000) or '.' + quote_text = self.create_quote_text(Decimal(c+u)/100000000, + mode='symbol') or '' #r = {} #run_hook('set_quote_text', c+u, r) @@ -420,29 +530,42 @@ class ElectrumWindow(App): text = _("Not connected") #icon = QIcon(":icons/status_disconnected.png") - #TODO - #status_card = self.root.main_screen.ids.tabs.ids.\ - # screen_dashboard.ids.status_card + try: + status_card = self.root.main_screen.ids.tabs.ids.\ + screen_dashboard.ids.status_card + except AttributeError: + return self.status = text.strip() - #status_card.quote_text = quote_text.strip() - #status_card.uncomfirmed = unconfirmed.strip() - ##app.base_unit = self.base_unit().strip() + status_card.quote_text = quote_text.strip() + status_card.uncomfirmed = unconfirmed.strip() def format_amount(self, x, is_diff=False, whitespaces=False): ''' ''' - return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces) - - def update_wallet(self): + global format_satoshis + if not format_satoshis: + from electrum.wallet import format_satoshis + return format_satoshis(x, is_diff, self.num_zeros, + self.decimal_point, whitespaces) + + def read_amount(self, x): + if x in['.', '']: + return None + p = pow(10, self.decimal_point) + return int( p * Decimal(x) ) + + def update_wallet(self, *dt): ''' ''' - self.update_status() - if (self.wallet.up_to_date or - not self.network or not self.network.is_connected()): - #TODO - #self.update_history_tab() - #self.update_receive_tab() - #self.update_contacts_tab() + if not self.exchanger: + from electrum_gui.kivy.plugins.exchange_rate import Exchanger + self.exchanger = Exchanger(self) + self.exchanger.start() + return + self._trigger_update_status() + if (self.wallet.up_to_date or not self.network or not self.network.is_connected()): + self.update_history_tab() + self.update_contacts_tab() self.update_completions() def update_account_selector(self): @@ -458,54 +581,54 @@ class ElectrumWindow(App): else: self.account_selector.hide() - def update_history_tab(self, see_all=False): - def parse_histories(items): - results = [] - for item in items: - tx_hash, conf, is_mine, value, fee, balance, timestamp = item - if conf > 0: - try: - time_str = datetime.datetime.fromtimestamp( - timestamp).isoformat(' ')[:-3] - except: - time_str = _("unknown") - - if conf == -1: - time_str = _('unverified') - icon = "atlas://gui/kivy/theming/light/close" - elif conf == 0: - time_str = _('pending') - icon = "atlas://gui/kivy/theming/light/unconfirmed" - elif conf < 6: - time_str = '' # add new to fix error when conf < 0 - conf = max(1, conf) - icon = "atlas://gui/kivy/theming/light/clock{}".format(conf) - else: - icon = "atlas://gui/kivy/theming/light/confirmed" + def parse_histories(self, items): + for item in items: + tx_hash, conf, is_mine, value, fee, balance, timestamp = item + time_str = _("unknown") + if conf > 0: + try: + time_str = datetime.datetime.fromtimestamp( + timestamp).isoformat(' ')[:-3] + except Exception: + time_str = _("error") + + if conf == -1: + time_str = _('unverified') + icon = "atlas://gui/kivy/theming/light/close" + elif conf == 0: + time_str = _('pending') + icon = "atlas://gui/kivy/theming/light/unconfirmed" + elif conf < 6: + time_str = '' # add new to fix error when conf < 0 + conf = max(1, conf) + icon = "atlas://gui/kivy/theming/light/clock{}".format(conf) + else: + icon = "atlas://gui/kivy/theming/light/confirmed" - if value is not None: - v_str = self.format_amount(value, True, whitespaces=True) - else: - v_str = '--' + if value is not None: + v_str = self.format_amount(value, True, whitespaces=True) + else: + v_str = '--' - balance_str = self.format_amount(balance, whitespaces=True) + balance_str = self.format_amount(balance, whitespaces=True) - if tx_hash: - label, is_default_label = self.wallet.get_label(tx_hash) - else: - label = _('Pruned transaction outputs') - is_default_label = False + if tx_hash: + label, is_default_label = self.wallet.get_label(tx_hash) + else: + label = _('Pruned transaction outputs') + is_default_label = False - results.append(( - conf, icon, time_str, label, v_str, balance_str, tx_hash)) + yield (conf, icon, time_str, label, v_str, balance_str, tx_hash) - return results + def update_history_tab(self, see_all=False): - history_card = self.root.main_screen.ids.tabs.ids.\ + try: + history_card = self.root.main_screen.ids.tabs.ids.\ screen_dashboard.ids.recent_activity_card - histories = parse_histories(reversed( + except AttributeError: + return + histories = self.parse_histories(reversed( self.wallet.get_tx_history(self.current_account))) - #history_view.content_adapter.data = histories # repopulate History Card last_widget = history_card.ids.content.children[-1] @@ -513,26 +636,34 @@ class ElectrumWindow(App): history_add = history_card.ids.content.add_widget history_add(last_widget) RecentActivityItem = Factory.RecentActivityItem - - history_card.ids.btn_see_all.opacity = (0 if see_all or - len(histories) < 8 else 1) - if not see_all: - histories = histories[:8] - - create_quote_text = self.create_quote_text + global Decimal, ref + if not ref: + from weakref import ref + if not Decimal: + from decimal import Decimal + + get_history_rate = self.get_history_rate + count = 0 for items in histories: + count += 1 conf, icon, date_time, address, amount, balance, tx = items ri = RecentActivityItem() ri.icon = icon ri.date = date_time + mintimestr = date_time.split()[0] ri.address = address - ri.amount = amount - ri.quote_text = create_quote_text( - Decimal(amount)/100000000, mode='symbol') + ri.amount = amount.strip() + ri.quote_text = get_history_rate(ref(ri), + Decimal(amount), + mintimestr) ri.balance = balance ri.confirmations = conf ri.tx_hash = tx history_add(ri) + if count == 8 and not see_all: + break + + history_card.ids.btn_see_all.opacity = (0 if count < 8 else 1) def update_receive_tab(self): #TODO move to address managment @@ -590,24 +721,45 @@ class ElectrumWindow(App): receive_list.content_adapter.data = data def update_contacts_tab(self): - data = [] + contact_list = self.root.main_screen.ids.tabs.ids.\ + screen_contacts.ids.contact_container + #contact_list.clear_widgets() + + child = -1 + children = contact_list.children for address in self.wallet.addressbook: label = self.wallet.labels.get(address, '') - item = (address, label, "%d" % self.wallet.get_num_tx(address)) - data.append(item) - # item.setFont(0, QFont(MONOSPACE_FONT)) - # # 32 = label can be edited (bool) - # item.setData(0,32, True) - # # 33 = payto string - # item.setData(0,33, address) + child += 1 + try: + if children[child].label == label: + continue + except IndexError: + pass + tx = self.wallet.get_num_tx(address) + ci = Factory.ContactItem() + ci.address = address + ci.label = label + ci.tx_amount = tx + contact_list.add_widget(ci) + + #self.run_hook('update_contacts_tab') + + def set_pay_from(self, l): + #TODO + return + self.pay_from = l + self.from_list.clear() + self.from_label.setHidden(len(self.pay_from) == 0) + self.from_list.setHidden(len(self.pay_from) == 0) + for addr in self.pay_from: + c, u = self.wallet.get_addr_balance(addr) + balance = self.format_amount(c + u) + self.from_list.addTopLevelItem(QTreeWidgetItem( [addr, balance] )) - self.run_hook('update_contacts_tab') - contact_list = app.root.main_screen.ids.tabs.ids.\ - screen_contacts.ids.contacts_list - contact_list.content_adapter.data = data def update_completions(self): + #TODO: check and remove if not used l = [] for addr, label in self.wallet.labels.items(): if addr in self.wallet.addressbook: @@ -616,19 +768,134 @@ class ElectrumWindow(App): #self.run_hook('update_completions', l) self.completions = l + def protected(func): + return lambda s, *args, **kwargs: s.do_protect(func, args, **kwargs) + + def do_protect(self, func, **kwargs): + print kwargs + instance = kwargs.get('instance', None) + password = kwargs.get('password', None) + message = kwargs.get('message', '') + + def run_func(instance=None, password=None): + args = (self, instance, password) + apply(func, args) + + if self.wallet.use_encryption: + return self.password_required_dialog(post_ok=run_func, message=message) + + return run_func() + + def do_send(self): + app = App.get_running_app() + screen_send = app.root.main_screen.ids.tabs.ids.screen_send + scrn = screen_send.content.ids + label = unicode(scrn.message_e.text) + # TODO + #if self.gui_object.payment_request: + # outputs = self.gui_object.payment_request.outputs + # amount = self.gui_object.payment_request.get_amount() + #else: + + r = unicode(scrn.payto_e.text).strip() + + # label or alias, with address in brackets + global re + if not re: + import re + m = re.match('(.*?)\s*\<([1-9A-HJ-NP-Za-km-z]{26,})\>', r) + to_address = m.group(2) if m else r + + global is_valid + if not is_valid: + from electrum.bitcoin import is_valid + + if not is_valid(to_address): + app.show_error(_('Invalid Bitcoin Address') + + ':\n' + to_address) + return + + try: + amount = self.read_amount(unicode(scrn.amount_e.text)) + except Exception: + app.show_error(_('Invalid Amount')) + return + try: + fee = self.read_amount(unicode(scrn.fee_e.amt)) + except Exception as err: + print err + app.show_error(_('Invalid Fee')) + return + + from pudb import set_trace; set_trace() + message = 'sending {} {} to {}'.format(\ + app.base_unit, scrn.amount_e.text, r) + + confirm_fee = self.config.get('confirm_fee', 100000) + if fee >= confirm_fee: + if not self.question(_("The fee for this transaction seems unusually high.\nAre you really sure you want to pay %(fee)s in fees?")%{ 'fee' : self.format_amount(fee) + ' '+ self.base_unit()}): + return + + self.send_tx(to_address, amount, fee, label) + + @protected + def send_tx(self, outputs, fee, label, password): + + # first, create an unsigned tx + domain = self.get_payment_sources() + try: + tx = self.wallet.make_unsigned_transaction(outputs, fee, None, domain) + tx.error = None + except Exception as e: + traceback.print_exc(file=sys.stdout) + self.show_info(str(e)) + return + + # call hook to see if plugin needs gui interaction + #run_hook('send_tx', tx) + + # sign the tx + def sign_thread(): + time.sleep(0.1) + keypairs = {} + self.wallet.add_keypairs_from_wallet(tx, keypairs, password) + self.wallet.sign_transaction(tx, keypairs, password) + return tx, fee, label + + def sign_done(tx, fee, label): + if tx.error: + self.show_info(tx.error) + return + if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE: + self.show_error(_("This transaction requires a higher fee, or " + "it will not be propagated by the network.")) + return + if label: + self.wallet.set_label(tx.hash(), label) + + if not self.gui_object.payment_request: + if not tx.is_complete() or self.config.get('show_before_broadcast'): + self.show_transaction(tx) + return + + self.broadcast_transaction(tx) + + WaitingDialog(self, 'Signing..').start(sign_thread, sign_done) + def notify_transactions(self, *dt): ''' ''' if not self.network or not self.network.is_connected(): return - iface = self.network.interface - if len(iface.pending_transactions_for_notifications) > 0: + iface = self.network + ptfn = iface.pending_transactions_for_notifications + if len(ptfn) > 0: # Combine the transactions if there are more then three - tx_amount = len(iface.pending_transactions_for_notifications) + tx_amount = len(ptfn) if(tx_amount >= 3): total_amount = 0 - for tx in iface.pending_transactions_for_notifications: + for tx in ptfn: is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) if(v > 0): total_amount += v @@ -645,11 +912,18 @@ class ElectrumWindow(App): iface.pending_transactions_for_notifications.remove(tx) is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx) if(v > 0): - from pudb import set_trace; set_trace() self.notify( - _("New transaction received. {amount}s {unit}s"). + _("{} new transaction received. {amount}s {unit}s"). format( amount=self.format_amount(v), - unit=self.base_unit())) + unit=self.base_unit)) + + def copy(self, text): + ''' Copy provided text to clipboard + ''' + if not self._clipboard: + from kivy.core.clipboard import Clipboard + self._clipboard = Clipboard + self._clipboard.put(text, 'text/plain') def notify(self, message): try: @@ -668,31 +942,42 @@ class ElectrumWindow(App): ''' ''' # pause nfc - # pause qrscanner(Camera) if active + if self.qrscanner: + self.qrscanner.stop() + if self.nfcscanner: + self.nfcscanner.nfc_disable() return True def on_resume(self): ''' ''' - # resume nfc - # resume camera if active - pass + if self.qrscanner and qrscanner.get_parent_window(): + self.qrscanner.start() + if self.nfcscanner: + self.nfcscanner.nfc_enable() def on_size(self, instance, value): width, height = value self._orientation = 'landscape' if width > height else 'portrait' + + global inch + if not inch: + from kivy.metrics import inch + 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', manager=None): + def load_screen(self, index=0, direction='left', manager=None, switch=True): ''' Load the appropriate screen as mentioned in the parameters. ''' manager = manager or self.root.manager - screen = Builder.load_file('gui/kivy/ui_screens/'\ + screen = Builder.load_file('gui/kivy/uix/ui_screens/'\ + self.screens[index] + '.kv') screen.name = self.screens[index] - manager.switch_to(screen, direction=direction) + if switch: + manager.switch_to(screen, direction=direction) + return screen def load_next_screen(self): ''' @@ -705,7 +990,7 @@ class ElectrumWindow(App): self.load_screen() def load_previous_screen(self): - ''' + ''' Load the previous screen from disk. ''' manager = root.manager try: @@ -715,51 +1000,239 @@ class ElectrumWindow(App): except IndexError: pass - def show_error(self, error, - width='200dp', - pos=None, - arrow_pos=None, - exit=False, - icon='atlas://gui/kivy/theming/light/error', - duration=0, - modal=False): + def save_new_contact(self, address, label): + address = unicode(address) + label = unicode(label) + global is_valid + if not is_valid: + from electrum.bitcoin import is_valid + + + if is_valid(address): + if label: + self.set_label(address, text=label) + self.wallet.add_contact(address) + self.update_contacts_tab() + self.update_history_tab() + self.update_completions() + else: + self.show_error(_('Invalid Address')) + + def send_payment(self, address, amount=0, label='', message=''): + tabs = self.tabs + screen_send = tabs.ids.screen_send + + if label and self.wallet.labels.get(address) != label: + #if self.question('Give label "%s" to address %s ?'%(label,address)): + if address not in self.wallet.addressbook and not self.wallet. is_mine(address): + self.wallet.addressbook.append(address) + self.wallet.set_label(address, label) + + # switch_to the send screen + tabs.ids.panel.switch_to(tabs.ids.tab_send) + + label = self.wallet.labels.get(address) + m_addr = label + ' <'+ address +'>' if label else address + + # populate + def set_address(*l): + content = screen_send.content.ids + content.payto_e.text = m_addr + content.message_e.text = message + if amount: + content.amount_e.text = amount + + # wait for screen to load + Clock.schedule_once(set_address, .5) + + def set_send(self, address, amount, label, message): + self.send_payment(address, amount=amount, label=label, message=message) + + def prepare_for_payment_request(self): + tabs = self.tabs + screen_send = tabs.ids.screen_send + + # switch_to the send screen + tabs.ids.panel.switch_to(tabs.ids.tab_send) + + content = screen_send.content.ids + if content: + self.set_frozen(content, False) + screen_send.screen_label.text = _("please wait...") + return True + + def payment_request_ok(self): + tabs = self.tabs + screen_send = tabs.ids.screen_send + + # switch_to the send screen + tabs.ids.panel.switch_to(tabs.ids.tab_send) + + content = screen_send.content + self.set_frozen(content, True) + + content.ids.payto_e.text = self.gui_object.payment_request.domain + content.ids.amount_e.text = self.format_amount(self.gui_object.payment_request.get_amount()) + content.ids.message_e.text = self.gui_object.payment_request.memo + + # wait for screen to load + Clock.schedule_once(set_address, .5) + + def do_clear(self): + tabs = self.tabs + screen_send = tabs.ids.screen_send + content = screen_send.ids.content + cts = content.ids + cts.payto_e.text = cts.message_e.text = cts.amount_e.text = \ + cts.fee_e.text = '' + + self.set_frozen(content, False) + + self.set_pay_from([]) + self.update_status() + + def set_frozen(self, entry, frozen): + if frozen: + entry.disabled = True + Factory.Animation(opacity=0).start(content) + else: + entry.disabled = False + Factory.Animation(opacity=1).start(content) + + def set_addrs_frozen(self,addrs,freeze): + for addr in addrs: + if not addr: continue + if addr in self.wallet.frozen_addresses and not freeze: + self.wallet.unfreeze(addr) + elif addr not in self.wallet.frozen_addresses and freeze: + self.wallet.freeze(addr) + self.update_receive_tab() + + def payment_request_error(self): + tabs = self.tabs + screen_send = tabs.ids.screen_send + + # switch_to the send screen + tabs.ids.panel.switch_to(tabs.ids.tab_send) + + self.do_clear() + self.show_info(self.gui_object.payment_request.error) + + def encode_uri(self, 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(self, 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 + + def delete_imported_key(self, addr): + self.wallet.delete_imported_key(addr) + self.update_receive_tab() + self.update_history_tab() + + def delete_pending_account(self, k): + self.wallet.delete_pending_account(k) + self.update_receive_tab() + + def get_sendable_balance(self): + return sum(sum(self.wallet.get_addr_balance(a)) + for a in self.get_payment_sources()) + + + def get_payment_sources(self): + if self.pay_from: + return self.pay_from + else: + return self.wallet.get_account_addresses(self.current_account) + + + def send_from_addresses(self, addrs): + self.set_pay_from( addrs ) + tabs = self.tabs + screen_send = tabs.ids.screen_send + self.tabs.setCurrentIndex(1) + + + def payto(self, addr): + if not addr: + return + label = self.wallet.labels.get(addr) + m_addr = label + ' <' + addr + '>' if label else addr + self.tabs.setCurrentIndex(1) + self.payto_e.setText(m_addr) + self.amount_e.setFocus() + + + def delete_contact(self, x): + if self.question(_("Do you want to remove") + + " %s "%x + + _("from your list of contacts?")): + self.wallet.delete_contact(x) + self.wallet.set_label(x, None) + self.update_history_tab() + self.update_contacts_tab() + self.update_completions() + + def show_error(self, error, width='200dp', pos=None, arrow_pos=None, + exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, + modal=False): ''' Show a error Message Bubble. ''' - self.show_info_bubble( - text=error, - icon=icon, - width=width, - pos=pos or Window.center, - arrow_pos=arrow_pos, - exit=exit, - duration=duration, - modal=modal) - - def show_info(self, error, - width='200dp', - pos=None, - arrow_pos=None, - exit=False, - duration=0, - modal=False): + self.show_info_bubble( text=error, icon=icon, width=width, + pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit, + duration=duration, modal=modal) + + def show_info(self, error, width='200dp', pos=None, arrow_pos=None, + exit=False, duration=0, modal=False): ''' Show a Info Message Bubble. ''' self.show_error(error, icon='atlas://gui/kivy/theming/light/error', - duration=duration, - modal=modal, - exit=exit, - pos=pos, - 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, - exit=False): + duration=duration, modal=modal, exit=exit, pos=pos, + arrow_pos=arrow_pos) + + def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, + arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show a Information Bubble .. parameters:: @@ -769,13 +1242,13 @@ class ElectrumWindow(App): 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() + info_bubble = self.info_bubble = Factory.InfoBubble() + win = Window if info_bubble.parent: - Window.remove_widget(info_bubble + win.remove_widget(info_bubble if not info_bubble.modal else info_bubble._modal_view) @@ -794,7 +1267,6 @@ class ElectrumWindow(App): 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 @@ -805,4 +1277,6 @@ class ElectrumWindow(App): 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, exit=exit) + if not pos: + pos = (win.center[0], win.center[1] - (info_bubble.height/2)) + info_bubble.show(pos, duration, width, modal=modal, exit=exit)+ \ No newline at end of file diff --git a/gui/kivy/plugins/exchange_rate.py b/gui/kivy/plugins/exchange_rate.py @@ -6,13 +6,18 @@ This module is responsible for getting the conversion rates from different bitcoin exchanges. ''' +import decimal +import json + from kivy.network.urlrequest import UrlRequest from kivy.event import EventDispatcher from kivy.properties import (OptionProperty, StringProperty, AliasProperty, ListProperty) from kivy.clock import Clock -import decimal -import json +from kivy.cache import Cache + +# Register local cache +Cache.register('history_rate', timeout=220) EXCHANGES = ["BitcoinAverage", "BitcoinVenezuela", @@ -25,27 +30,32 @@ EXCHANGES = ["BitcoinAverage", "LocalBitcoins", "Winkdex"] +HISTORY_EXCHNAGES = ['Coindesk', + 'Winkdex', + 'BitcoinVenezuela'] + class Exchanger(EventDispatcher): ''' Provide exchanges rate between crypto and different national currencies. See Module Documentation for details. ''' - symbols = {'ALL': 'Lek', 'AED': 'د.إ', 'AFN':'؋', 'ARS': '$', 'AMD': '֏', - 'AWG': 'ƒ', 'ANG': 'ƒ', 'AOA': 'Kz', 'BDT': '৳', 'BHD': 'BD', - 'BIF': 'FBu', 'BTC': 'BTC', 'BTN': 'Nu', 'CDF': 'FC', 'CHF': 'CHF', - 'CLF': 'UF', 'CLP':'$', 'CVE': '$', 'DJF':'Fdj', 'DZD': 'دج', - 'AUD': '$', 'AZN': 'ман', 'BSD': '$', 'BBD': '$', 'BYR': 'p', 'CRC': '₡', - 'BZD': 'BZ$', 'BMD': '$', 'BOB': '$b', 'BAM': 'KM', 'BWP': 'P', - 'BGN': 'лв', 'BRL': 'R$', 'BND': '$', 'KHR': '៛', 'CAD': '$', - 'ERN': 'Nfk', 'ETB': 'Br', 'KYD': '$', 'USD': '$', 'CLP': '$', - 'HRK': 'kn', 'CUP':'₱', 'CZK': 'Kč', 'DKK': 'kr', 'DOP': 'RD$', - 'XCD': '$', 'EGP': '£', 'SVC': '$' , 'EEK': 'kr', 'EUR': '€', - 'FKP': '£', 'FJD': '$', 'GHC': '¢', 'GIP': '£', 'GTQ': 'Q', 'GBP': '£', - 'GYD': '$', 'HNL': 'L', 'HKD': '$', 'HUF': 'Ft', 'ISK': 'kr', - 'INR': '₹', 'IDR': 'Rp', 'IRR': '﷼', 'IMP': '£', 'ILS': '₪', 'COP': '$', - 'JMD': 'J$', 'JPY': '¥', 'JEP': '£', 'KZT': 'лв', 'KPW': '₩', - 'KRW': '₩', 'KGS': 'лв', 'LAK': '₭', 'LVL': 'Ls', 'CNY': '¥'} + symbols = {'ALL': u'Lek', 'AED': u'د.إ', 'AFN':u'؋', 'ARS': u'$', + 'AMD': u'֏', 'AWG': u'ƒ', 'ANG': u'ƒ', 'AOA': u'Kz', 'BDT': u'৳', + 'BHD': u'BD', 'BIF': u'FBu', 'BTC': u'BTC', 'BTN': u'Nu', 'CDF': u'FC', + 'CHF': u'CHF', 'CLF': u'UF', 'CLP':u'$', 'CVE': u'$', 'DJF':u'Fdj', + 'DZD': u'دج', 'AUD': u'$', 'AZN': u'ман', 'BSD': u'$', 'BBD': u'$', + 'BYR': u'p', 'CRC': u'₡', 'BZD': u'BZ$', 'BMD': u'$', 'BOB': u'$b', + 'BAM': u'KM', 'BWP': u'P', 'BGN': 'uлв', 'BRL': u'R$', 'BND': u'$', + 'KHR': u'៛', 'CAD': u'$', 'ERN': u'Nfk', 'ETB': u'Br', 'KYD': u'$', + 'USD': u'$', 'CLP': u'$', 'HRK': u'kn', 'CUP': u'₱', 'CZK': u'Kč', + 'DKK': u'kr', 'DOP': u'RD$', 'XCD': u'$', 'EGP': u'£', 'SVC': u'$' , + 'EEK': u'kr', 'EUR': u'€', u'FKP': u'£', 'FJD': u'$', 'GHC': u'¢', + 'GIP': u'£', 'GTQ': u'Q', 'GBP': u'£', 'GYD': u'$', 'HNL': u'L', + 'HKD': u'$', 'HUF': u'Ft', 'ISK': u'kr', 'INR': u'₹', 'IDR': u'Rp', + 'IRR': u'﷼', 'IMP': '£', 'ILS': '₪', 'COP': '$', 'JMD': u'J$', + 'JPY': u'¥', 'JEP': u'£', 'KZT': u'лв', 'KPW': u'₩', 'KRW': u'₩', + 'KGS': u'лв', 'LAK': u'₭', 'LVL': u'Ls', 'CNY': u'¥'} _use_exchange = OptionProperty('Blockchain', options=EXCHANGES) '''This is the exchange to be used for getting the currency exchange rates @@ -56,23 +66,16 @@ class Exchanger(EventDispatcher): ''' def _set_currency(self, value): - exchanger = self.exchanger + value = str(value) if self.use_exchange == 'CoinDesk': self._update_cd_currency(self.currency) return - try: - self._currency = value - self.electrum_cinfig.set_key('currency', value, True) - except AttributeError: - self._currency = 'EUR' + self._currency = value + self.parent.electrum_config.set_key('currency', value, True) def _get_currency(self): - try: - self._currency = self.electrum_config.get('currency', 'EUR') - except AttributeError: - pass - finally: - return self._currency + self._currency = self.parent.electrum_config.get('currency', 'EUR') + return self._currency currency = AliasProperty(_get_currency, _set_currency, bind=('_currency',)) @@ -104,6 +107,7 @@ class Exchanger(EventDispatcher): self.parent = parent self.quote_currencies = None self.exchanges = EXCHANGES + self.history_exchanges = HISTORY_EXCHNAGES def exchange(self, btc_amount, quote_currency): if self.quote_currencies is None: @@ -115,10 +119,40 @@ class Exchanger(EventDispatcher): return btc_amount * decimal.Decimal(quote_currencies[quote_currency]) + def get_history_rate(self, item, btc_amt, mintime, maxtime): + def on_success(request, response): + response = json.loads(response) + + try: + hrate = response['bpi'][mintime] + hrate = abs(btc_amt) * decimal.Decimal(hrate) + Cache.append('history_rate', uid, hrate) + except KeyError: + hrate = 'not found' + + self.parent.set_history_rate(item, hrate) + + # Check local cache before getting data from remote + exchange = 'coindesk' + uid = '{}:{}'.format(exchange, mintime) + hrate = Cache.get('history_rate', uid) + + if hrate: + return hrate + + req = UrlRequest(url='https://api.coindesk.com/v1/bpi/historical' + '/close.json?start={}&end={}' + .format(mintime, maxtime) + ,on_success=on_success, timeout=15) + return None + def update_rate(self, dt): ''' This is called from :method:`start` every X seconds; to update the rates for currencies for the currently selected exchange. ''' + if not self.parent.network or not self.parent.network.is_connected(): + return + update_rates = { "BitcoinAverage": self.update_ba, "BitcoinVenezuela": self.update_bv, @@ -268,7 +302,7 @@ class Exchanger(EventDispatcher): for r in response: quote_currencies[r] = _lookup_rate(response, r) self.quote_currencies = quote_currencies - except KeyError: + except KeyError, TypeError: pass self.parent.set_currencies(quote_currencies) @@ -329,9 +363,8 @@ class Exchanger(EventDispatcher): timeout=5) def start(self): - # check rates every few seconds self.update_rate(0) - # check every few seconds + # check every 20 seconds Clock.unschedule(self.update_rate) Clock.schedule_interval(self.update_rate, 20) diff --git a/gui/kivy/qr_scanner/__init__.py b/gui/kivy/qr_scanner/__init__.py @@ -7,69 +7,23 @@ from collections import namedtuple from kivy.uix.anchorlayout import AnchorLayout from kivy.core import core_select_lib +from kivy.metrics import dp 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]) + camera_size = ListProperty([320, 240] if dp(1) < 2 else [640, 480]) symbols = ListProperty([]) # XXX can't work now, due to overlay. show_bounds = BooleanProperty(False) + running = BooleanProperty(False) + Qrcode = namedtuple('Qrcode', ['type', 'data', 'bounds', 'quality', 'count']) diff --git a/gui/kivy/qr_scanner/scanner_android.py b/gui/kivy/qr_scanner/scanner_android.py @@ -88,7 +88,7 @@ class SurfaceHolderCallback(PythonJavaClass): 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) @@ -96,7 +96,7 @@ class SurfaceHolderCallback(PythonJavaClass): @java_method('(Landroid/view/SurfaceHolder;)V') def surfaceCreated(self, surface): pass - + @java_method('(Landroid/view/SurfaceHolder;)V') def surfaceDestroyed(self, surface): pass @@ -170,6 +170,7 @@ class AndroidCamera(Widget): @run_on_ui_thread def stop(self): + self.running = False if self._android_camera is None: return self._android_camera.setPreviewCallback(None) @@ -179,6 +180,7 @@ class AndroidCamera(Widget): @run_on_ui_thread def start(self): + self.running = True if self._android_camera is not None: return @@ -196,6 +198,9 @@ class AndroidCamera(Widget): # attach the android surfaceview to our android widget holder self._holder.view = self._android_surface + # set orientation + self._android_camera.setDisplayOrientation(90) + 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. diff --git a/gui/kivy/qrcodewidget.py b/gui/kivy/qrcodewidget.py @@ -1,179 +0,0 @@ -''' 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 @@ -1,105 +0,0 @@ -from kivy.app import App -from kivy.uix.screenmanager import Screen -from kivy.properties import ObjectProperty -from kivy.clock import Clock - - -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.manager.current_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 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 ScreenSend(CScreen): - pass - -class ScreenReceive(CScreen): - pass - -class ScreenContacts(CScreen): - - def add_new_contact(self): - NewContactDialog().open() diff --git a/gui/kivy/statusbar.py b/gui/kivy/statusbar.py @@ -1,7 +0,0 @@ -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 @@ -1,14 +0,0 @@ -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.atlas b/gui/kivy/theming/light.atlas @@ -1 +1 @@ -{"light-0.png": {"closebutton": [962, 737, 60, 43], "card_top": [810, 328, 32, 16], "tab_btn_disabled": [674, 312, 32, 32], "tab_btn_pressed": [742, 312, 32, 32], "globe": [884, 219, 72, 72], "btn_send_nfc": [996, 514, 18, 15], "shadow_right": [958, 220, 32, 5], "logo_atom_dull": [528, 346, 64, 64], "tab": [792, 346, 64, 64], "logo": [457, 163, 128, 128], "qrcode": [163, 146, 145, 145], "close": [906, 441, 88, 88], "btn_create_act_disabled": [953, 169, 32, 32], "create_act_text": [996, 490, 22, 10], "card_bottom": [962, 719, 32, 16], "confirmed": [896, 716, 64, 64], "carousel_deselected": [958, 227, 64, 64], "network": [499, 296, 48, 48], "blue_bg_round_rb": [906, 419, 31, 20], "action_bar": [602, 308, 36, 36], "pen": [660, 346, 64, 64], "arrow_back": [396, 294, 50, 50], "clock3": [698, 716, 64, 64], "contact": [448, 295, 49, 49], "star_big_inactive": [587, 163, 128, 128], "lightblue_bg_round_lb": [939, 419, 31, 20], "manualentry": [310, 157, 145, 134], "stepper_restore_password": [396, 412, 392, 117], "tab_disabled": [717, 169, 96, 32], "mail_icon": [924, 356, 65, 54], "tab_strip": [815, 169, 96, 32], "tab_btn": [708, 312, 32, 32], "btn_create_account": [943, 792, 64, 32], "btn_send_address": [996, 720, 18, 15], "add_contact": [549, 301, 51, 43], "gear": [2, 132, 159, 159], "wallets": [776, 312, 32, 32], "stepper_left": [2, 412, 392, 117], "nfc_stage_one": [2, 531, 489, 122], "nfc_clock": [698, 782, 243, 240], "btn_nfc": [1009, 812, 13, 12], "textinput_active": [790, 415, 114, 114], "clock2": [943, 826, 64, 64], "nfc_phone": [2, 655, 372, 367], "clock4": [764, 716, 64, 64], "paste_icon": [807, 214, 75, 77], "shadow": [726, 346, 64, 64], "carousel_selected": [943, 958, 64, 64], "card": [987, 169, 32, 32], "unconfirmed": [858, 346, 64, 64], "info": [462, 346, 64, 64], "electrum_icon640": [376, 702, 320, 320], "action_group_dark": [991, 362, 33, 48], "nfc": [594, 346, 64, 64], "clock1": [943, 892, 64, 64], "create_act_text_active": [996, 502, 22, 10], "icon_border": [396, 346, 64, 64], "stepper_full": [493, 536, 392, 117], "card_btn": [913, 169, 38, 32], "wallet": [376, 656, 49, 44], "important": [717, 203, 88, 88], "dialog": [1005, 419, 18, 20], "error": [887, 539, 128, 114], "stepper_restore_seed": [2, 293, 392, 117], "white_bg_round_top": [972, 419, 31, 20], "settings": [640, 312, 32, 32], "clock5": [830, 716, 64, 64]}}- \ No newline at end of file +{"light-0.png": {"closebutton": [641, 591, 60, 43], "card_top": [901, 792, 32, 16], "tab_btn_disabled": [833, 483, 32, 32], "tab_btn_pressed": [901, 483, 32, 32], "bit_logo": [589, 728, 44, 51], "globe": [686, 267, 72, 72], "btn_send_nfc": [955, 793, 18, 15], "shadow_right": [975, 803, 32, 5], "logo_atom_dull": [773, 517, 64, 64], "action_group_light": [431, 344, 33, 48], "tab": [390, 715, 64, 64], "logo": [296, 211, 128, 128], "qrcode": [2, 194, 145, 145], "close": [834, 810, 88, 88], "btn_create_act_disabled": [985, 911, 32, 32], "white_bg_round_top": [834, 788, 31, 20], "card_bottom": [867, 792, 32, 16], "confirmed": [839, 636, 64, 64], "overflow_btn_dn": [989, 520, 16, 10], "carousel_deselected": [760, 275, 64, 64], "network": [692, 467, 48, 48], "blue_bg_round_rb": [935, 495, 31, 20], "dropdown_background": [765, 599, 29, 35], "action_bar": [795, 479, 36, 36], "pen": [905, 517, 64, 64], "overflow_background": [796, 599, 29, 35], "arrow_back": [971, 650, 50, 50], "clock3": [641, 636, 64, 64], "contact": [641, 466, 49, 49], "star_big_inactive": [426, 211, 128, 128], "lightblue_bg_round_lb": [968, 495, 31, 20], "manualentry": [149, 205, 145, 134], "stepper_restore_password": [247, 464, 392, 117], "tab_disabled": [752, 233, 96, 32], "mail_icon": [522, 725, 65, 54], "tab_strip": [850, 233, 96, 32], "tab_btn": [867, 483, 32, 32], "btn_create_account": [948, 233, 64, 32], "btn_send_address": [935, 793, 18, 15], "add_contact": [742, 472, 51, 43], "gear": [2, 33, 105, 159], "wallets": [703, 594, 60, 40], "stepper_left": [247, 583, 392, 117], "nfc_stage_one": [324, 900, 489, 122], "nfc_clock": [2, 460, 243, 240], "btn_nfc": [752, 219, 13, 12], "textinput_active": [718, 784, 114, 114], "clock2": [958, 275, 64, 64], "nfc_phone": [556, 213, 128, 126], "clock4": [707, 636, 64, 64], "paste_icon": [945, 945, 75, 77], "shadow": [324, 715, 64, 64], "carousel_selected": [826, 275, 64, 64], "card": [686, 216, 64, 49], "unconfirmed": [456, 715, 64, 64], "info": [707, 517, 64, 64], "electrum_icon640": [2, 702, 320, 320], "action_button_group": [971, 520, 16, 10], "action_group_dark": [396, 344, 33, 48], "nfc": [839, 517, 64, 64], "contact_avatar": [971, 532, 49, 49], "clock1": [892, 275, 64, 64], "create_act_text_active": [971, 638, 22, 10], "icon_border": [641, 517, 64, 64], "stepper_full": [324, 781, 392, 117], "card_btn": [945, 911, 38, 32], "wallet": [635, 735, 49, 44], "important": [924, 810, 88, 88], "dialog": [1001, 495, 18, 20], "error": [815, 908, 128, 114], "stepper_restore_seed": [2, 341, 392, 117], "contact_overlay": [905, 636, 64, 64], "settings": [396, 394, 54, 64], "create_act_text": [995, 638, 22, 10], "clock5": [773, 636, 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_button_group.png b/gui/kivy/theming/light/action_button_group.png Binary files differ. diff --git a/gui/kivy/theming/light/action_group_light.png b/gui/kivy/theming/light/action_group_light.png Binary files differ. diff --git a/gui/kivy/theming/light/bit_logo.png b/gui/kivy/theming/light/bit_logo.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_top.png b/gui/kivy/theming/light/card_top.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/contact_avatar.png b/gui/kivy/theming/light/contact_avatar.png Binary files differ. diff --git a/gui/kivy/theming/light/contact_overlay.png b/gui/kivy/theming/light/contact_overlay.png Binary files differ. diff --git a/gui/kivy/theming/light/dropdown_background.png b/gui/kivy/theming/light/dropdown_background.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/logo.png b/gui/kivy/theming/light/logo.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/nfc_phone.png b/gui/kivy/theming/light/nfc_phone.png Binary files differ. diff --git a/gui/kivy/theming/light/overflow_background.png b/gui/kivy/theming/light/overflow_background.png Binary files differ. diff --git a/gui/kivy/theming/light/overflow_btn_dn.png b/gui/kivy/theming/light/overflow_btn_dn.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/tab_btn_pressed.png b/gui/kivy/theming/light/tab_btn_pressed.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/wallets.png b/gui/kivy/theming/light/wallets.png Binary files differ. diff --git a/gui/kivy/tools/blacklist.txt b/gui/kivy/tools/blacklist.txt @@ -0,0 +1,99 @@ +# eggs +*.egg-info + +# unit test +unittest/* + +# python config +config/makesetup + +# unused pygame files +pygame/_camera_* +pygame/camera.pyo +pygame/*.html +pygame/*.bmp +pygame/*.svg +pygame/cdrom.so +pygame/pygame_icon.icns +pygame/LGPL +pygame/threads/Py25Queue.pyo +pygame/*.ttf +pygame/mac* +pygame/_numpy* +pygame/sndarray.pyo +pygame/surfarray.pyo +pygame/_arraysurfarray.pyo + +# unused kivy files (platform specific) +kivy/input/providers/wm_* +kivy/input/providers/mactouch* +kivy/input/providers/probesysfs* +kivy/input/providers/mtdev* +kivy/input/providers/hidinput* +kivy/core/camera/camera_videocapture* +kivy/core/spelling/*osx* +kivy/core/video/video_pyglet* + +# unused encodings +lib-dynload/*codec* +encodings/cp*.pyo +encodings/tis* +encodings/shift* +encodings/bz2* +encodings/iso* +encodings/undefined* +encodings/johab* +encodings/p* +encodings/m* +encodings/euc* +encodings/k* +encodings/unicode_internal* +encodings/quo* +encodings/gb* +encodings/big5* +encodings/hp* +encodings/hz* + +# unused python modules +bsddb/* +wsgiref/* +hotshot/* +pydoc_data/* +tty.pyo +anydbm.pyo +nturl2path.pyo +LICENCE.txt +macurl2path.pyo +dummy_threading.pyo +audiodev.pyo +antigravity.pyo +dumbdbm.pyo +sndhdr.pyo +__phello__.foo.pyo +sunaudio.pyo +os2emxpath.pyo +multiprocessing/dummy* + +# unused binaries python modules +lib-dynload/termios.so +lib-dynload/_lsprof.so +lib-dynload/*audioop.so +#lib-dynload/mmap.so +lib-dynload/_hotshot.so +lib-dynload/_csv.so +lib-dynload/future_builtins.so +lib-dynload/_heapq.so +lib-dynload/_json.so +lib-dynload/grp.so +lib-dynload/resource.so +lib-dynload/pyexpat.so + +# odd files +plat-linux3/regen + +#>sqlite3 +# conditionnal include depending if some recipes are included or not. +sqlite3/* +lib-dynload/_sqlite3.so +#<sqlite3 + diff --git a/gui/kivy/tools/buildozer.spec b/gui/kivy/tools/buildozer.spec @@ -0,0 +1,172 @@ +[app] + +# (str) Title of your application +title = Electrum + +# (str) Package name +package.name = electrum + +# (str) Package domain (needed for android/ios packaging) +package.domain = org.sierra3d + +# (str) Source code where the main.py live +source.dir = . + +# (list) Source files to include (let empty to include all the files) +source.include_exts = py,png,jpg,kv,atlas,ttf,*,txt, gif + +# (list) Source files to exclude (let empty to not exclude anything) +source.exclude_exts = spec + +# (list) List of directory to exclude (let empty to not exclude anything) +#source.exclude_dirs = + +# (list) List of exclusions using pattern matching +#source.exclude_patterns = license,images/*/*.jpg + +# (str) Application versioning (method 1) +#version.regex = __version__ = '(.*)' +#version.filename = %(source.dir)s/main.py + +# (str) Application versioning (method 2) +version = 1.9.7 + +# (list) Application requirements +requirements = pil, qrcode, ecdsa, pbkdf2, pyopenssl, plyer==master, kivy==master + +# (str) Presplash of the application +presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png + +# (str) Icon of the application +icon.filename = %(source.dir)s/icons/electrum_android_launcher_icon.png + +# (str) Supported orientation (one of landscape, portrait or all) +orientation = portrait + +# (bool) Indicate if the application should be fullscreen or not +fullscreen = False + + +# +# Android specific +# + +# (list) Permissions +android.permissions = INTERNET, WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE , CAMERA, NFC +# (int) Android API to use +#android.api = 14 + +# (int) Minimum API required (8 = Android 2.2 devices) +#android.minapi = 8 + +# (int) Android SDK version to use +#android.sdk = 21 + +# (str) Android NDK version to use +#android.ndk = 9 + +# (bool) Use --private data storage (True) or --dir public storage (False) +android.private_storage = False + +# (str) Android NDK directory (if empty, it will be automatically downloaded.) +#android.ndk_path = + +# (str) Android SDK directory (if empty, it will be automatically downloaded.) +#android.sdk_path = + +# (str) Android entry point, default is ok for Kivy-based app +#android.entrypoint = org.renpy.android.PythonActivity + +# (list) List of Java .jar files to add to the libs so that pyjnius can access +# their classes. Don't add jars that you do not need, since extra jars can slow +# down the build process. Allows wildcards matching, for example: +# OUYA-ODK/libs/*.jar +#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar +android.add_jars = lib/android/zbar.jar + +# (list) List of Java files to add to the android project (can be java or a +# directory containing the files) +#android.add_src = + +# (str) python-for-android branch to use, if not master, useful to try +# not yet merged features. +android.branch = master + +# (str) OUYA Console category. Should be one of GAME or APP +# If you leave this blank, OUYA support will not be enabled +#android.ouya.category = GAME + +# (str) Filename of OUYA Console icon. It must be a 732x412 png image. +#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png + +# (str) XML file to include as an intent filters in <activity> tag +#android.manifest.intent_filters = + +# (list) Android additionnal libraries to copy into libs/armeabi +android.add_libs_armeabi = lib/android/*.so + +# (bool) Indicate whether the screen should stay on +# Don't forget to add the WAKE_LOCK permission if you set this to True +#android.wakelock = False + +# (list) Android application meta-data to set (key=value format) +#android.meta_data = + +# (list) Android library project to add (will be added in the +# project.properties automatically.) +#android.library_references = + +# +# iOS specific +# + +# (str) Name of the certificate to use for signing the debug version +# Get a list of available identities: buildozer ios list_identities +#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)" + +# (str) Name of the certificate to use for signing the release version +#ios.codesign.release = %(ios.codesign.debug)s + + +[buildozer] + +# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) +log_level = 2 + + +# ----------------------------------------------------------------------------- +# List as sections +# +# You can define all the "list" as [section:key]. +# Each line will be considered as a option to the list. +# Let's take [app] / source.exclude_patterns. +# Instead of doing: +# +# [app] +# source.exclude_patterns = license,data/audio/*.wav,data/images/original/* +# +# This can be translated into: +# +# [app:source.exclude_patterns] +# license +# data/audio/*.wav +# data/images/original/* +# + +# ----------------------------------------------------------------------------- +# Profiles +# +# You can extend section / key with a profile +# For example, you want to deploy a demo version of your application without +# HD content. You could first change the title to add "(demo)" in the name +# and extend the excluded directories to remove the HD content. +# +# [app@demo] +# title = My Application (demo) +# +# [app:source.exclude_patterns@demo] +# images/hd/* +# +# Then, invoke the command line with the "demo" profile: +# +# buildozer --profile demo android debug diff --git a/gui/kivy/ui_screens/mainscreen.kv b/gui/kivy/ui_screens/mainscreen.kv @@ -1,286 +0,0 @@ -#:import TabbedCarousel electrum_gui.kivy.tabbed_carousel.TabbedCarousel -#:import ScreenDashboard electrum_gui.kivy.screens.ScreenDashboard -#:import Factory kivy.factory.Factory -#:import Carousel electrum_gui.kivy.carousel.Carousel - -Screen: - canvas.before: - Color: - rgba: 0.917, 0.917, 0.917, 1 - Rectangle: - size: self.size - pos: self.pos - BoxLayout: - orientation: 'vertical' - ActionBar: - id: action_bar - size_hint: 1, None - height: '40dp' - border: 4, 4, 4, 4 - background_image: 'atlas://gui/kivy/theming/light/action_bar' - ScreenManager: - id: manager - ScreenTabs: - id: tabs - name: "tabs" - #ScreenPassword: - # id: password - # name: 'password' - -<TabbedCarousel> - carousel: carousel - do_default_tab: False - Carousel: - scroll_timeout: 190 - anim_type: 'out_quart' - min_move: .05 - anim_move_duration: .1 - anim_cancel_duration: .54 - scroll_distance: '10dp' - on_index: root.on_index(*args) - id: carousel - -################################ -## Cards (under Dashboard) -################################ - -<Card@GridLayout> - cols: 1 - padding: '12dp' , '22dp', '12dp' , '12dp' - spacing: '12dp' - size_hint: 1, None - height: max(100, self.minimum_height) - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - border: 9, 9, 9, 9 - source: 'atlas://gui/kivy/theming/light/card' - size: self.size - pos: self.pos - -<CardLabel@Label> - color: 0.45, 0.45, 0.45, 1 - size_hint: 1, None - text: '' - text_size: self.width, None - height: self.texture_size[1] - halign: 'left' - valign: 'top' - -<CardButton@Button> - background_normal: 'atlas://gui/kivy/theming/light/card_btn' - bold: True - font_size: '10sp' - color: 0.699, 0.699, 0.699, 1 - size_hint: None, None - size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) - -<CardSeparator@Widget> - size_hint: 1, None - height: dp(1) - color: .909, .909, .909, 1 - canvas: - Color: - rgba: root.color if root.color else (0, 0, 0, 0) - Rectangle: - size: self.size - pos: self.pos - -<CardRecentActivity@Card> - BoxLayout: - size_hint: 1, None - height: lbl.height - CardLabel: - id: lbl - text: _('RECENT ACTIVITY') - CardButton: - id: btn_see_all - text: _('SEE ALL') - font_size: '12sp' - on_release: app.gui.main_gui.update_history(see_all=True) - GridLayout: - id: content - spacing: '7dp' - cols: 1 - size_hint: 1, None - height: self.minimum_height - CardSeparator - -<CardPaymentRequest@Card> - CardLabel: - text: _('PAYMENT REQUEST') - CardSeparator: - -<CardStatusInfo@Card> - status: app.status - base_unit: 'BTC' - quote_text: '.' - unconfirmed: '.' - BoxLayout: - size_hint: 1, None - height: '72dp' - IconButton: - mipmap: True - color: .90, .90, .90, 1 - source: 'atlas://gui/kivy/theming/light/qrcode' - size_hint: None, 1 - width: self.height - on_release: - Factory.WalletAddressesDialog().open() - GridLayout: - id: grid - cols: 1 - orientation: 'vertical' - CardLabel: - halign: 'right' - valign: 'top' - bold: True - size_hint: 1, None - font_size: '38sp' - text: - '[color=#4E4F4F]{}[/color]'\ - '[sup][color=9b948d]{}[/color][/sup]'\ - .format(unicode(root.status), root.base_unit) - CardLabel - halign: 'right' - markup: True - font_size: '15dp' - text: '[color=#c3c3c3]{}[/color]'.format(root.quote_text) - CardLabel - halign: 'right' - markup: True - text: '[color=#c3c3c3]{}[/color]'.format(root.unconfirmed) - -<DashboardActionView@ActionView> - ActionPrevious: - id: action_previous - app_icon: 'atlas://gui/kivy/theming/light/wallets' - with_previous: False - size_hint: None, 1 - mipmap: True - width: '77dp' - ActionButton: - id: action_logo - important: True - size_hint: 1, 1 - markup: True - mipmap: True - bold: True - font_size: '22dp' - icon: 'atlas://gui/kivy/theming/light/logo' - minimum_width: '1dp' - ActionButton: - id: action_contact - important: True - width: '25dp' - icon: 'atlas://gui/kivy/theming/light/add_contact' - text: 'Add Contact' - on_release: NewContactDialog().open() - ActionOverflow: - id: action_preferences - canvas.after: - Color: - rgba: 1, 1, 1, 1 - border: 0, 0, 0, 0 - overflow_image: 'atlas://gui/kivy/theming/light/settings' - width: '32dp' - ActionButton: - text: _('Seed') - on_release: - action_preferences._dropdown.dismiss() - if app.wallet.seed: app.gui.main_gui.protected_seed_dialog(self) - ActionButton: - text: _('Password') - ActionButton: - text: _('Network') - on_release: - app.root.current = 'screen_network' - action_preferences._dropdown.dismiss() - ActionButton: - text: _('Preferences') - on_release: - action_preferences._dropdown.dismiss() - app.gui.main_gui.show_settings_dialog(self) - -<ScreenDashboard> - action_view: Factory.DashboardActionView() - ScrollView: - do_scroll_x: False - RelativeLayout: - size_hint: 1, None - height: grid.height - GridLayout - id: grid - cols: 1 #if root.width < root.height else 2 - size_hint: 1, None - height: self.minimum_height - padding: '12dp' - spacing: '12dp' - GridLayout: - cols: 1 - size_hint: 1, None - height: self.minimum_height - spacing: '12dp' - orientation: 'vertical' - CardStatusInfo: - id: status_card - CardPaymentRequest: - id: payment_card - CardRecentActivity: - id: recent_activity_card - -<CleanHeader@TabbedPanelHeader> - border: 0, 0, 4, 0 - markup: False - color: (0.191, 0.558, 0.742, 1) if self.state == 'down' else (0.636, 0.636, 0.636, 1) - text_size: self.size - halign: 'center' - valign: 'middle' - bold: True - font_size: '12sp' - background_normal: 'atlas://gui/kivy/theming/light/tab_btn' - background_disabled_normal: 'atlas://gui/kivy/theming/light/tab_btn_disabled' - background_down: 'atlas://gui/kivy/theming/light/tab_btn_pressed' - canvas.before: - Color: - rgba: 1, 1, 1, .7 - Rectangle: - size: self.size - pos: self.x + 1, self.y - 1 - texture: self.texture - -<ScreenTabs@Screen> - TabbedCarousel: - id: panel - background_image: 'atlas://gui/kivy/theming/light/tab' - strip_image: 'atlas://gui/kivy/theming/light/tab_strip' - strip_border: 4, 0, 2, 0 - ScreenDashboard: - id: screen_dashboard - tab: tab_dashboard - #ScreenSend: - # id: screen_send - # tab: tab_send - #ScreenReceive: - # id: screen_receive - # tab: tab_receive - #ScreenContacts: - # id: screen_contacts - # tab: tab_contacts - CleanHeader: - id: tab_dashboard - text: _('DASHBOARD') - slide: 0 - #CleanHeader: - # id: tab_send - # text: _('SEND') - # slide: 1 - #CleanHeader: - # id: tab_receive - # text: _('RECEIVE') - # slide: 2 - #CleanHeader: - # id: tab_contacts - # text: _('CONTACTS') - # slide: 3- \ No newline at end of file diff --git a/gui/kivy/ui_screens/screenreceive.kv b/gui/kivy/ui_screens/screenreceive.kv @@ -0,0 +1,129 @@ +<ScreenReceiveContent@BoxLayout> + opacity: 0 + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + mode: 'qr' + orientation: 'vertical' + SendReceiveToggle + SendToggle: + id: toggle_qr + text: 'QR' + state: 'down' if root.mode == 'qr' else 'normal' + source: 'atlas://gui/kivy/theming/light/qrcode' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'qr': root.mode = 'nr' + root.mode = 'qr' + SendToggle: + id: toggle_nfc + text: 'NFC' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'nr' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + #size_hint: 1, None + #height: self.minimum_height + SendReceiveCardTop + height: '110dp' + BoxLayout: + size_hint: 1, None + height: '42dp' + rows: 1 + Label: + color: amount_e.foreground_color + bold: True + text_size: self.size + valign: 'bottom' + font_size: '22sp' + text: app.base_unit + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + on_text_validate: payto_e.focus = True + CardSeparator + BoxLayout: + size_hint: 1, None + height: '32dp' + spacing: '5dp' + Label: + id: lbl_quote + font_size: '12dp' + size_hint: .5, 1 + color: .761, .761, .761, 1 + #text: app.create_quote_text(Decimal(amount_e.text)) + text_size: self.size + halign: 'left' + valign: 'middle' + Label: + color: lbl_quote.color + font_size: '12dp' + text: 'Ask to scan the QR below' + text_size: self.size + halign: 'right' + valign: 'middle' + SendReceiveBlueBottom + id: blue_bottom + padding: '12dp', 0, '12dp', '12dp' + WalletSelector: + id: wallet_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + CardSeparator + opacity: wallet_selection.opacity + color: blue_bottom.foreground_color + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + on_text: + if not args[1].startswith('Select'):\ + qr.data = app.encode_uri(self.text) + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + Widget: + size_hint_y: None + height: dp(10) + BoxLayout + #size_hint: 1, None + #height: '160dp' if app.expert_mode else '220dp' + Widget + QRCodeWidget: + id: qr + size_hint: None, 1 + width: self.height + data: app.encode_uri(app.wallet.addresses()[0]) if app.wallet.addresses() else '' + on_touch_down: + if self.collide_point(*args[1].pos):\ + app.show_info_bubble(icon=self.ids.qrimage.texture, text='texture') + Widget + CreateAccountButtonGreen: + background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Goto next step') if app.wallet.seed else _('Create unsigned transaction') + size_hint_y: None + height: '38dp' + disabled: True if wallet_selection.opacity == 0 else False + on_release: + message = 'sending {} {} to {}'.format(\ + app.base_unit, amount_e.text, payto_e.text) + app.gui.main_gui.do_send(self, message=message)+ \ No newline at end of file diff --git a/gui/kivy/ui_screens/screensend.kv b/gui/kivy/ui_screens/screensend.kv @@ -0,0 +1,187 @@ +<TextInputSendBlue@TextInput> + padding: '5dp' + size_hint: 1, None + height: '27dp' + pos_hint: {'center_y':.5} + multiline: False + hint_text_color: self.foreground_color + foreground_color: .843, .914, .972, 1 + background_color: 1, 1, 1, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_active: 'atlas://gui/kivy/theming/light/textinput_active' + +<ScreenSendContent@BoxLayout> + opacity: 0 + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + orientation: 'vertical' + mode: 'address' + SendReceiveToggle: + SendToggle: + id: toggle_address + text: 'ADDRESS' + group: 'send_type' + state: 'down' if root.mode == 'address' else 'normal' + source: 'atlas://gui/kivy/theming/light/globe' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'address': root.mode = 'fc' + root.mode = 'address' + SendToggle: + id: toggle_nfc + text: 'NFC' + group: 'send_type' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'str' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + size_hint: 1, None + height: self.minimum_height + SendReceiveCardTop + id: card_address + BoxLayout + size_hint: 1, None + height: '42dp' + rows: 1 + Label + bold: True + color: amount_e.foreground_color + text_size: self.size + valign: 'bottom' + font_size: '22sp' + text: app.base_unit + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + on_text_validate: payto_e.focus = True + CardSeparator + BoxLayout: + size_hint: 1, None + height: '42dp' + spacing: '5dp' + Label: + font_size: '12dp' + color: lbl_fee.color + text: app.gui.main_gui.create_quote_text(Decimal(amount_e.text)) if hasattr(app, 'gui') else '0' + text_size: self.size + halign: 'left' + valign: 'middle' + Label: + id: lbl_fee + color: .761, .761, .761, 1 + font_size: '12dp' + text: '[b]{}[/b] of fee'.format(fee_e.value) + text_size: self.size + halign: 'right' + valign: 'middle' + IconButton: + id: fee_e + source: 'atlas://gui/kivy/theming/light/contact' + text: str(self.value) + value: .0005 + pos_hint: {'center_y': .5} + size_hint: None, None + size: '32dp', '32dp' + on_release: print 'TODO' + SendReceiveBlueBottom: + id: blue_bottom + size_hint: 1, None + height: self.minimum_height + BoxLayout + size_hint: 1, None + height: blue_bottom.item_height + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/contact' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: payto_e + hint_text: "Enter Contact or adress" + on_text_validate: + Factory.Animation(opacity=1,\ + height=blue_bottom.item_height)\ + .start(message_selection) + message_e.focus = True + Widget: + size_hint: None, None + width: dp(2) + height: qr.height + pos_hint: {'center_y':.5} + canvas.after: + Rectangle: + size: self.size + pos: self.pos + IconButton: + id: qr + source: 'atlas://gui/kivy/theming/light/qrcode' + pos_hint: {'center_y': .5} + size_hint: None, None + size: '22dp', '22dp' + CardSeparator + opacity: message_selection.opacity + color: blue_bottom.foreground_color + BoxLayout: + id: message_selection + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/pen' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: message_e + hint_text: 'Enter description here' + on_text_validate: + anim = Factory.Animation(opacity=1, height=blue_bottom.item_height) + anim.start(wallet_selection) + #anim.start(address_selection) + CardSeparator + opacity: wallet_selection.opacity + color: blue_bottom.foreground_color + WalletSelector: + id: wallet_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + CreateAccountButtonGreen: + background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Goto next step') if app.wallet.seed else _('Create unsigned transaction') + size_hint_y: None + height: '38dp' + disabled: True if wallet_selection.opacity == 0 else False + on_release: + message = 'sending {} {} to {}'.format(\ + app.base_unit, amount_e.text, payto_e.text) + app.gui.main_gui.do_send(self, message=message) + Widget diff --git a/gui/kivy/uix/__init__.py b/gui/kivy/uix/__init__.py @@ -0,0 +1 @@ + diff --git a/gui/kivy/combobox.py b/gui/kivy/uix/combobox.py diff --git a/gui/kivy/console.py b/gui/kivy/uix/console.py diff --git a/gui/kivy/uix/dialogs/__init__.py b/gui/kivy/uix/dialogs/__init__.py @@ -0,0 +1,190 @@ +from kivy.app import App +from kivy.clock import Clock +from kivy.factory import Factory +from kivy.properties import NumericProperty, StringProperty, BooleanProperty +from kivy.core.window import Window + +from electrum.i18n import _ + + + +class AnimatedPopup(Factory.Popup): + ''' An Animated Popup that animates in and out. + ''' + + anim_duration = NumericProperty(.25) + '''Duration of animation to be used + ''' + + __events__ = ['on_activate', 'on_deactivate'] + + + def on_activate(self): + '''Base function to be overridden on inherited classes. + Called when the popup is done animating. + ''' + pass + + def on_deactivate(self): + '''Base function to be overridden on inherited classes. + Called when the popup is done animating. + ''' + pass + + def open(self): + '''Do the initialization of incoming animation here. + Override to set your custom animation. + ''' + def on_complete(*l): + self.dispatch('on_activate') + + self.opacity = 0 + super(AnimatedPopup, self).open() + anim = Factory.Animation(opacity=1, d=self.anim_duration) + anim.bind(on_complete=on_complete) + anim.start(self) + + def dismiss(self): + '''Do the initialization of incoming animation here. + Override to set your custom animation. + ''' + def on_complete(*l): + super(AnimatedPopup, self).dismiss() + self.dispatch('on_deactivate') + + anim = Factory.Animation(opacity=0, d=.25) + anim.bind(on_complete=on_complete) + anim.start(self) + +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') + + def on_release(self, instance): + pass + + def on_press(self, instance): + pass + + def close(self): + self._on_release = None + self.dismiss() + + +class SelectionDialog(EventsDialog): + + def add_widget(self, widget, index=0): + if self.content: + self.content.add_widget(widget, index) + return + super(SelectionDialog, self).add_widget(widget) + + +class InfoBubble(Factory.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 '' + + :attr:`icon` is a `StringProperty` defaults to `''` + ''' + + fs = BooleanProperty(False) + ''' Show Bubble in half screen mode + + :attr:`fs` is a `BooleanProperty` defaults to `False` + ''' + + modal = BooleanProperty(False) + ''' Allow bubble to be hidden on touch. + + :attr:`modal` is a `BooleanProperty` defauult to `False`. + ''' + + exit = BooleanProperty(False) + '''Indicates whether to exit app after bubble is closed. + + :attr:`exit` is a `BooleanProperty` defaults to False. + ''' + + dim_background = BooleanProperty(False) + ''' Indicates Whether to draw a background on the windows behind the bubble. + + :attr:`dim` is a `BooleanProperty` defaults to `False`. + ''' + + def on_touch_down(self, touch): + if self.modal: + return True + self.hide() + if self.collide_point(*touch.pos): + return True + + def show(self, pos, duration, width=None, modal=False, exit=False): + '''Animate the bubble into position''' + self.modal, self.exit = modal, exit + if width: + self.width = width + if self.modal: + from kivy.uix.modalview import ModalView + self._modal_view = m = ModalView(background_color=[.5, .5, .5, .2]) + Window.add_widget(m) + m.add_widget(self) + else: + 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 = Factory.Animation(opacity=1, pos=pos, d=.32) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) + + + def hide(self, now=False): + ''' Auto fade out the Bubble + ''' + def on_stop(*l): + if self.modal: + m = self._modal_view + m.remove_widget(self) + Window.remove_widget(m) + Window.remove_widget(self) + if self.exit: + App.get_running_app().stop() + import sys + sys.exit() + if now: + return on_stop() + + anim = Factory.Animation(opacity=0, d=.25) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) diff --git a/gui/kivy/uix/dialogs/carousel_dialog.py b/gui/kivy/uix/dialogs/carousel_dialog.py @@ -0,0 +1,239 @@ +''' Dialogs intended to be used along with a slidable carousel inside +and indicators on either top, left, bottom or right side. These indicators can +be touched to travel to a particular slide. +''' +from electrum.i18n import _ + + +from kivy.app import App +from kivy.clock import Clock +from kivy.properties import NumericProperty, ObjectProperty +from kivy.factory import Factory +from kivy.lang import Builder + +import weakref + + +class CarouselHeader(Factory.TabbedPanelHeader): + '''Tabbed Panel Header with a circular image on top to be used as a + indicator for the current slide. + ''' + + slide = NumericProperty(0) + ''' indicates the link to carousels slide''' + + +class CarouselDialog(Factory.AnimatedPopup): + ''' A Popup dialog with a CarouselIndicator used as the content. + ''' + + carousel_content = ObjectProperty(None) + + def add_widget(self, widget, index=0): + if isinstance(widget, Factory.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 WalletAddressesDialog(CarouselDialog): + ''' Show current wallets and their addresses using qrcode widget + ''' + + def __init__(self, **kwargs): + self._loaded = False + super(WalletAddressesDialog, self).__init__(**kwargs) + + def on_activate(self): + # do activate routine here + slide = None + + if not self._loaded: + self._loaded = True + CarouselHeader = Factory.CarouselHeader + ch = CarouselHeader() + ch.slide = 0 # idx + slide = Factory.ScreenAddress() + + slide.tab = ch + + self.add_widget(slide) + self.add_widget(ch) + + app = App.get_running_app() + if not slide: + slide = self.carousel_content.carousel.slides[0] + + # add a tab for each wallet + self.wallet_name = app.wallet.get_account_names()[0] + labels = app.wallet.labels + + addresses = app.wallet.addresses() + _labels = {} + + for address in addresses: + _labels[labels.get(address, address)] = address + + slide.labels = _labels + Clock.schedule_once(lambda dt: self._setup_slide(slide)) + + def _setup_slide(self, slide): + btn_address = slide.ids.btn_address + btn_address.values = values = slide.labels.keys() + if not btn_address.text: + btn_address.text = values[0] + + +class RecentActivityDialog(CarouselDialog): + ''' + ''' + def on_activate(self): + # animate to first slide + carousel = self.carousel_content.carousel + carousel.load_slide(carousel.slides[0]) + + item = self.item + try: + self.address = item.address + except ReferenceError: + self.dismiss() + return + + self.amount = item.amount[1:] + self.amount_color = item.amount_color + self.confirmations = item.confirmations + self.quote_text = item.quote_text + date_time = item.date.split() + if len(date_time) == 2: + self.date = date_time[0] + self.time = date_time[1] + self.status = 'Validated' + else: + self.date = item.date + self.status = 'Pending' + self.tx_hash = item.tx_hash + + app = App.get_running_app() + + tx_hash = item.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) + else: + is_mine = False + + self.is_mine = is_mine + + if is_mine: + if fee is not None: + self.fee = app.format_amount(fee) + else: + self.fee = 'unknown' + + labels = app.wallet.labels + addresses = app.wallet.addresses() + _labels = {} + + self.wallet_name = app.wallet.get_account_names()[0] + for address in addresses: + _labels[labels.get(address, address)] = address + + self.labels = _labels + + def open(self): + self._trans_actv = self._det_actv = self._in_actv\ + = self._out_actv = False + super(RecentActivityDialog, self).open() + + def dismiss(self): + if self._in_actv: + self.ids.list_inputs.content = "" + self.ids.list_inputs.clear_widgets() + if self._out_actv: + self.ids.list_outputs.content = "" + self.ids.list_outputs.clear_widgets() + super(RecentActivityDialog, self).dismiss() + + def dropdown_selected(self, value): + app = App.get_running_app() + try: + labels = self.labels + except AttributeError: + return + + address = labels.get(self.address, self.address[1:]) + + if value.startswith(_('Copy')): + app.copy(address) + elif value.startswith(_('Send')): + app.send_payment(address) + self.dismiss() + + def activate_screen_transactionid(self, screen): + if self._trans_actv: + return + + self._trans_actv = True + Clock.schedule_once( + lambda dt: self._activate_screen_transactionid(screen), .1) + + def _activate_screen_transactionid(self, screen): + content = screen.content + if not content: + content = Factory.RecentActivityScrTransID() + screen.content = content + screen.add_widget(content) + content.tx_hash = self.tx_hash + content.text_color = self.text_color + content.carousel_content = self.carousel_content + + def activate_screen_inputs(self, screen): + if self._in_actv: + return + + self._in_actv = True + Clock.schedule_once( + lambda dt: self._activate_screen_inputs(screen), .1) + + def _activate_screen_inputs(self, screen): + content = screen.content + if not content: + content = Factory.RecentActivityScrInputs() + screen.content = content + screen.add_widget(content) + self.populate_inputs_outputs(content, 'in') + + def activate_screen_outputs(self, screen): + if self._out_actv: + return + + self._out_actv = True + Clock.schedule_once( + lambda dt: self._activate_screen_outputs(screen), .1) + + def _activate_screen_outputs(self, screen): + content = screen.content + if not content: + content = Factory.RecentActivityScrOutputs() + screen.content = content + screen.add_widget(content) + self.populate_inputs_outputs(content, 'out') + + def populate_inputs_outputs(self, content, mode): + app = App.get_running_app() + tx_hash = self.tx_hash + if tx_hash: + tx = app.wallet.transactions.get(tx_hash) + if mode == 'out': + content.data = \ + [(address, app.format_amount(value))\ + for address, value in tx.outputs] + else: + content.data = \ + [(input['address'], input['prevout_hash'])\ + for input in tx.inputs] diff --git a/gui/kivy/uix/dialogs/create_restore.py b/gui/kivy/uix/dialogs/create_restore.py @@ -0,0 +1,488 @@ +''' Dialogs and widgets Responsible for creation, restoration of accounts are +defined here. + +Namely: CreateAccountDialog, CreateRestoreDialog, ChangePasswordDialog, +RestoreSeedDialog +''' + +from functools import partial + +from kivy.app import App +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ObjectProperty, StringProperty, OptionProperty +from kivy.core.window import Window + +from electrum_gui.kivy.uix.dialogs import EventsDialog + +from electrum.i18n import _ + + +Builder.load_string(''' +#:import Window kivy.core.window.Window +#:import _ electrum.i18n._ + + +<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' + + +<-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: None, None + size: '5dp', '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 + + +<RestoreSeedDialog> + GridLayout + cols: 1 + padding: 0, '12dp' + orientation: 'vertical' + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + CreateAccountTextInput: + id: text_input_seed + size_hint: 1, None + height: '110dp' + hint_text: + _('Enter your seedphrase') + on_text: next.disabled = not bool(root._wizard.is_any(self)) + Label: + font_size: '12sp' + text_size: self.width, None + size_hint: 1, None + height: self.texture_size[1] + halign: 'justify' + valign: 'middle' + text: + _('If you need additional information, please check ' + '[color=#0000ff][ref=1]' + 'https://electrum.org/faq.html#seed[/ref][/color]') + on_ref_press: + import webbrowser + webbrowser.open('https://electrum.org/faq.html#seed') + 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 + + +<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' + GridLayout: + size_hint_y: None + height: self.minimum_height + cols: 1 + 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 + Widget: + size_hint_y: None + height: '13dp' + 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', 'restore') else False + opacity: 0 if self.disabled else 1 + multiline: False + on_text_validate: + ti_new_password.focus = True + Widget: + size_hint_y: None + height: 0 if ti_password.disabled else '13dp' + CreateAccountTextInput: + id: ti_new_password + hint_text: 'Enter new pincode' + multiline: False + password: True + on_text_validate: ti_confirm_password.focus = True + Widget: + size_hint_y: None + height: '13dp' + CreateAccountTextInput: + id: ti_confirm_password + hint_text: 'Confirm pincode' + password: True + multiline: False + on_text_validate: root.validate_new_password() + Widget + GridLayout: + rows: 1 + spacing: '12dp' + size_hint: 1, None + height: self.minimum_height + CreateAccountButtonBlue: + id: back + text: _('Back') + root: root + disabled: True if root.mode[0] == 'r' else self.disabled + CreateAccountButtonGreen: + id: next + text: _('Confirm') if root.mode[0] == 'r' else _('Next') + root: root + +''') + + +class CreateAccountDialog(EventsDialog): + ''' Abstract dialog to be used as the base for all Create Account Dialogs + ''' + crcontent = ObjectProperty(None) + + def __init__(self, **kwargs): + super(CreateAccountDialog, self).__init__(**kwargs) + self.action = kwargs.get('action') + _trigger_size_dialog = Clock.create_trigger(self._size_dialog) + Window.bind(size=_trigger_size_dialog, + rotation=_trigger_size_dialog) + _trigger_size_dialog() + + def _size_dialog(self, dt): + app = App.get_running_app() + 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 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 CreateRestoreDialog(CreateAccountDialog): + ''' Initial Dialog for creating or restoring seed''' + + def on_parent(self, instance, value): + if value: + app = App.get_running_app() + 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): + app = App.get_running_app() + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(CreateRestoreDialog, self).close() + + +class ChangePasswordDialog(CreateAccountDialog): + + message = StringProperty(_('Empty Message')) + '''Message to be displayed.''' + + mode = OptionProperty('new', + options=('new', 'confirm', 'create', 'restore')) + ''' Defines the mode of the password dialog.''' + + def validate_new_password(self): + self.ids.next.dispatch('on_release') + + def on_parent(self, instance, value): + if value: + # change the stepper image used to indicate the current state + stepper = self.ids.stepper + stepper.opacity = 1 + t_wallet_name = self.ids.ti_wallet_name + if self.mode in ('create', 'restore'): + t_wallet_name.text = 'Default Wallet' + t_wallet_name.readonly = True + #self.ids.ti_new_password.focus = True + else: + t_wallet_name.text = '' + t_wallet_name.readonly = False + #t_wallet_name.focus = True + stepper.source = 'atlas://gui/kivy/theming/light/stepper_left' + self._back = _back = partial(self.ids.back.dispatch, 'on_release') + app = App.get_running_app() + 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 + app = App.get_running_app() + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(ChangePasswordDialog, self).close() + + +class InitSeedDialog(CreateAccountDialog): + + mode = StringProperty('create') + ''' Defines the mode for which to optimize the UX. defaults to 'create'. + + Can be one of: 'create', 'restore', 'create_2of2', 'create_2fa'... + ''' + + 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: + app = App.get_running_app() + 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): + app = App.get_running_app() + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(InitSeedDialog, self).close() + + +class RestoreSeedDialog(CreateAccountDialog): + + def __init__(self, **kwargs): + self._wizard = kwargs['wizard'] + super(RestoreSeedDialog, self).__init__(**kwargs) + + def on_parent(self, instance, value): + if value: + tis = self.ids.text_input_seed + tis.focus = True + tis._keyboard.bind(on_key_down=self.on_key_down) + stepper = self.ids.stepper + stepper.opacity = 1 + stepper.source = ('atlas://gui/kivy/theming' + '/light/stepper_restore_seed') + self._back = _back = partial(self.ids.back.dispatch, + 'on_release') + app = App.get_running_app() + app.navigation_higherarchy.append(_back) + + def on_key_down(self, keyboard, keycode, key, modifiers): + if keycode[0] in (13, 271): + self.on_enter() + return True + + def on_enter(self): + #self._remove_keyboard() + # press next + next = self.ids.next + if not next.disabled: + next.dispatch('on_release') + + def _remove_keyboard(self): + tis = self.ids.text_input_seed + if tis._keyboard: + tis._keyboard.unbind(on_key_down=self.on_key_down) + tis.focus = False + + def close(self): + self._remove_keyboard() + app = App.get_running_app() + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(RestoreSeedDialog, self).close() diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py @@ -0,0 +1,478 @@ +from electrum import Wallet +from electrum.i18n import _ + +from kivy.app import App +from kivy.uix.widget import Widget +from kivy.core.window import Window +from kivy.clock import Clock +from kivy.factory import Factory + +Factory.register('CreateRestoreDialog', + module='electrum_gui.kivy.uix.dialogs.create_restore') + +import sys +import threading +from functools import partial +import weakref + +# global Variables +app = App.get_running_app() + + +class InstallWizard(Widget): + '''Installation Wizard. Responsible for instantiating the + creation/restoration of wallets. + + events:: + `on_wizard_complete` Fired when the wizard is done creating/ restoring + wallet/s. + ''' + + __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."), + on_complete=None): + '''Perform a blocking task in the background by running the passed + method in a thread. + ''' + + def target(): + + # run your threaded function + try: + task() + except Exception as err: + Clock.schedule_once(lambda dt: app.show_error(str(err))) + + # on completion hide message + Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) + + # call completion routine + if on_complete: + Clock.schedule_once(lambda dt: on_complete()) + + 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 get_seed_text(self, ti_seed): + text = unicode(ti_seed.text.lower()).strip() + text = ' '.join(text.split()) + return text + + def is_any(self, seed_e): + text = self.get_seed_text(seed_e) + return (Wallet.is_seed(text) or + Wallet.is_mpk(text) or + Wallet.is_address(text) or + Wallet.is_private_key(text)) + + def run(self, action): + '''Entry point of our Installation wizard + ''' + if not action: + return + + Factory.CreateRestoreDialog( + on_release=self.on_creatrestore_complete, + action=action).open() + + def on_creatrestore_complete(self, dialog, button): + if not button: + # soft back or escape button pressed + return self.dispatch('on_wizard_complete', None) + dialog.close() + + action = dialog.action + if button == dialog.ids.create: + # create + # TODO take from UI instead of hardcoding + #t = dialog.wallet_type + t = 'standard' + + if t == 'standard': + wallet = Wallet(self.storage) + action = 'create' + + elif t == '2fa': + wallet = Wallet_2of3(self.storage) + run_hook('create_cold_seed', wallet, self) + self.create_cold_seed(wallet) + return + + elif t == '2of2': + wallet = Wallet_2of2(self.storage) + action = 'create_2of2_1' + + elif t == '2of3': + wallet = Wallet_2of3(self.storage) + action = 'create_2of3_1' + + if action in ['create_2fa_2', 'create_2of3_2']: + wallet = Wallet_2of3(self.storage) + + if action in ['create', 'create_2of2_1', + 'create_2fa_2', 'create_2of3_1']: + self.password_dialog(wallet=wallet, mode=action) + + elif button == dialog.ids.restore: + # restore + wallet = None + self.restore_seed_dialog(wallet) + + else: + self.dispatch('on_wizard_complete', None) + + def restore_seed_dialog(self, wallet): + #TODO t currently hardcoded + t = 'standard' + if t == 'standard': + from electrum_gui.kivy.uix.dialogs.create_restore import\ + RestoreSeedDialog + RestoreSeedDialog( + on_release=partial(self.on_verify_restore_ok, wallet), + wizard=weakref.proxy(self)).open() + + elif t in ['2fa', '2of2']: + r = self.multi_seed_dialog(1) + if not r: + return + text1, text2 = r + password = self.password_dialog(wallet=wallet) + if t == '2of2': + wallet = Wallet_2of2(self.storage) + elif t == '2of3': + wallet = Wallet_2of3(self.storage) + elif t == '2fa': + wallet = Wallet_2of3(self.storage) + + if Wallet.is_seed(text1): + wallet.add_seed(text1, password) + if Wallet.is_seed(text2): + wallet.add_cold_seed(text2, password) + else: + wallet.add_master_public_key("cold/", text2) + + elif Wallet.is_mpk(text1): + if Wallet.is_seed(text2): + wallet.add_seed(text2, password) + wallet.add_master_public_key("cold/", text1) + else: + wallet.add_master_public_key("m/", text1) + wallet.add_master_public_key("cold/", text2) + + if t == '2fa': + run_hook('restore_third_key', wallet, self) + + wallet.create_account() + + elif t in ['2of3']: + r = self.multi_seed_dialog(2) + if not r: + return + text1, text2, text3 = r + password = self.password_dialog() + wallet = Wallet_2of3(self.storage) + + if Wallet.is_seed(text1): + wallet.add_seed(text1, password) + if Wallet.is_seed(text2): + wallet.add_cold_seed(text2, password) + else: + wallet.add_master_public_key("cold/", text2) + + elif Wallet.is_mpk(text1): + if Wallet.is_seed(text2): + wallet.add_seed(text2, password) + wallet.add_master_public_key("cold/", text1) + else: + wallet.add_master_public_key("m/", text1) + wallet.add_master_public_key("cold/", text2) + + wallet.create_account() + + def on_verify_restore_ok(self, wallet, _dlg, btn, restore=False): + if btn in (_dlg.ids.back, _dlg.ids.but_close) : + _dlg.close() + Factory.CreateRestoreDialog( + on_release=self.on_creatrestore_complete).open() + return + + seed = self.get_seed_text(_dlg.ids.text_input_seed) + if not seed: + return app.show_error(_("No seed!"), duration=.5) + + _dlg.close() + + if Wallet.is_seed(seed): + return self.password_dialog(wallet=wallet, mode='restore', + seed=seed) + elif Wallet.is_mpk(seed): + wallet = Wallet.from_mpk(seed, self.storage) + elif Wallet.is_address(seed): + wallet = Wallet.from_address(seed, self.storage) + elif Wallet.is_private_key(seed): + wallet = Wallet.from_private_key(seed, self.storage) + else: + return app.show_error(_('Not a valid seed. App will now exit'), + exit=True, modal=True, duration=.5) + return + + + def show_seed(self, wallet=None, instance=None, password=None, + wallet_name=None, mode='create', seed=''): + if instance and (not wallet or not wallet.seed): + return app.show_error(_('No seed')) + + if not seed: + try: + seed = self.wallet.get_seed(password) + except Exception: + return app.show_error(_('Incorrect Password')) + + brainwallet = seed + + msg2 = _("[color=#414141]"+\ + "[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][b]lost forever![/b][/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() + mode = _dlg.mode + if _btn != _dlg.ids.confirm: + if not instance: + self.password_dialog(wallet, mode=mode) + return + # confirm + if instance is None: + # in initial phase create mode + # save seed with password + wallet.add_seed(seed, password) + sid = None if mode == 'create' else 'hot' + + if mode == 'create': + def create(password): + wallet.create_accounts(password) + wallet.synchronize() # generate first addresses offline + + self.waiting_dialog(partial(create, password), + on_complete=partial(self.load_network, + wallet, mode=mode)) + elif mode == 'create_2of2_1': + mode = 'create_2of2_2' + elif mode == 'create_2of3_1': + mode = 'create_2of3_2' + elif mode == 'create_2fa_2': + mode = 'create_2fa_3' + + if mode == 'create_2of2_2': + xpub_hot = wallet.master_public_keys.get("m/") + xpub = self.multi_mpk_dialog(xpub_hot, 1) + if not xpub: + return + wallet.add_master_public_key("cold/", xpub) + wallet.create_account() + self.waiting_dialog(wallet.synchronize) + + if mode == 'create_2of3_2': + xpub_hot = wallet.master_public_keys.get("m/") + r = self.multi_mpk_dialog(xpub_hot, 2) + if not r: + return + xpub1, xpub2 = r + wallet.add_master_public_key("cold/", xpub1) + wallet.add_master_public_key("remote/", xpub2) + wallet.create_account() + self.waiting_dialog(wallet.synchronize) + + if mode == 'create_2fa_3': + run_hook('create_remote_key', wallet, self) + if not wallet.master_public_keys.get("remote/"): + return + wallet.create_account() + self.waiting_dialog(wallet.synchronize) + + + from electrum_gui.kivy.uix.dialogs.create_restore import InitSeedDialog + InitSeedDialog(message=msg2, + seed_msg=brainwallet, on_release=on_ok_press, mode=mode).open() + + def password_dialog(self, wallet=None, instance=None, mode='create', + seed=''): + """Can be called directly (instance is None) + or from a callback (instance is not None)""" + app = App.get_running_app() + + if mode != 'create' and wallet and wallet.is_watching_only(): + return app.show_error('This is a watching only wallet') + + if instance and not wallet.seed: + return app.show_error('No seed !!', exit=True, modal=True) + + 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.") + + def on_release(wallet, seed, _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: + if mode == 'restore': + # back is disabled cause seed is already set + return + _dlg.close() + if not instance: + # back on create + Factory.CreateRestoreDialog( + on_release=self.on_creatrestore_complete).open() + return + + # Confirm + wallet_name = _dlg.ids.ti_wallet_name.text + new_password = unicode(ti_new_password.text) + new_password2 = unicode(ti_confirm_password.text) + + if new_password != new_password2: + # passwords don't match + 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'), duration=.5) + + if not new_password: + new_password = None + + if mode == 'restore': + wallet = Wallet.from_seed(seed, self.storage) + password = (unicode(ti_password.text) + if wallet and wallet.use_encryption else + None) + + def on_complete(*l): + wallet.create_accounts(new_password) + self.load_network(wallet, mode='restore') + _dlg.close() + + self.waiting_dialog(lambda: wallet.add_seed(seed, new_password), + msg=_("saving seed"), + on_complete=on_complete) + return + + if not instance: + # create mode + _dlg.close() + seed = wallet.make_seed() + + return self.show_seed(password=new_password, wallet=wallet, + wallet_name=wallet_name, mode=mode, + seed=seed) + + # change password mode + try: + seed = wallet.decode_seed(password) + except BaseException: + return app.show_error(_('Incorrect Password'), duration=.5) + + # test carefully + try: + wallet.update_password(seed, password, new_password) + except BaseException: + return app.show_error(_('Failed to update password'), exit=True) + 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.update_wallet() + + from electrum_gui.kivy.uix.dialogs.create_restore import ChangePasswordDialog + cpd = ChangePasswordDialog( + message=msg, + mode=mode, + on_release=partial(on_release, + wallet, seed)).open() + + def load_network(self, wallet, mode='create'): + #if not self.config.get('server'): + if self.network: + if self.network.interfaces: + if mode not in ('restore', 'create'): + self.network_dialog() + else: + app.show_error(_('You are offline')) + self.network.stop() + self.network = None + + if mode in ('restore', 'create'): + # auto cycle + self.config.set_key('auto_cycle', True, True) + + # start wallet threads + wallet.start_threads(self.network) + + if not mode == 'restore': + return self.dispatch('on_wizard_complete', wallet) + + def get_text(text): + def set_text(*l): app.info_bubble.ids.lbl.text=text + Clock.schedule_once(set_text) + + def on_complete(*l): + if not self.network: + app.show_info( + _("This wallet was restored offline. It may contain more" + " addresses than displayed."), duration=.5) + return self.dispatch('on_wizard_complete', wallet) + + if wallet.is_found(): + app.show_info(_("Recovery successful"), duration=.5) + else: + app.show_info(_("No transactions found for this seed"), + duration=.5) + return self.dispatch('on_wizard_complete', wallet) + + self.waiting_dialog(lambda: wallet.restore(get_text), + on_complete=on_complete) + + def on_wizard_complete(self, wallet): + pass diff --git a/gui/kivy/uix/dialogs/new_contact.py b/gui/kivy/uix/dialogs/new_contact.py @@ -0,0 +1,26 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.cache import Cache + +Factory.register('QrScannerDialog', module='electrum_gui.kivy.uix.dialogs.qr_scanner') + +class NewContactDialog(Factory.AnimatedPopup): + + def load_qr_scanner(self): + self.dismiss() + dlg = Cache.get('electrum_widgets', 'QrScannerDialog') + if not dlg: + dlg = Factory.QrScannerDialog() + Cache.append('electrum_widgets', 'QrScannerDialog', dlg) + dlg.bind(on_release=self.on_release) + dlg.open() + + def on_release(self, instance, uri): + self.new_contact(uri=uri) + + def new_contact(self, uri={}): + # load NewContactScreen + app = App.get_running_app() + #app.root. + # set contents of uri in the new contact screen diff --git a/gui/kivy/uix/dialogs/nfc_transaction.py b/gui/kivy/uix/dialogs/nfc_transaction.py @@ -0,0 +1,32 @@ +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)+ \ No newline at end of file diff --git a/gui/kivy/uix/dialogs/qr_scanner.py b/gui/kivy/uix/dialogs/qr_scanner.py @@ -0,0 +1,41 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.lang import Builder + +Factory.register('QRScanner', module='electrum_gui.kivy.qr_scanner') + +class QrScannerDialog(Factory.EventsDialog): + + def on_symbols(self, instance, value): + instance.stop() + self.dismiss() + uri = App.get_running_app().decode_uri(value[0].data) + #address = uri.get('address', 'empty') + #label = uri.get('label', '') + #amount = uri.get('amount', 0.0) + #message = uir.get('message', '') + self.dispatch('on_release', uri) + + +Builder.load_string(''' +<QrScannerDialog> + title: + _(\ + '[size=18dp]Hold your QRCode up to the camera[/size][size=7dp]\\n[/size]') + title_size: '24sp' + border: 7, 7, 7, 7 + size_hint: None, None + size: '320dp', '270dp' + pos_hint: {'center_y': .53} + separator_color: .89, .89, .89, 1 + separator_height: '1.2dp' + title_color: .437, .437, .437, 1 + background: 'atlas://gui/kivy/theming/light/dialog' + on_activate: + qrscr.start() + qrscr.size = self.size + on_deactivate: qrscr.stop() + QRScanner: + id: qrscr + on_symbols: root.on_symbols(*args) +''')+ \ No newline at end of file diff --git a/gui/kivy/uix/drawer.py b/gui/kivy/uix/drawer.py @@ -0,0 +1,257 @@ +'''Drawer Widget to hold the main window and the menu/hidden section that +can be swiped in from the left. This Menu would be only hidden in phone mode +and visible in Tablet Mode. + +This class is specifically in lined to save on start up speed(minimize i/o). +''' + +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import OptionProperty, NumericProperty, ObjectProperty +from kivy.clock import Clock +from kivy.lang import Builder + +import gc + +# delayed imports +app = None + + +class Drawer(Factory.RelativeLayout): + '''Drawer Widget to hold the main window and the menu/hidden section that + can be swiped in from the left. This Menu would be only hidden in phone mode + and visible in Tablet Mode. + + ''' + + 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('9dp') + '''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('9dp') + '''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) + ''' This is the widget that is hidden in phone mode on the left side of + drawer or displayed on the left of the overlay widget in tablet mode. + + :attr:`hidden_widget` is a `ObjectProperty` defaults to None. + ''' + + overlay_widget = ObjectProperty(None) + '''This a pointer to the default widget that is overlayed either on top or + to the right of the hidden widget. + ''' + + def __init__(self, **kwargs): + super(Drawer, self).__init__(**kwargs) + + self._triigger_gc = Clock.create_trigger(self._re_enable_gc, .2) + + def toggle_drawer(self): + if app.ui_mode[0] == 't': + return + Factory.Animation.cancel_all(self.overlay_widget) + anim = Factory.Animation(x=self.hidden_widget.width + if self.state in ('opening', 'closed') else 0, + d=.1, t='linear') + anim.bind(on_complete = self._complete_drawer_animation) + anim.start(self.overlay_widget) + + def _re_enable_gc(self, dt): + global gc + gc.enable() + + def on_touch_down(self, touch): + if self.disabled: + return + + if not self.collide_point(*touch.pos): + return + + touch.grab(self) + + # disable gc for smooth interaction + # This is still not enough while wallet is synchronising + # look into pausing all background tasks while ui interaction like this + gc.disable() + + global app + if not app: + app = App.get_running_app() + + # skip on tablet mode + if app.ui_mode[0] == 't': + return super(Drawer, self).on_touch_down(touch) + + state = self.state + touch.ud['send_touch_down'] = False + start = 0 #if state[0] == 'c' else self.hidden_widget.right + drag_area = self.drag_area\ + if self.state[0] == 'c' else\ + (self.overlay_widget.x) + + if touch.x < start or touch.x > drag_area: + if self.state == 'open': + self.toggle_drawer() + return + 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): + if not touch.grab_current is self: + return + self._touch = False + # 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) + + ov = self.overlay_widget + ov.x=min(self.hidden_widget.width, + max(ov.x + touch.dx*2, 0)) + + #_anim = Animation(x=x, duration=1/2, t='in_out_quart') + #_anim.cancel_all(ov) + #_anim.start(ov) + + 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.ungrab(self) + 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): + if not touch.grab_current is self: + return + + self._triigger_gc() + + touch.ungrab(self) + touch.grab_current = None + + # skip on tablet mode + get = touch.ud.get + if app.ui_mode[0] == 't': + return super(Drawer, self).on_touch_up(touch) + + self.old_x = [1, ] * 10 + self.speed = sum(( + (self.old_x[x + 1] - self.old_x[x]) for x in range(9))) / 9. + + if get('send_touch_down', None): + # touch up called before moving + Clock.unschedule(self._change_touch_mode) + self._touch = None + Clock.schedule_once( + lambda dt: super(Drawer, self).on_touch_down(touch)) + if get('in_drag_area', None): + if abs(touch.x - touch.ox) < self.scroll_distance: + anim_to = (0 if self.state[0] == 'c' + else self.hidden_widget.width) + Factory.Animation(x=anim_to, d=.1).start(self.overlay_widget) + return + touch.ud['in_drag_area'] = False + if not get('send_touch_down', None): + self.toggle_drawer() + Clock.schedule_once(lambda dt: super(Drawer, self).on_touch_up(touch)) + + def _complete_drawer_animation(self, *args): + self.state = 'open' if self.state in ('opening', 'closed') else 'closed' + + def add_widget(self, widget, index=1): + if not widget: + return + + iget = self.ids.get + if not iget('hidden_widget') or not iget('overlay_widget'): + super(Drawer, self).add_widget(widget) + return + + if not self.hidden_widget: + self.hidden_widget = self.ids.hidden_widget + if not self.overlay_widget: + self.overlay_widget = self.ids.overlay_widget + + if self.overlay_widget.children and self.hidden_widget.children: + Logger.debug('Drawer: Accepts only two widgets. discarding rest') + return + + if not self.hidden_widget.children: + self.hidden_widget.add_widget(widget) + else: + self.overlay_widget.add_widget(widget) + widget.x = 0 + + def remove_widget(self, widget): + if self.overlay_widget.children[0] == widget: + self.overlay_widget.clear_widgets() + return + if widget == self.hidden_widget.children: + self.hidden_widget.clear_widgets() + return + + def clear_widgets(self): + self.overlay_widget.clear_widgets() + self.hidden_widget.clear_widgets() + +if __name__ == '__main__': + from kivy.app import runTouchApp + from kivy.lang import Builder + runTouchApp(Builder.load_string(''' +Drawer: + Button: + Button +'''))+ \ No newline at end of file diff --git a/gui/kivy/uix/gridview.py b/gui/kivy/uix/gridview.py @@ -0,0 +1,205 @@ +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): + self._from_widths = False + super(GridView, self).__init__(**kwargs) + #self.on_headers(self, self.headers) + + def on_widths(self, instance, value): + if not self.get_root_window(): + return + 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/menus.py b/gui/kivy/uix/menus.py diff --git a/gui/kivy/uix/qrcodewidget.py b/gui/kivy/uix/qrcodewidget.py @@ -0,0 +1,180 @@ +''' 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: + import sys + 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: + self.x + dp(2), self.y + dp(2),\ + self.right - dp(2), self.y + dp(2),\ + self.right - dp(2), self.top - dp(2),\ + self.x + dp(2), self.top - dp(2),\ + self.x + dp(2), self.y + 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 = .05 + 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), .1) + + 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), .1) + 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/uix/screens.py b/gui/kivy/uix/screens.py @@ -0,0 +1,300 @@ +from kivy.app import App +from kivy.cache import Cache +from kivy.clock import Clock +from kivy.compat import string_types +from kivy.properties import (ObjectProperty, DictProperty, NumericProperty, + ListProperty) +from kivy.lang import Builder +from kivy.factory import Factory + + +# Delayed imports +app = None + + +class CScreen(Factory.Screen): + + __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave') + + action_view = ObjectProperty(None) + + def _change_action_view(self): + app = App.get_running_app() + action_bar = app.root.manager.current_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_enter(self): + # FIXME: use a proper event don't use animation time of screen + Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25) + + def on_activate(self): + Clock.schedule_once(lambda dt: self._change_action_view()) + + def on_leave(self): + self.dispatch('on_deactivate') + + def on_deactivate(self): + Clock.schedule_once(lambda dt: self._change_action_view()) + + def load_screen(self, screen_name): + content = self.content + if not content: + Builder.load_file('gui/kivy/uix/ui_screens/{}.kv'.format(screen_name)) + if screen_name.endswith('send'): + content = Factory.ScreenSendContent() + elif screen_name.endswith('receive'): + content = Factory.ScreenReceiveContent() + content.ids.toggle_qr.state = 'down' + self.content = content + self.add_widget(content) + Factory.Animation(opacity=1, d=.25).start(content) + return + if screen_name.endswith('receive'): + content.mode = 'qr' + else: + content.mode = 'address' + + +class EScreen(Factory.EffectWidget, CScreen): + + background_color = ListProperty((0.929, .929, .929, .929)) + + speed = NumericProperty(0) + + effect_flex_scroll = ''' +uniform float speed; + +vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) +{{ + return texture2D( + texture, + vec2(tex_coords.x + sin( + tex_coords.y * 3.1416 / .2 + 3.1416 / .5 + ) * speed, tex_coords.y)); +}} +''' + def __init__(self, **kwargs): + super(EScreen, self).__init__(**kwargs) + self.old_x = [1, ] * 10 + self._anim = Factory.Animation(speed=0, d=.22) + from kivy.uix.effectwidget import AdvancedEffectBase + self.speed = 0 + self.scrollflex = AdvancedEffectBase( + glsl=self.effect_flex_scroll, + uniforms={'speed': self.speed} + ) + self._trigger_straighten = Clock.create_trigger( + self.straighten_screen, .15) + + def on_speed(self, *args): + value = max(-0.05, min(0.05, float("{0:.5f}".format(args[1])))) + self.scrollflex.uniforms['speed'] = value + + def on_parent(self, instance, value): + if value: + value.bind(x=self.screen_moving) + + def screen_moving(self, instance, value): + self.old_x.append(value/self.width) + self.old_x.pop(0) + self.speed = sum(((self.old_x[x + 1] - self.old_x[x]) for x in range(9))) / 9. + self._anim.cancel_all(self) + self._trigger_straighten() + + def straighten_screen(self, dt): + self._anim.start(self) + + +class ScreenDashboard(EScreen): + ''' Dashboard screen: Used to display the main dashboard. + ''' + + tab = ObjectProperty(None) + + def __init__(self, **kwargs): + self.ra_dialog = None + super(ScreenDashboard, self).__init__(**kwargs) + + def show_tx_details(self, item): + ra_dialog = Cache.get('electrum_widgets', 'RecentActivityDialog') + if not ra_dialog: + Factory.register('RecentActivityDialog', + module='electrum_gui.kivy.uix.dialogs.carousel_dialog') + Factory.register('GridView', + module='electrum_gui.kivy.uix.gridview') + ra_dialog = ra_dialog = Factory.RecentActivityDialog() + Cache.append('electrum_widgets', 'RecentActivityDialog', ra_dialog) + ra_dialog.item = item + ra_dialog.open() + + +class ScreenAddress(CScreen): + '''This is the dialog that shows a carousel of the currently available + addresses. + ''' + + labels = DictProperty({}) + ''' + ''' + + tab = ObjectProperty(None) + ''' The tab associated With this Carousel + ''' + + +class ScreenPassword(Factory.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 MainScreen(Factory.Screen): + pass + + +class ScreenSend(EScreen): + pass + + +class ScreenReceive(EScreen): + pass + + +class ScreenContacts(EScreen): + + def add_new_contact(self): + dlg = Cache.get('electrum_widgets', 'NewContactDialog') + if not dlg: + dlg = NewContactDialog() + Cache.append('electrum_widgets', 'NewContactDialog', dlg) + dlg.open() + + +class CSpinner(Factory.Spinner): + '''CustomDropDown that allows fading out the dropdown + ''' + + def _update_dropdown(self, *largs): + dp = self._dropdown + cls = self.option_cls + if isinstance(cls, string_types): + cls = Factory.get(cls) + dp.clear_widgets() + def do_release(option): + Clock.schedule_once(lambda dt: dp.select(option.text), .25) + for value in self.values: + item = cls(text=value) + item.bind(on_release=do_release) + dp.add_widget(item) + + +class TabbedCarousel(Factory.TabbedPanel): + '''Custom TabbedOanel using a carousel used in the Main Screen + ''' + + carousel = ObjectProperty(None) + + def animate_tab_to_center(self, value): + scrlv = self._tab_strip.parent + if not scrlv: + return + + idx = self.tab_list.index(value) + if idx == 0: + scroll_x = 1 + elif idx == len(self.tab_list) - 1: + scroll_x = 0 + else: + 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 = Factory.Animation(scroll_x=scroll_x, 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_leave') + self.switch_to(tab) + carousel.slides[tab.slide].dispatch('on_enter') + except AttributeError: + current_slide.dispatch('on_enter') + + 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) + try: + tab = self.tab_list[-1] + except IndexError: + return + 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_leave') + carousel.load_slide(slide) + slide.dispatch('on_enter') + + def add_widget(self, widget, index=0): + if isinstance(widget, Factory.CScreen): + self.carousel.add_widget(widget) + return + super(TabbedCarousel, self).add_widget(widget, index=index) + + +class ELTextInput(Factory.TextInput): + '''Custom TextInput used in main screens for numeric entry + ''' + + 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/uix/ui_screens/mainscreen.kv b/gui/kivy/uix/ui_screens/mainscreen.kv @@ -0,0 +1,1405 @@ +#:import _ electrum.i18n._ +#:import Cache kivy.cache.Cache +#:import Factory kivy.factory.Factory +#:set font_light 'data/fonts/Roboto-Condensed.ttf' +#:set btc_symbol unichr(171) +#:set mbtc_symbol unichr(187) + +<WalletActionPrevious@ActionPrevious> + app_icon: 'atlas://gui/kivy/theming/light/' + ('wallets' if app.ui_mode[0] != 't' else 'tab_btn') + with_previous: False + size_hint: None, 1 + mipmap: True + on_release: app.root.children[0].toggle_drawer() + +<CloseButton@IconButton> + source: 'atlas://gui/kivy/theming/light/closebutton' + opacity: 1 if self.state == 'normal' else .75 + size_hint: None, None + size: '27dp', '27dp' + +####################### +# Screen Contacts +####################### +<ContactImage@Widget>: + source: 'atlas://gui/kivy/theming/light/contact_avatar' + size_hint_x: None + width: self.height + canvas: + Color: + rgba: 1, 1, 1, 1 + Ellipse: + source: root.source + size: self.width + dp(6), self.height + dp(6) + pos: self.x - dp(3), self.y - dp(3) + Ellipse: + source: 'atlas://gui/kivy/theming/light/contact_overlay' + size: self.width + dp(11), self.height + dp(11) + pos: self.x - dp(5.5), self.y - dp(5.5) + +<ContactLabel@Label> + color: .305, .309, .309, 1 + text_size: self.size + halign: 'left' + valign: 'middle' + +<ContactSeperator@Widget> + canvas.before: + Color: + rgba: .890, .890, .890, 1 + Rectangle: + size: self.size + pos: self.x, self.y + dp(9) + size_hint: None, None + size: '1dp', '22dp' + pos_hint_y: .5 + +<ContactTextInput@TextInput> + background_normal: self.background_down + background_down: 'atlas://gui/kivy/theming/light/tab_btn' + size_hint_y: None + height: '22dp' + +<ContactBitLogo@Image> + source: 'atlas://gui/kivy/theming/light/bit_logo' + size_hint_x: None + width: '32dp' + +<ContactItem@BoxLayout> + address: '' + label: '' + tx_amt: 0 + size_hint_y: None + height: '65dp' + padding: dp(12) + spacing: dp(5) + canvas.before: + Color: + rgba: 1, 1, 1, 1 + Rectangle: + size: self.size + pos: self.pos + ContactImage: + id: contact_image + Widget: + size_hint_x: None + width: '9dp' + ContactLabel: + id: contact_label + text: root.label + ContactSeperator: + ContactBitLogo: + +<ScreenContacts> + name: 'contacts' + on_activate: + if not self.action_view:\ + self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view + BoxLayout: + orientation: 'vertical' + spacing: '1dp' + ContactTextInput: + ScrollView: + canvas.before: + Color: + rgba: .8901, .8901, .8901, 1 + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols: 1 + id: contact_container + size_hint_y: None + height: self.minimum_height + spacing: '1dp' + +<SendActionView@ActionView> + foreground_color: (.466, .466, .466, 1) + color_active: (0.235, .588, .89, 1) + WalletActionPrevious: + id: action_previous + width: but_star.width + ActionButton: + id: action_logo + important: True + size_hint: 1, 1 + markup: True + mipmap: True + bold: True + markup: True + color: 1, 1, 1, 1 + text: + "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ + .format(app.base_unit, app.status) + font_size: '22dp' + minimum_width: '1dp' + Butt_star: + id: but_star + on_release: + if self.state == 'down':\ + app.show_info_bubble(\ + text='[b]Expert mode on[/b]\n you can now select your address',\ + icon='atlas://gui/kivy/theming/light/star_big_inactive',\ + duration=1, arrow_pos='', width='250dp') + +<ScreenSend> + name: 'send' + action_view: Factory.SendActionView() + on_activate: + root.load_screen('screensend') + on_deactivate: + self.content.ids.amount_e.focus = False + self.content.ids.payto_e.focus = False + self.content.ids.message_e.focus = False + +<SendToggle@ToggleButton> + source: '' + group: 'transfer_type' + markup: False + bold: True + border: 4, 4, 4, 4 + background_normal: self.background_down + color: + (.140, .140, .140, 1) if self.state == 'down' else (.796, .796, .796, 1) + canvas.after: + Color: + rgba: 1, 1, 1, 1 + Image: + source: root.source + color: root.color + size: '30dp', '30dp' + center_x: root.center_x - ((root.texture_size[0]/2)+(self.width/1.5)) + center_y: root.center_y + +<BlueSpinner@BoxLayout> + foreground_color: 1, 1, 1, 1 + spacing: '9dp' + text: '' + values: ('', ) + icon: '' + Image: + source: root.icon + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + OppositeSpinner: + color: root.foreground_color + background_normal: 'atlas://gui/kivy/theming/light/action_group_light' + markup: False + shorten: True + font_size: '16dp' + size_hint: 1, .7 + pos_hint: {'center_y': .5} + text: root.text + text_size: self.size + halign: 'left' + valign: 'middle' + on_text: + root.text = args[1] + values: root.values + +<AddressSelector@BlueSpinner> + icon: 'atlas://gui/kivy/theming/light/globe' + values: app.wallet.addresses() + text: _("Select Your address") + +<WalletSelector@BlueSpinner> + icon: 'atlas://gui/kivy/theming/light/wallet' + values: ('default Wallet',) + text: _('Select your wallet') + +<SendReceiveToggle@BoxLayout> + padding: '5dp', '5dp' + size_hint: 1, None + height: '45dp' + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + border: 12, 12, 12, 12 + source: 'atlas://gui/kivy/theming/light/card' + size: self.width + dp(3), self.height + pos: self.x - dp(1.5), self.y + +<SendReceiveCardTop@GridLayout> + canvas.before: + BorderImage: + border: 9, 9, 9, 9 + source: 'atlas://gui/kivy/theming/light/card_top' + size: self.size + pos:self.pos + padding: '12dp', '22dp', '12dp', 0 + cols: 1 + size_hint: 1, None + height: '120dp' + spacing: '4dp' + +<SendReceiveBlueBottom@GridLayout> + canvas.before: + Color: + rgba: .238, .585, .878, 1 + BorderImage: + border: 9, 9, 9, 9 + source: 'atlas://gui/kivy/theming/light/card_bottom' + size: self.size + pos: self.pos + Color: + rgba: 1, 1, 1, 1 + + item_height: dp(42) + foreground_color: .843, .914, .972, 1 + cols: 1 + padding: '12dp', 0 + + +<ScreenReceive> + name: 'receive' + action_view: Factory.ReceiveActionView() + on_activate: + root.load_screen('screenreceive') + on_deactivate: + self.content.ids.amount_e.focus = False + +<ReceiveActionView@ActionView> + WalletActionPrevious: + id: action_previous + width: '32dp' + ActionButton: + id: action_logo + important: True + size_hint: 1, 1 + markup: True + mipmap: True + bold: True + markup: True + color: 1, 1, 1, 1 + text: + "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ + .format(app.base_unit, app.status) + font_size: '22dp' + minimum_width: '1dp' + Butt_star: + id: but_star + on_release: + if self.state == 'down':\ + app.show_info_bubble(\ + text='[b]Expert mode on[/b]\n you can now select your address',\ + icon='atlas://gui/kivy/theming/light/star_big_inactive',\ + duration=1, arrow_pos='', width='250dp') + +############################################### +## 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 + +<StripLayout> + padding: 0, 0, 0, 0 + +<TabbedCarousel> + carousel: carousel + do_default_tab: False + Carousel: + scroll_timeout: 190 + anim_type: 'out_quart' + min_move: .05 + anim_move_duration: .1 + anim_cancel_duration: .54 + scroll_distance: '10dp' + on_index: root.on_index(*args) + id: carousel + +<CScreen> + content: None + state: 'deactivated' + on_enter: self.state = 'loading' + on_activate: + self.state = 'activated' + on_leave: self.state = 'unloading' + on_deactivate: self.state = 'deactivated' + color: 1, 1, 1, 1 + Image: + source: 'atlas://gui/kivy/theming/light/logo_atom_dull' + size_hint: (None, None) + pos_hint: {'center_x': .5, 'y': .02} + allow_stretch: True + size: ('32dp', '32dp') + Label: + id: screen_label + size: root.size + font_size: '45sp' + color: root.color + text: '' if root.state == 'activated' or root.content else 'Loading...' + +<EScreen> + background_color: .929, .929, .929 ,1 + effects: [self.scrollflex] + +<OppositeDropDown@DropDown> + #auto_width: False + size_hint: None, None + size: self.container.minimum_size if self.container else (0, 0) + on_container: if args[1]: self.container.padding = '4dp', '4dp', '4dp', '4dp' + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + pos:self.pos + border: 20, 20, 20, 20 + source: 'atlas://gui/kivy/theming/light/dropdown_background' + size: self.size + +<LightOptions@SpinnerOption> + font_size: '14sp' + border: 4, 4, 4, 4 + color: 0.439, 0.439, 0.439, .8 + background_normal: 'atlas://gui/kivy/theming/light/action_button_group' + background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' + size_hint_y: None + height: '48dp' + text_size: self.size[0] - dp(20), self.size[1] + halign: 'left' + valign: 'middle' + shorten: True + on_press: + ddn = self.parent.parent + Factory.Animation(opacity=0, d=.25).start(ddn) + + +<OppositeSpinner@CSpinner> + dropdown_cls: Factory.OppositeDropDown + option_cls: Factory.LightOptions + border: 20, 20, 9, 9 + background_normal: 'atlas://gui/kivy/theming/light/action_group_dark' + background_down: self.background_normal + values: ('Copy to clipboard', 'Send Payment') + size_hint: None, 1 + width: '12dp' + on_release: + ddn = self._dropdown + ddn.opacity = 0 + Factory.Animation(opacity=1, d=.25).start(ddn) + +<NewContactDialog> + title: _('[size=7dp] \n[/size] Add a Contact[size=7dp]\n[/size]') + title_size: '24sp' + border: 7, 7, 7, 7 + size_hint: None, None + size: '320dp', '235dp' + pos_hint: {'center_y': .53} + separator_color: .89, .89, .89, 1 + separator_height: '1.2dp' + title_color: .437, .437, .437, 1 + background: 'atlas://gui/kivy/theming/light/dialog' + padding: 0, 0 + on_parent: + self.content.padding = 0, 0, 0, 0 + self.content.parent.parent.padding = 2, 12, 2, 2 + self.content.parent.parent.spacing = 0, -2 + BoxLayout: + id: bl + spacing: '1.2dp' + canvas: + Color: + rgba: .901, .901, .901, 1 + Rectangle: + size: self.size + pos: self.pos + ContactButton: + source: 'atlas://gui/kivy/theming/light/qrcode' + text: _('QR Scan') + on_release: root.load_qr_scanner() + ContactButton: + id: but_contact + source: 'atlas://gui/kivy/theming/light/manualentry' + text: 'Manual Enrty' + on_release: + root.new_contact() + FloatLayout: + size_hint: None, None + size: 0, 0 + CloseButton: + id: but_close + top: root.top - dp(5) + right: root.right - dp(5) + on_release: root.dismiss() + + +<DialogButton@Button> + border: 12, 12, 12, 12 + background_normal: 'atlas://gui/kivy/theming/light/dialog' + background_down: '' + background_color: (.94, .94, .94, 1) if self.state == 'normal' else (.191, .496, .742, 1) + + +<ContactButton@DialogButton> + source: '' + color: 0, 0, 0, 0 + item_color: (.211, .211, .211, 1) if root.state == 'normal' else (1, 1, 1, 1) + Image: + id: img + allow_stretch: True + color: root.item_color + mipmap: True + source: root.source + center: (root.center, self.size)[0] + size: root.width/2., root.height/2. + Label: + text: root.text + color: root.item_color + size: img.width, self.texture_size[1] + center: root.center_x, img.y - (self.height/2) + + +<CarouselHeader> + border: 0, 0, 0, 0 + text: repr(self) + color: 0, 0, 0, 0 + background_normal: 'atlas://gui/kivy/theming/light/carousel_selected' + background_down: 'atlas://gui/kivy/theming/light/carousel_deselected' + on_state: + o = .5 if args[1] == 'down' else 1 + anim=Factory.Animation(opacity=o, d=.2).start(self) + +<CarouselIndicator@TabbedCarousel> + tab_pos: 'bottom_mid' + tab_height: '32dp' + tab_width: self.tab_height + background_image: 'atlas://data/images/defaulttheme/action_item' + strip_border: 0, 0, 0, 0 + +<-CarouselDialog> + header_color: '#707070ff' + text_color: 0.701, 0.701, 0.701, 1 + title_size: '13sp' + title: '' + separator_color: 0.89, 0.89, 0.89, 1 + background: 'atlas://gui/kivy/theming/light/tab_btn' + carousel_content: carousel_content + canvas.before: + Color: + rgba: 0, 0, 0, .9 + Rectangle: + size: Window.size + pos: 0, 0 + Color: + rgba: 1, 1, 1, 1 + BorderImage: + border: 12, 12, 12, 12 + source: 'atlas://gui/kivy/theming/light/dialog' + size: root.width, root.height - self.carousel_content.tab_height if self.carousel_content else 0 + pos: root.x, self.y + self.carousel_content.tab_height if self.carousel_content else 10 + BoxLayout: + orientation: 'vertical' + GridLayout: + cols: 1 + size_hint: 1, None + height: self.minimum_height + padding: 0, '7sp' + Label: + font_size: root.title_size + text: u'[color={}]{}[/color]'.format(root.header_color, root.title) + text_size: self.width, None + halign: 'left' + size_hint: 1, None + height: self.texture_size[1] + CardSeparator: + color: root.separator_color + height: root.separator_height + FloatLayout: + size_hint: None, None + size: 0, 0 + CloseButton: + id: but_close + top: root.top - dp(10) + right: root.right - dp(10) + on_release: root.dismiss() + CarouselIndicator: + id: carousel_content + +<WalletAddressesDialog> + wallet_name: 'Default Wallet' + on_parent: if args[1]: self.children[0].padding = '15dp', '6dp', '15dp', 0 + title_color: .437, .437, .437 + title_size: '23sp' + separator_height: '1dp' + size_hint: None, None + width: min(Window.width - dp(27), dp(360)) + height: min(Window.height - dp(70), self.width * 1.5) + title: '[size=5sp] \n[/size]{}[size=9sp]\n'.format(self.wallet_name) + +<ScreenAddress> + name: 'addresses' + BoxLayout: + size_hint: 1, .95 + pos_hint: {'top':1} + orientation: 'vertical' + padding: 0, '2.2dp' + Label: + markup: False + color: 0, 0, 0, .3 + text: 'Your bitcoin address:' + font_size: '15sp' + text_size: self.width, None + halign: 'left' + size_hint: 1, None + height: self.texture_size[1] + shorten: True + BoxLayout: + orientation: 'vertical' + spacing: dp(5) + BoxLayout: + size_hint: 1, None + spacing: dp(5) + height: '27dp' + OppositeSpinner: + id: btn_address + markup: False + color: 0.439, 0.439, 0.439, 1 + font_size: '14sp' + text_size: self.width - dp(22), self.height + shorten: True + halign: 'left' + valign: 'middle' + size_hint: .8, .8 + pos_hint: {'center_y': .5} + on_text: + if args[1]: qr.data = app.encode_uri(root.labels[args[1]]) + FloatLayout + id: fl_paste + size_hint: None, 1 + width: '37dp' + Button: + right: fl_paste.right + y: fl_paste.y + size_hint: None, None + height: '37dp' + width: self.height + border: 2, 2, 2, 2 + background_color: .705, .705, .705, 1.5 if self.state == 'normal' else 2 + background_normal: 'atlas://gui/kivy/theming/light/paste_icon' + background_down: self.background_normal + on_release: + app.copy(root.labels[btn_address.text]) + app.show_info_bubble(\ + text='Copied', width='50dp', arrow_pos='', duration=3,\ + pos=(Window.width/2, root.parent.top + dp(18))) + QRCodeWidget: + id: qr + show_border: False + +<CurrencyLabel@Label> + color: .698, .701, .701, 1 + font_size: '14sp' + text_size: self.width, None + halign: 'left' + +<CurrencySpinner@OppositeSpinner> + size_hint_x: None + width: '60dp' + text_size: self.width, None + font_size: '18sp' + halign: 'left' + color: .698, .701, .701, 1 + +<SelectionDialog> + border: 7, 7, 7, 7 + size_hint: None, None + size: '320dp', '200dp' + pos_hint: {'center_y': .53} + separator_color: .89, .89, .89, 1 + separator_height: '1.1dp' + title_color: .437, .437, .437, 1 + background: 'atlas://gui/kivy/theming/light/dialog' + on_parent: self.content.parent.parent.padding = '12dp', 0, '12dp', '12dp' + RelativeLayout: + orientation: 'vertical' + padding: 0, '6dp', 0, 0 + RelativeLayout: + id: container + BoxLayout + size_hint_y: None + height: '48dp' + pos_hint: {'y':0} + DialogButton: + id: btn_cancel + text: _('Cancel') + markup: False + color: .439, .439, .439, 1 + background_color: + (.235, .588, .882, 1) if self.state[0] == 'd'\ + else (.890, .890, .890, 1) + on_release: + root.dismiss() + Widget: + size_hint_x: None + width: '24dp' + DialogButton: + id: btn_ok + text: _('Ok') + background_color: + (.235, .588, .882, 1) if self.state[0] == 'n'\ + else (.890, .890, .890, 1) + on_release: + root.dispatch('on_release', self) + FloatLayout: + size_hint: None, None + size: 0, 0 + CloseButton: + id: but_close + top: root.height - dp(18) + right: root.width - dp(18) + on_release: root.dismiss() + + +<CurrencySelectionDialog@SelectionDialog> + title: '[size=9dp] \n[/size]Currency Selection[size=7dp]\n[/size]' + title_size: '24sp' + on_activate: + spinner_exchanges.text = app.exchanger.use_exchange + spinner_currencies.text = app.exchanger.currency + on_release: + app.exchanger.currency = spinner_currencies.text + app.exchanger.use_exchange = spinner_exchanges.text + root.dismiss() + BoxLayout + size_hint_y: None + height: '24dp' + spacing: '24dp' + pos_hint: {'top': .95, 'x': 0} + CurrencyLabel: + text: _('Currency') + size_hint_x: .4 + CurrencyLabel: + id: lbl_source + text: _('Exchange Source') + BoxLayout: + spacing: '24dp' + size_hint_y: None + height: '32dp' + pos_hint: {'x': 0, 'top': .8} + CurrencySpinner: + id: spinner_currencies + text: app.exchanger.currency + values: app.currencies + Widget: + CurrencySpinner: + id: spinner_exchanges + text: app.exchanger.use_exchange + pos: lbl_source.x, spinner_currencies.y + width: '140dp' + height: spinner_currencies.height + values: app.exchanger.exchanges + +<RecentActivityDialog> + on_parent: + if args[1]:\ + self.children[0].padding = '15dp', '6dp', '15dp', 0 + is_mine: True + amount: '0.00' + amount_color: '#000000ff' + quote_text: '0' + address: u'' + address_known: unicode(self.address or self.address[1:]) in app.wallet.addressbook + self.labels.keys() + confirmations: 0 + date: '0/0/0' + fee: _('unknown') + labels: {} + status: 'unknown' + separator_height: '1dp' + time: _('00:00') + tx_hash: None + title: + u'[size=9] \n[/size]'\ + u'[size={sz}sp][color={clr}]'.format(sz=25.7 if self.width > dp(300) else 22, clr=root.header_color) + \ + _(u'You ') + (_(u'sent') if self.is_mine else _(u'received')) + u'[/color]'\ + '[color={clr}] [font={fnt}]{smbl}[/font]{amt}[/color][/size]\n'.format(\ + clr=root.amount_color, smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light, amt=root.amount[:9]) + \ + _(u'About ') + root.quote_text + _(u' at transaction time') + \ + u'[size={}dp] \n[/size]'.format(5 if self.width > dp(300) else 1) + size_hint: None, None + width: min(Window.width - dp(27), dp(320)) + height: max(grid.height + dp(120), dp(320)) + font_size: '13sp' + CScreen: + tab: screen_details + GridLayout + id: grid + padding: 0, 0, 0, '12dp' + spacing: '18dp' + cols: 1 + size_hint: 1, None + height: self.minimum_height + pos_hint: {'top':1} + GridLayout: + cols: 1 + size_hint_y: None + height: '39sp' + spacing: '5dp' + CardLabel: + color: root.text_color + font_size: root.font_size + text: _('To') if root.is_mine else _('From') + BoxLayout: + id: bl_address + spacing: '4dp' + size_hint_y: None + height: '32dp' + padding: 0, 0, 0, '9dp' + OppositeSpinner: + id: btn_address + markup: False + color: 0.45, 0.45, 0.45, 1 + font_name: font_light + font_size: '15sp' + text: root.address + shorten: True + size_hint: None, 1 + text_size: self.width - dp(30), self.height + halign: 'left' + width: min(self._label.get_extents(self.text)[0] + dp(32), bl_address.width) + on_release: + self._dropdown.auto_width = False + self._dropdown.width = max(self.width, dp(140)) + on_text: + if args[1] != root.address[1:]:\ + root.dropdown_selected(args[1]);\ + self.text = root.address + GridLayout: + cols: 2 + spacing: '18dp' + size_hint: 1, None + height: self.minimum_height + CardLabel: + color: root.text_color + font_size: root.font_size + text: + _('Date') + '[color={}][size=4dp]\n\n[/size]'\ + '[size=18dp]{}[/size][/color]'.format(\ + root.header_color, root.date) + CardLabel: + color: root.text_color + font_size: root.font_size + text: + _('Status') +\ + '[size=4dp]\n\n[/size]'\ + '[color={}][size=18dp]{}[/size][/color]'.format(\ + '#009900' if root.status == 'Validated' else root.header_color,\ + root.status) + CardLabel: + color: root.text_color + font_size: root.font_size + text: + _('Time') +\ + '[size=4dp]\n\n[/size]'\ + '[color={}][size=18dp]{}[/size][/color]'.format(\ + root.header_color, root.time) + CardLabel: + color: root.text_color + font_size: root.font_size + text: + _('Confirmations') +\ + '[size=4dp]\n\n[/size]'\ + '[color={}][size=18dp]{}'\ + '[/size][/color]'.format(root.header_color, root.confirmations) + GridLayout: + id: fl + size_hint: 1, None + height: self.minimum_height + cols: 1 + BoxLayout: + size_hint: 1, None + height: '48sp' if self.opacity else 0 + opacity: 1 if root.is_mine else 0 + CardLabel + size_hint: 1, 1 + text_size: self.size + valign: 'top' + font_size: root.font_size + color: root.text_color + markup: True + text: + _('Transaction Fees') +\ + '[size=4dp]\n\n[/size]'\ + '[color={}][size=18dp]{}'\ + '[/size][/color]'.format(root.header_color, root.fee) + CardLabel: + size_hint: 1, 1 + text_size: self.size + valign: 'top' + font_size: root.font_size + color: root.text_color + text: _('Wallet') + markup: True + DialogButton: + id: btn_dialog + size_hint: 1, None + height: '48sp' if self.opacity else 0 + opacity: 0 if root.is_mine or root.address_known else 1 + background_color: + (.191, .496, .742, 1) if self.state == 'normal'\ + else (.933, .933, .933, 1) + color: + (1, 1, 1, 1) if self.state == 'normal'\ + else (.218, .218, .218, 1) + disabled: False if self.opacity else True + text: _('Add address to contacts') + on_release: + root.dismiss() + app.save_new_contact(root.address[1:], '') + CScreen: + tab: transaction_id + color: .5, .5, .5, .5 + on_activate: root.activate_screen_transactionid(self) + CScreen: + id: list_inputs + color: .5, .5, .5, .5 + data: '' + tab: inputs + on_enter: root.activate_screen_inputs(self) + CScreen: + id: list_outputs + color: .5, .5, .5, .5 + data: '' + tab: outputs + on_enter: root.activate_screen_outputs(self) + + # define the tab headers here + CarouselHeader: + id: screen_details + slide: 0 + CarouselHeader: + id: transaction_id + slide: 1 + CarouselHeader: + id: inputs + slide: 2 + CarouselHeader: + id: outputs + slide: 3 + +<RecentActivityScrOutputs@GridView> + pos_hint:{'top': 1} + size_hint_y: .9 + headers: [_('Address'), _('Amount')] + widths:[root.width*.63, root.width*.36] + +<RecentActivityScrInputs@GridView> + pos_hint:{'top': 1} + size_hint_y: .9 + headers: [_('Address'), _('Previous output')] + widths:[root.width*.63, root.width*.36] + +<RecentActivityScrTransID@GridLayout> + cols: 1 + padding: '5dp' + spacing: '2dp' + text_color: 1, 1, 1 + tx_hash: None + carousel_content: None + CardLabel: + color: root.text_color + font_size: '13dp' + text: _('Transaction ID :') + ELTextInput: + readonly: True + text: root.tx_hash if root.tx_hash else '' + size_hint_y: None + height: '30dp' + BoxLayout: + padding: 0, '9pt', 0, 0 + orientation: 'vertical' + spacing: '5dp' + DialogButton: + background_color: + (.191, .496, .742, 1) if self.state == 'normal'\ + else (.933, .933, .933, 1) + color: + (1, 1, 1, 1) if self.state == 'normal'\ + else (.218, .218, .218, 1) + text: 'Inputs >>' + size_hint_y: None + height: '38dp' + on_release: + root.carousel_content.carousel.load_next() + DialogButton: + text: 'Outputs >>>' + background_color: + (.191, .496, .742, 1) if self.state == 'normal'\ + else (.933, .933, .933, 1) + color: + (1, 1, 1, 1) if self.state == 'normal'\ + else (.218, .218, .218, 1) + size_hint_y: None + height: '38dp' + on_release: + carousel = root.carousel_content.carousel + carousel.load_slide(carousel.slides[3]) + Widget + + +################################ +## Cards (under Dashboard) +################################ + +<Card@GridLayout> + cols: 1 + padding: '12dp' , '22dp', '12dp' , '12dp' + spacing: '12dp' + size_hint: 1, None + height: max(100, self.minimum_height) + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + border: 18, 18, 18, 18 + source: 'atlas://gui/kivy/theming/light/card' + size: self.size + pos: self.pos + +<CardLabel@Label> + color: 0.45, 0.45, 0.45, 1 + size_hint: 1, None + text: '' + text_size: self.width, None + height: self.texture_size[1] + halign: 'left' + valign: 'top' + +<CardButton@Button> + background_normal: 'atlas://gui/kivy/theming/light/card_btn' + bold: True + font_size: '10sp' + color: 0.699, 0.699, 0.699, 1 + size_hint: None, None + size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) + +<CardSeparator@Widget> + size_hint: 1, None + height: dp(1) + color: .909, .909, .909, 1 + canvas: + Color: + rgba: root.color if root.color else (0, 0, 0, 0) + Rectangle: + size: self.size + pos: self.pos + +<CardItem@ButtonBehavior+GridLayout> + canvas.before: + Color: + rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 + Rectangle + size: self.size + pos: self.x, self.y + dp(5) + cols: 1 + padding: '2dp', '2dp' + spacing: '2dp' + size_hint: 1, None + height: self.minimum_height + +<RecentActivityItem@CardItem> + icon: 'atlas://gui/kivy/theming/light/important' + address:'no address set' + amount: '+0.00' + balance: 'xyz'# balance_after + amount_color: '#DB3627' if float(self.amount) < 0 else '#2EA442' + confirmations: 0 + date: '0/0/0' + quote_text: '.' + + spacing: '9dp' + on_release: + dash = app.root.main_screen.ids.tabs.ids.screen_dashboard + dash.show_tx_details(root) + BoxLayout: + size_hint: 1, None + spacing: '8dp' + height: '32dp' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + orientation: 'vertical' + Widget + CardLabel: + shorten: True + text: root.address + markup: False + text_size: self.size + CardLabel: + color: .699, .699, .699, 1 + text: root.date + font_size: '12sp' + Widget + CardLabel: + halign: 'right' + font_size: '13sp' + size_hint: None, 1 + width: '90sp' + markup: True + font_name: font_light + text: + u'[color={amount_color}]{sign}{symbol}{amount}[/color]\n'\ + u'[color=#B2B3B3][size=12sp]{qt}[/size]'\ + u'[/color]'.format(amount_color=root.amount_color,\ + amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ + symbol=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol) + CardSeparator + +<CardRecentActivity@Card> + BoxLayout: + size_hint: 1, None + height: lbl.height + CardLabel: + id: lbl + text: _('RECENT ACTIVITY') + CardButton: + id: btn_see_all + disabled: True if not self.opacity else False + text: _('SEE ALL') + font_size: '12sp' + on_release: app.update_history_tab(see_all=True) + GridLayout: + id: content + spacing: '7dp' + cols: 1 + size_hint: 1, None + height: self.minimum_height + CardSeparator + +<CardPaymentRequest@Card> + CardLabel: + text: _('PAYMENT REQUEST') + CardSeparator: + +<CardStatusInfo@Card> + padding: '12dp' , '12dp' + status: app.status + quote_text: '' + unconfirmed: '' + cols: 2 + FloatLayout + anchor_x: 'left' + size_hint: 1, None + height: '82dp' + IconButton: + mipmap: True + pos_hint: {'x': 0, 'center_y': .45} + color: .90, .90, .90, 1 + source: 'atlas://gui/kivy/theming/light/qrcode' + size_hint: None, .85 + width: self.height + on_release: + dlg = Cache.get('electrum_widgets', 'WalletAddressesDialog') + + if not dlg:\ + Factory.register('WalletAddressesDialog', module='electrum_gui.kivy.uix.dialogs.carousel_dialog');\ + dlg = Factory.WalletAddressesDialog();\ + Cache.append('electrum_widgets', 'WalletAddressesDialog', dlg) + + dlg.open() + CardLabel: + id: top_label + halign: 'right' + valign: 'top' + bold: True + pos_hint: {'top': 1, 'right': 1} + font_name: font_light + balance_in_numbers: bool(ord(root.status[0]) not in range(ord('A'), ord('z'))) + font_size: '50sp' if self.balance_in_numbers else '30sp' + text_size: self.width, root.height/2 + text: + u'[color=#4E4F4F]{}{}[/color]'\ + .format('' if not self.balance_in_numbers else\ + (btc_symbol if app.base_unit == 'BTC' else mbtc_symbol), root.status) + BoxLayout + pos_hint: {'y': 0, 'right': 1} + spacing: '5dp' + CardLabel + halign: 'right' + markup: True + font_size: '22dp' + font_name: font_light + text: u'[color=#c3c3c3]{}[/color]'.format(root.quote_text) + IconButton + color: .698, .698, .698, 1 + source: 'atlas://gui/kivy/theming/light/gear' + size_hint_y: None + height: '28dp' + opacity: .5 if self.state == 'down' else 1 + on_release: + dlg = Cache.get('electrum_widgets', 'CurrencySelectionDialog') + + if not dlg:\ + Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ + dlg = Factory.CurrencySelectionDialog();\ + Cache.append('electrum_widgets', 'CurrencySelectionDialog', dlg) + + dlg.open() + +<ActionDropDown> + auto_width: False + on_size: if not self.auto_width: self.width = dp(190) + on_container: self.container.padding = '4dp', '4dp', '4dp', '4dp' + on_parent: + if args[1]:\ + self.opacity = 0;\ + anim = Factory.Animation(opacity=1, d=.25);\ + anim.start(self) + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + pos:self.pos + border: 20, 20, 20, 20 + source: 'atlas://gui/kivy/theming/light/overflow_background' + size: self.size + +<ActionItem> + color: 0.235, 0.239, 0.239, 1 + +<ActionButton>: + border: 4, 0, 0, 0 + background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' + +<OverflowButton@ActionButton> + text_size: dp(172), None + last: False + halign: 'left' + valign: 'middle' + overflow: None + background_normal: + 'atlas://gui/kivy/theming/light/' +\ + ('action_button_group'\ + if (self.inside_group and not self.last) else 'tab_btn') + on_press: + ddn = self.overflow._dropdown + Factory.Animation.cancel_all(ddn) + anim = Factory.Animation(opacity=0, d=.25) + anim.bind(on_complete=ddn.dismiss) + anim.start(ddn) + +<DashboardActionView@ActionView> + WalletActionPrevious: + id: action_previous + size_hint_x: None + width: action_preferences.width + action_contact.width + ActionButton: + id: action_logo + important: True + pos_hint: {'center_y': .5} + size_hint: 1, .93 + bold: True + icon: 'atlas://gui/kivy/theming/light/logo' + background_down: self.background_normal + minimum_width: '1dp' + ActionButton: + id: action_contact + important: True + width: '25dp' + icon: 'atlas://gui/kivy/theming/light/add_contact' + text: 'Add Contact' + on_release: + Factory.register('NewContactDialog', module='electrum_gui.kivy.uix.dialogs.new_contact') + dlg = Factory.NewContactDialog().open() + + ActionOverflow: + id: action_preferences + background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' + border: 0, 0, 0, 0 + overflow_image: 'atlas://gui/kivy/theming/light/settings' + width: '40dp' + size_hint_x: None + canvas.after: + Color: + rgba: 1, 1, 1, 1 + OverflowButton: + text: _('Settings') + overflow: action_preferences + OverflowButton: + text: _('Help') + last: True + overflow: action_preferences + +<ScreenDashboard> + name: 'dashboard' + action_view: Factory.DashboardActionView() + content: content + ScrollView: + id: content + do_scroll_x: False + GridLayout + id: grid + cols: 1 #if root.width < root.height else 2 + size_hint: 1, None + height: self.minimum_height + padding: '12dp' + spacing: '12dp' + GridLayout: + cols: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '12dp' + orientation: 'vertical' + CardStatusInfo: + id: status_card + CardPaymentRequest: + id: payment_card + CardRecentActivity: + id: recent_activity_card + + +<CleanHeader@TabbedPanelHeader> + border: 0, 0, 4, 0 + markup: False + color: (.188, 0.505, 0.854, 1) if self.state == 'down' else (0.636, 0.636, 0.636, 1) + text_size: self.size + halign: 'center' + valign: 'middle' + bold: True + font_size: '12.5sp' + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_disabled_normal: 'atlas://gui/kivy/theming/light/tab_btn_disabled' + background_down: 'atlas://gui/kivy/theming/light/tab_btn_pressed' + canvas.before: + Color: + rgba: 1, 1, 1, .7 + Rectangle: + size: self.size + pos: self.x + 1, self.y - 1 + texture: self.texture + +<ScreenTabs@Screen> + TabbedCarousel: + id: panel + tab_height: '48dp' + background_image: 'atlas://gui/kivy/theming/light/tab' + strip_image: 'atlas://gui/kivy/theming/light/tab_strip' + strip_border: dp(4), 0, dp(2), 0 + ScreenDashboard: + id: screen_dashboard + tab: tab_dashboard + ScreenSend: + id: screen_send + tab: tab_send + ScreenReceive: + id: screen_receive + tab: tab_receive + ScreenContacts: + id: screen_contacts + tab: tab_contacts + CleanHeader: + id: tab_dashboard + text: _('DASHBOARD') + slide: 0 + CleanHeader: + id: tab_send + text: _('SEND') + slide: 1 + CleanHeader: + id: tab_receive + text: _('RECEIVE') + slide: 2 + CleanHeader: + id: tab_contacts + text: _('CONTACTS') + slide: 3 + +<MainScreen> + name: 'main_screen' + canvas.before: + Color: + rgba: 0.917, 0.917, 0.917, 1 + Rectangle: + size: self.size + pos: self.pos + BoxLayout: + orientation: 'vertical' + ActionBar: + id: action_bar + size_hint: 1, None + height: '48dp' + border: 4, 4, 4, 4 + background_image: 'atlas://gui/kivy/theming/light/action_bar' + ScreenManager: + id: manager + ScreenTabs: + id: tabs + name: "tabs" + #ScreenPassword: + # id: password + # name: 'password + +<Drawer> + overlay_widget: overlay_widget + RelativeLayout: + id: hidden_widget + size_hint: None, None + 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 + canvas.before: + Color: + rgba: .176, .176, .176, 1 + Rectangle: + size: self.size + pos: self.pos + Color + rgba: 1, 1, 1, 1 + BorderImage + border: 0, 32, 0, 0 + source: 'atlas://gui/kivy/theming/light/shadow_right' + size: root.overlay_widget.x if root.overlay_widget else self.width, self.height + RelativeLayout: + id: overlay_widget + size_hint: None, None + x: hidden_widget.width if app.ui_mode[0] == 't' else 0 + size_hint: None, None + width: (root.width - self.x) if app.ui_mode[0] == 't' else root.width + height: root.height + +####################################################### +## This is our child of the root widget of the app +####################################################### +Drawer + size_hint: None, None + size: Window.size + WalletManagement + id: wallet_management + ScreenManager: + # Screen manager for screens meant to change everything including + # ActionBar, currently we only have the main screen here. + id: manager + MainScreen+ \ No newline at end of file diff --git a/gui/kivy/uix/ui_screens/screenreceive.kv b/gui/kivy/uix/ui_screens/screenreceive.kv @@ -0,0 +1,138 @@ +#:import Decimal decimal.Decimal + +<ScreenReceiveContent@BoxLayout> + opacity: 0 + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + mode: 'qr' + orientation: 'vertical' + on_parent: + if args[1]:\ + first_address = app.wallet.addresses()[0];\ + qr.data = app.encode_uri(first_address,\ + amount=amount_e.text,\ + label=app.wallet.labels.get(first_address, first_address),\ + message='') if app.wallet.addresses() else '' + SendReceiveToggle + SendToggle: + id: toggle_qr + text: 'QR' + state: 'down' if root.mode == 'qr' else 'normal' + source: 'atlas://gui/kivy/theming/light/qrcode' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'qr': root.mode = 'nr' + root.mode = 'qr' + SendToggle: + id: toggle_nfc + text: 'NFC' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'nr' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + #size_hint: 1, None + #height: self.minimum_height + SendReceiveCardTop + height: '110dp' + BoxLayout: + size_hint: 1, None + height: '42dp' + rows: 1 + Label: + color: amount_e.foreground_color + bold: True + text_size: self.size + valign: 'bottom' + font_size: '22sp' + text: + u'[font={fnt}]{smbl}[/font]'.\ + format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + CardSeparator + BoxLayout: + size_hint: 1, None + height: '32dp' + spacing: '5dp' + Label: + color: lbl_quote.color + font_size: '12dp' + text: 'Ask to scan the QR below' + text_size: self.size + halign: 'left' + valign: 'middle' + Label: + id: lbl_quote + font_size: '12dp' + size_hint: .5, 1 + color: .761, .761, .761, 1 + text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' + text_size: self.size + halign: 'right' + valign: 'middle' + SendReceiveBlueBottom + id: blue_bottom + padding: '12dp', 0, '12dp', '12dp' + WalletSelector: + id: wallet_selection + foreground_color: blue_bottom.foreground_color + size_hint: 1, None + height: blue_bottom.item_height + CardSeparator + opacity: wallet_selection.opacity + color: blue_bottom.foreground_color + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + on_text: + if not args[1].startswith('Select'):\ + qr.data = app.encode_uri(args[1],\ + amount=amount_e.text,\ + label=app.wallet.labels.get(args[1], args[1]),\ + message='') + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + Widget: + size_hint_y: None + height: dp(10) + FloatLayout + id: bl + QRCodeWidget: + id: qr + size_hint: None, 1 + width: min(self.height, bl.width) + pos_hint: {'center': (.5, .5)} + on_touch_down: + if self.collide_point(*args[1].pos):\ + app.show_info_bubble(icon=self.ids.qrimage.texture, text='texture') + CreateAccountButtonGreen: + background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Goto next step') if app.wallet.seed else _('Create unsigned transaction') + size_hint_y: None + height: '38dp' + disabled: True if wallet_selection.opacity == 0 else False + on_release: + message = 'sending {} {} to {}'.format(\ + app.base_unit, amount_e.text, payto_e.text) + app.gui.main_gui.do_send(self, message=message)+ \ No newline at end of file diff --git a/gui/kivy/uix/ui_screens/screensend.kv b/gui/kivy/uix/ui_screens/screensend.kv @@ -0,0 +1,232 @@ +#:import Decimal decimal.Decimal + +<TextInputSendBlue@TextInput> + padding: '5dp' + size_hint: 1, None + height: '27dp' + pos_hint: {'center_y':.5} + multiline: False + hint_text_color: self.foreground_color + foreground_color: .843, .914, .972, 1 + background_color: 1, 1, 1, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_active: 'atlas://gui/kivy/theming/light/textinput_active' + +<TransactionFeeDialog@SelectionDialog> + return_obj: None + min_fee: app.format_amount(app.wallet.fee) + title: + '[size=9dp] \n[/size]Transaction Fee[size=9dp]\n'\ + '[color=#ADAEAE]Minimum is BTC {}[/color][/size]'.format(self.min_fee) + title_size: '24sp' + on_activate: + ti_fee.focus = True + if self.return_obj:\ + ti_fee.text = "BTC " + self.return_obj.amt + on_deactivate: ti_fee.focus = False + on_release: + if self.return_obj and ti_fee.text:\ + txt = ti_fee.text;\ + spc = txt.rfind(' ') + 1;\ + txt = '' if spc == 0 else txt[spc:];\ + num = 0 if not txt else float(txt);\ + self.return_obj.amt = max(self.min_fee, txt) + root.dismiss() + ELTextInput + id: ti_fee + size_hint: 1, None + height: '34dp' + multiline: False + on_text_validate: root.dispatch('on_release', self) + pos_hint: {'center_y': .7} + text: "BTC " + root.min_fee + input_type: 'number' + +<ScreenSendContent@BoxLayout> + opacity: 0 + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + orientation: 'vertical' + mode: 'address' + SendReceiveToggle: + SendToggle: + id: toggle_address + text: 'ADDRESS' + group: 'send_type' + state: 'down' if root.mode == 'address' else 'normal' + source: 'atlas://gui/kivy/theming/light/globe' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'address': root.mode = 'fc' + root.mode = 'address' + SendToggle: + id: toggle_nfc + text: 'NFC' + group: 'send_type' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'str' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + size_hint: 1, None + height: self.minimum_height + SendReceiveCardTop + id: card_address + BoxLayout + size_hint: 1, None + height: '42dp' + rows: 1 + Label + id: lbl_symbl + bold: True + color: amount_e.foreground_color + text_size: self.size + valign: 'bottom' + halign: 'left' + font_size: '22sp' + text: + u'[font={fnt}]{smbl}[/font]'.\ + format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + on_text_validate: payto_e.focus = True + CardSeparator + BoxLayout: + size_hint: 1, None + height: '42dp' + spacing: '5dp' + Label: + id: fee_e + color: .761, .761, .761, 1 + font_size: '12dp' + amt: app.format_amount(app.wallet.fee) + text: + u'[b]{sign}{symbl}{amt}[/b] of fee'.\ + format(symbl=lbl_symbl.text,\ + sign='+' if self.amt > 0 else '-', amt=self.amt) + size_hint_x: None + width: self.texture_size[0] + halign: 'left' + valign: 'middle' + IconButton: + color: 0.694, 0.694, 0.694, 1 + source: 'atlas://gui/kivy/theming/light/gear' + pos_hint: {'center_y': .5} + size_hint: None, None + size: '22dp', '22dp' + on_release: + dlg = Cache.get('electrum_widgets', 'TransactionFeeDialog') + + if not dlg:\ + Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ + dlg = Factory.TransactionFeeDialog();\ + Cache.append('electrum_widgets', 'TransactionDialog', dlg) + + dlg.return_obj = fee_e + dlg.open() + Label: + font_size: '12dp' + color: fee_e.color + text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' + text_size: self.size + halign: 'right' + valign: 'middle' + SendReceiveBlueBottom: + id: blue_bottom + size_hint: 1, None + height: self.minimum_height + BoxLayout + size_hint: 1, None + height: blue_bottom.item_height + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/contact' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: payto_e + hint_text: "Enter Contact or adress" + on_text_validate: + Factory.Animation(opacity=1,\ + height=blue_bottom.item_height)\ + .start(message_selection) + message_e.focus = True + Widget: + size_hint: None, None + width: dp(2) + height: qr.height + pos_hint: {'center_y':.5} + canvas.after: + Rectangle: + size: self.size + pos: self.pos + IconButton: + id: qr + source: 'atlas://gui/kivy/theming/light/qrcode' + pos_hint: {'center_y': .5} + size_hint: None, None + size: '22dp', '22dp' + CardSeparator + opacity: message_selection.opacity + color: blue_bottom.foreground_color + BoxLayout: + id: message_selection + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/pen' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: message_e + hint_text: 'Enter description here' + on_text_validate: + anim = Factory.Animation(opacity=1, height=blue_bottom.item_height) + anim.start(wallet_selection) + #anim.start(address_selection) + CardSeparator + opacity: wallet_selection.opacity + color: blue_bottom.foreground_color + WalletSelector: + id: wallet_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + CreateAccountButtonGreen: + background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Goto next step') if app.wallet.seed else _('Create unsigned transaction') + size_hint_y: None + height: '38dp' + disabled: True if wallet_selection.opacity == 0 else False + on_release: app.do_send() + Widget diff --git a/gui/kivy/utils.py b/gui/kivy/utils.py @@ -1,2 +0,0 @@ - - diff --git a/lib/android/libiconv.so b/lib/android/libiconv.so Binary files differ. diff --git a/lib/android/libzbarjni.so b/lib/android/libzbarjni.so Binary files differ. diff --git a/lib/android/zbar.jar b/lib/android/zbar.jar Binary files differ.