treorganize files and bring code inline with current master - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit c121c1aa4e2d20825d4eafc201144e84df3129dd DIR parent 9938316400ad263ab9bec40e93f7e0b10c2ed9cd HTML 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: M data/fonts/Roboto-Bold.ttf | 0 A data/fonts/Roboto-Condensed.ttf | 0 A data/fonts/Roboto-Medium.ttf | 0 M gui/kivy/Makefile | 2 +- M gui/kivy/__init__.py | 54 +++++++++++++++++++++++++++---- D gui/kivy/carousel.py | 33 ------------------------------- D gui/kivy/dialog.py | 686 ------------------------------- D gui/kivy/drawer.py | 188 ------------------------------- D gui/kivy/gridview.py | 203 ------------------------------- D gui/kivy/installwizard.py | 328 ------------------------------- M gui/kivy/main.kv | 439 ++++--------------------------- M gui/kivy/main_window.py | 1023 ++++++++++++++++++++++--------- M gui/kivy/plugins/exchange_rate.py | 97 +++++++++++++++++++++---------- M gui/kivy/qr_scanner/__init__.py | 54 +++---------------------------- M gui/kivy/qr_scanner/scanner_androi… | 9 +++++++-- D gui/kivy/qrcodewidget.py | 179 ------------------------------- D gui/kivy/screens.py | 105 ------------------------------- D gui/kivy/statusbar.py | 7 ------- D gui/kivy/textinput.py | 14 -------------- M gui/kivy/theming/light-0.png | 0 M gui/kivy/theming/light.atlas | 4 ++-- M gui/kivy/theming/light/action_bar.… | 0 A gui/kivy/theming/light/action_butt… | 0 A gui/kivy/theming/light/action_grou… | 0 A gui/kivy/theming/light/bit_logo.png | 0 M gui/kivy/theming/light/card.png | 0 M gui/kivy/theming/light/card_top.png | 0 M gui/kivy/theming/light/contact.png | 0 A gui/kivy/theming/light/contact_ava… | 0 A gui/kivy/theming/light/contact_ove… | 0 A gui/kivy/theming/light/dropdown_ba… | 0 M gui/kivy/theming/light/gear.png | 0 M gui/kivy/theming/light/logo.png | 0 M gui/kivy/theming/light/manualentry… | 0 M gui/kivy/theming/light/nfc_phone.p… | 0 A gui/kivy/theming/light/overflow_ba… | 0 A gui/kivy/theming/light/overflow_bt… | 0 M gui/kivy/theming/light/qrcode.png | 0 M gui/kivy/theming/light/settings.png | 0 M gui/kivy/theming/light/tab_btn_pre… | 0 M gui/kivy/theming/light/tab_strip.p… | 0 M gui/kivy/theming/light/wallets.png | 0 A gui/kivy/tools/blacklist.txt | 99 +++++++++++++++++++++++++++++++ A gui/kivy/tools/buildozer.spec | 172 ++++++++++++++++++++++++++++++ D gui/kivy/ui_screens/mainscreen.kv | 287 ------------------------------- A gui/kivy/ui_screens/screenreceive.… | 130 +++++++++++++++++++++++++++++++ A gui/kivy/ui_screens/screensend.kv | 187 +++++++++++++++++++++++++++++++ A gui/kivy/uix/__init__.py | 1 + R gui/kivy/combobox.py -> gui/kivy/u… | 0 R gui/kivy/console.py -> gui/kivy/ui… | 0 A gui/kivy/uix/dialogs/__init__.py | 190 ++++++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/carousel_dial… | 239 +++++++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/create_restor… | 488 +++++++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/installwizard… | 478 +++++++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/new_contact.py | 26 ++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/nfc_transacti… | 33 +++++++++++++++++++++++++++++++ A gui/kivy/uix/dialogs/qr_scanner.py | 42 +++++++++++++++++++++++++++++++ A gui/kivy/uix/drawer.py | 258 +++++++++++++++++++++++++++++++ A gui/kivy/uix/gridview.py | 205 ++++++++++++++++++++++++++++++ R gui/kivy/menus.py -> gui/kivy/uix/… | 0 A gui/kivy/uix/qrcodewidget.py | 180 +++++++++++++++++++++++++++++++ A gui/kivy/uix/screens.py | 300 +++++++++++++++++++++++++++++++ A gui/kivy/uix/ui_screens/mainscreen… | 1406 +++++++++++++++++++++++++++++++ A gui/kivy/uix/ui_screens/screenrece… | 139 ++++++++++++++++++++++++++++++ A gui/kivy/uix/ui_screens/screensend… | 232 ++++++++++++++++++++++++++++++ D gui/kivy/utils.py | 2 -- A lib/android/libiconv.so | 0 A lib/android/libzbarjni.so | 0 A lib/android/zbar.jar | 0 69 files changed, 5738 insertions(+), 2781 deletions(-) --- DIR diff --git a/data/fonts/Roboto-Bold.ttf b/data/fonts/Roboto-Bold.ttf Binary files differ. DIR diff --git a/data/fonts/Roboto-Condensed.ttf b/data/fonts/Roboto-Condensed.ttf Binary files differ. DIR diff --git a/data/fonts/Roboto-Medium.ttf b/data/fonts/Roboto-Medium.ttf Binary files differ. DIR diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile t@@ -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 \ DIR diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py t@@ -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 t@@ -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 t@@ -60,7 +59,6 @@ from electrum.plugins import init_plugins # return True # return False - class ElectrumGui: def __init__(self, config, network, app=None): t@@ -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 t@@ -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 DIR diff --git a/gui/kivy/carousel.py b/gui/kivy/carousel.py t@@ -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 DIR diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py t@@ -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") DIR diff --git a/gui/kivy/drawer.py b/gui/kivy/drawer.py t@@ -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 DIR diff --git a/gui/kivy/gridview.py b/gui/kivy/gridview.py t@@ -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() DIR diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py t@@ -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 DIR diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv t@@ -1,7 +1,4 @@ #:import Window kivy.core.window.Window -#:import _ electrum.i18n._ -#:import partial functools.partial - # Custom Global Widgets t@@ -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: t@@ -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' t@@ -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 DIR diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py t@@ -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) t@@ -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')) t@@ -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): t@@ -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', t@@ -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) t@@ -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: t@@ -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) t@@ -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. t@@ -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) t@@ -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 t@@ -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") t@@ -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) t@@ -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): t@@ -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] t@@ -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 t@@ -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: t@@ -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 t@@ -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: t@@ -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): ''' t@@ -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: t@@ -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:: t@@ -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) t@@ -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 t@@ -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 DIR diff --git a/gui/kivy/plugins/exchange_rate.py b/gui/kivy/plugins/exchange_rate.py t@@ -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", t@@ -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 t@@ -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',)) t@@ -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: t@@ -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, t@@ -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) t@@ -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) DIR diff --git a/gui/kivy/qr_scanner/__init__.py b/gui/kivy/qr_scanner/__init__.py t@@ -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']) DIR diff --git a/gui/kivy/qr_scanner/scanner_android.py b/gui/kivy/qr_scanner/scanner_android.py t@@ -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) t@@ -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 t@@ -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) t@@ -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 t@@ -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. DIR diff --git a/gui/kivy/qrcodewidget.py b/gui/kivy/qrcodewidget.py t@@ -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)) - - DIR diff --git a/gui/kivy/screens.py b/gui/kivy/screens.py t@@ -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() DIR diff --git a/gui/kivy/statusbar.py b/gui/kivy/statusbar.py t@@ -1,7 +0,0 @@ -from kivy.uix.boxlayout import BoxLayout -from kivy.properties import StringProperty - - -class StatusBar(BoxLayout): - - text = StringProperty('') DIR diff --git a/gui/kivy/textinput.py b/gui/kivy/textinput.py t@@ -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) DIR diff --git a/gui/kivy/theming/light-0.png b/gui/kivy/theming/light-0.png Binary files differ. DIR diff --git a/gui/kivy/theming/light.atlas b/gui/kivy/theming/light.atlas t@@ -1 +1 @@ parazyd.org:70 /git/electrum/commit/c121c1aa4e2d20825d4eafc201144e84df3129dd.gph:4082: line too long