From 59e3058de27cd2148d27e7fdbe51d036dde66fde Mon Sep 17 00:00:00 2001 From: zebra-lucky Date: Tue, 5 Jun 2018 15:07:49 +0300 Subject: [PATCH] add gui/kivy changes from electrum 3.1.3 - Add electrum 3.1.3 changes to gui/kivy - Get new electrum 3.1.3 gui/kivy/Readme.md - Get new electrum 3.1.3 gui/kivy/uix/dialogs/requests.py. - Get new electrum 3.1.3 gui/kivy/uix/dialogs/invoices.py. - Get new electrum 3.1.3 gui/kivy/uix/dialogs/addresses.py --- gui/kivy/Readme.md | 80 ++++++++ gui/kivy/Readme.txt | 24 --- gui/kivy/__init__.py | 2 +- gui/kivy/i18n.py | 7 +- gui/kivy/main.kv | 29 +-- gui/kivy/main_window.py | 171 +++++++++------- gui/kivy/nfc_scanner/__init__.py | 4 +- gui/kivy/nfc_scanner/scanner_android.py | 12 +- gui/kivy/theming/light/share.png | Bin 0 -> 3325 bytes gui/kivy/tools/buildozer.spec | 3 + gui/kivy/uix/context_menu.py | 1 - gui/kivy/uix/dialogs/__init__.py | 2 +- gui/kivy/uix/dialogs/addresses.py | 183 ++++++++++++++++++ gui/kivy/uix/dialogs/amount_dialog.py | 49 +++-- gui/kivy/uix/dialogs/fee_dialog.py | 105 +++++----- gui/kivy/uix/dialogs/fx_dialog.py | 2 + gui/kivy/uix/dialogs/installwizard.py | 40 ++-- gui/kivy/uix/dialogs/invoices.py | 169 ++++++++++++++++ gui/kivy/uix/dialogs/password_dialog.py | 95 ++++++--- gui/kivy/uix/dialogs/requests.py | 157 +++++++++++++++ gui/kivy/uix/dialogs/settings.py | 38 +--- gui/kivy/uix/dialogs/tx_dialog.py | 8 +- gui/kivy/uix/menus.py | 2 +- gui/kivy/uix/screens.py | 247 +++--------------------- gui/kivy/uix/ui_screens/history.kv | 63 +++--- gui/kivy/uix/ui_screens/invoices.kv | 66 ------- gui/kivy/uix/ui_screens/network.kv | 4 +- gui/kivy/uix/ui_screens/receive.kv | 23 ++- gui/kivy/uix/ui_screens/requests.kv | 66 ------- gui/kivy/uix/ui_screens/send.kv | 45 +++-- 30 files changed, 1030 insertions(+), 667 deletions(-) create mode 100644 gui/kivy/Readme.md delete mode 100644 gui/kivy/Readme.txt create mode 100644 gui/kivy/theming/light/share.png create mode 100644 gui/kivy/uix/dialogs/addresses.py create mode 100644 gui/kivy/uix/dialogs/invoices.py create mode 100644 gui/kivy/uix/dialogs/requests.py delete mode 100644 gui/kivy/uix/ui_screens/invoices.kv delete mode 100644 gui/kivy/uix/ui_screens/requests.kv diff --git a/gui/kivy/Readme.md b/gui/kivy/Readme.md new file mode 100644 index 00000000..72a8fcb8 --- /dev/null +++ b/gui/kivy/Readme.md @@ -0,0 +1,80 @@ +# Kivy GUI + +The Kivy GUI is used with Electrum-Zcash on Android devices. To generate an APK file, follow these instructions. + +## 1. Install python-for-android (p4a) +p4a is used to package Electrum-Zcash, Python, SDL and a bootstrap Java app into an APK file. +We patched p4a to add some functionality we need for Electrum-Zcash. Until those changes are +merged into p4a, you need to merge them locally (into the master branch): + +1.1 [kivy/python-for-android#1217](https://github.com/kivy/python-for-android/pull/1217) + +Something like this should work: + +```sh +cd /opt +git clone https://github.com/kivy/python-for-android +cd python-for-android +git remote add agilewalker https://github.com/agilewalker/python-for-android +git fetch --all +git checkout 93759f36ba45c7bbe0456a4b3e6788622924cbac +git merge a2fb5ecbc09c4847adbcfd03c6b1ca62b3d09b8d +``` + +## 2. Install buildozer +2.1 Buildozer is a frontend to p4a. Luckily we don't need to patch it: + +```sh +cd /opt +git clone https://github.com/kivy/buildozer +cd buildozer +sudo python3 setup.py install +``` + +2.2 Download the [Crystax NDK](https://www.crystax.net/en/download) manually. +Extract into `/opt/crystax-ndk-10.3.2` + +## 3. Update the Android SDK build tools + +### Method 1: Using the GUI + + Start the Android SDK manager in GUI mode: + + ~/.buildozer/android/platform/android-sdk-20/tools/android + + Check the latest SDK available and install it. + Close the SDK manager. + Reopen the SDK manager, scroll to the bottom and install the latest build tools (probably v27) + Install "Android Support Library Repository" from the SDK manager. + +### Method 2: Using the command line: + + Repeat the following command until there is nothing to install: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t tools,platform-tools + + Install Build Tools, android API 19 and Android Support Library: + + ~/.buildozer/android/platform/android-sdk-20/tools/android update sdk -u -t build-tools-27.0.3,android-19,extra-android-m2repository + + + +## 5. Create the UI Atlas +In the `gui/kivy` directory of Electrum-Zcash, run `make theming`. + +## 6. Download Electrum-Zcash dependencies +Run `contrib/make_packages`. + +## 7. Build the APK +Run `contrib/make_apk`. + +# FAQ +## Why do I get errors like `package me.dm7.barcodescanner.zxing does not exist` while compiling? +Update your Android build tools to version 27 like described above. + +## Why do I get errors like `(use -source 7 or higher to enable multi-catch statement)` while compiling? +Make sure that your p4a installation includes commit a3cc78a6d1a107cd3b6bd28db8b80f89e3ecddd2. +Also make sure you have recent SDK tools and platform-tools + +## I changed something but I don't see any differences on the phone. What did I do wrong? +You probably need to clear the cache: `rm -rf .buildozer/android/platform/build/{build,dists}` diff --git a/gui/kivy/Readme.txt b/gui/kivy/Readme.txt deleted file mode 100644 index ce85523c..00000000 --- a/gui/kivy/Readme.txt +++ /dev/null @@ -1,24 +0,0 @@ -Before compiling, create packages: `contrib/make_packages` - -Commands:: - - `make theming` to make a atlas out of a list of pngs - - `make apk` to make a apk - - -If something in included modules like kivy or any other module changes -then you need to rebuild the distribution. To do so: - - rm -rf .buildozer/android/platform/python-for-android/dist - - -how to build with ssl: - - rm -rf .buildozer/android/platform/build/ - ./contrib/make_apk - pushd /opt/electrum/.buildozer/android/platform/build/build/libs_collections/Electrum/armeabi-v7a - cp libssl1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libssl.so - cp libcrypto1.0.2g.so /opt/crystax-ndk-10.3.2/sources/openssl/1.0.2g/libs/armeabi-v7a/libcrypto.so - popd - ./contrib/make_apk diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py index 49d56d9a..fd9e6110 100644 --- a/gui/kivy/__init__.py +++ b/gui/kivy/__init__.py @@ -32,7 +32,7 @@ try: sys.argv = [''] import kivy except ImportError: - # This error ideally shouldn't raised with pre-built packages + # This error ideally shouldn't be raised with pre-built packages sys.exit("Error: Could not import kivy. Please install it using the" + \ "instructions mentioned here `http://kivy.org/#download` .") diff --git a/gui/kivy/i18n.py b/gui/kivy/i18n.py index 4721247a..a1ea53ed 100644 --- a/gui/kivy/i18n.py +++ b/gui/kivy/i18n.py @@ -1,21 +1,22 @@ import gettext + class _(str): observers = set() lang = None - def __new__(cls, s, *args, **kwargs): + def __new__(cls, s): if _.lang is None: _.switch_lang('en') - t = _.translate(s, *args, **kwargs) + t = _.translate(s) o = super(_, cls).__new__(cls, t) o.source_text = s return o @staticmethod def translate(s, *args, **kwargs): - return _.lang(s).format(args, kwargs) + return _.lang(s) @staticmethod def bind(label): diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index b3c2e7e8..6df7be0e 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -239,7 +239,7 @@ self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() canvas.before: Color: - rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1) + rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.15, 0.15, 0.17, 1) Rectangle: size: self.size pos: self.pos @@ -285,7 +285,8 @@ : size_hint: 1, None - height: '48dp' + height: '60dp' + font_size: '30dp' on_release: self.parent.update_amount(self.text) @@ -372,9 +373,6 @@ tab_height: '48dp' tab_width: panel.width/3 strip_border: 0, 0, 0, 0 - InvoicesScreen: - id: invoices_screen - tab: invoices_tab SendScreen: id: send_screen tab: send_tab @@ -384,34 +382,23 @@ ReceiveScreen: id: receive_screen tab: receive_tab - AddressScreen: - id: address_screen - tab: address_tab - CleanHeader: - id: invoices_tab - text: _('Invoices') - slide: 0 CleanHeader: id: send_tab text: _('Send') - slide: 1 + slide: 0 CleanHeader: id: history_tab - text: _('History') - slide: 2 + text: _('Balance') + slide: 1 CleanHeader: id: receive_tab text: _('Receive') - slide: 3 - CleanHeader: - id: address_tab - text: _('Addresses') - slide: 4 + slide: 2 #on_release: - # fixme: the following line was commented out because it does no seem to do what it is intended + # fixme: the following line was commented out because it does not seem to do what it is intended # Clock.schedule_once(lambda dt: self.parent.parent.dismiss() if self.parent else None, 0.05) on_press: Clock.schedule_once(lambda dt: app.popup_dialog(self.name), 0.05) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 38ee1ffe..d93365d9 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -82,6 +82,10 @@ class ElectrumWindow(App): server_port = StringProperty('') num_chains = NumericProperty(0) blockchain_name = StringProperty('') + fee_status = StringProperty('Fee') + balance = StringProperty('') + fiat_balance = StringProperty('') + is_fiat = BooleanProperty(False) blockchain_checkpoint = NumericProperty(0) auto_connect = BooleanProperty(False) @@ -95,8 +99,8 @@ class ElectrumWindow(App): from .uix.dialogs.choice_dialog import ChoiceDialog protocol = 's' def cb2(host): - from electrum_zcash.bitcoin import NetworkConstants - pp = servers.get(host, NetworkConstants.DEFAULT_PORTS) + from electrum_zcash import constants + pp = servers.get(host, constants.net.DEFAULT_PORTS) port = pp.get(protocol, '') popup.ids.host.text = host popup.ids.port.text = port @@ -171,8 +175,10 @@ class ElectrumWindow(App): def btc_to_fiat(self, amount_str): if not amount_str: return '' + if not self.fx.is_enabled(): + return '' rate = self.fx.exchange_rate() - if not rate: + if rate.is_nan(): return '' fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8) return "{:.2f}".format(fiat_amount).rstrip('0').rstrip('.') @@ -181,7 +187,7 @@ class ElectrumWindow(App): if not fiat_amount: return '' rate = self.fx.exchange_rate() - if not rate: + if rate.is_nan(): return '' satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate)) return format_satoshis_plain(satoshis, self.decimal_point()) @@ -234,6 +240,7 @@ class ElectrumWindow(App): self.tabs = None self.is_exit = False self.wallet = None + self.pause_time = 0 App.__init__(self)#, **kwargs) @@ -258,7 +265,7 @@ class ElectrumWindow(App): self.use_change = config.get('use_change', True) self.use_unconfirmed = not config.get('confirmed_only', False) - # create triggers so as to minimize updation a max of 2 times a sec + # create triggers so as to minimize updating 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_history = Clock.create_trigger(self.update_history, .5) @@ -266,6 +273,7 @@ class ElectrumWindow(App): # cached dialogs self._settings_dialog = None self._password_dialog = None + self.fee_status = self.electrum_config.get_fee_status() def wallet_name(self): return os.path.basename(self.wallet.storage.path) if self.wallet else ' ' @@ -387,12 +395,15 @@ class ElectrumWindow(App): intent = Intent(PythonActivity.mActivity, SimpleScannerActivity) def on_qr_result(requestCode, resultCode, intent): - if resultCode == -1: # RESULT_OK: - # this doesn't work due to some bug in jnius: - # contents = intent.getStringExtra("text") - String = autoclass("java.lang.String") - contents = intent.getStringExtra(String("text")) - on_complete(contents) + try: + if resultCode == -1: # RESULT_OK: + # this doesn't work due to some bug in jnius: + # contents = intent.getStringExtra("text") + String = autoclass("java.lang.String") + contents = intent.getStringExtra(String("text")) + on_complete(contents) + finally: + activity.unbind(on_activity_result=on_qr_result) activity.bind(on_activity_result=on_qr_result) PythonActivity.mActivity.startActivityForResult(intent, 0) @@ -433,7 +444,6 @@ class ElectrumWindow(App): #win.softinput_mode = 'below_target' self.on_size(win, win.size) self.init_ui() - self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # init plugins run_hook('init_kivy', self) # fiat currency @@ -452,8 +462,11 @@ class ElectrumWindow(App): if self.network: interests = ['updated', 'status', 'new_transaction', 'verified', 'interfaces'] self.network.register_callback(self.on_network_event, interests) + self.network.register_callback(self.on_fee, ['fee']) self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) + # load wallet + self.load_wallet_by_name(self.electrum_config.get_wallet_path()) # URI passed in config uri = self.electrum_config.get('url') if uri: @@ -471,17 +484,18 @@ class ElectrumWindow(App): wallet.start_threads(self.daemon.network) self.daemon.add_wallet(wallet) self.load_wallet(wallet) - self.on_resume() def load_wallet_by_name(self, path): if not path: return + if self.wallet and self.wallet.storage.path == path: + return wallet = self.daemon.load_wallet(path, None) if wallet: - if wallet != self.wallet: - self.stop_wallet() + if wallet.has_password(): + self.password_dialog(wallet, _('Enter PIN code'), lambda x: self.load_wallet(wallet), self.stop) + else: self.load_wallet(wallet) - self.on_resume() else: Logger.debug('Electrum-Zcash: Wallet not found. Launching install wizard') storage = WalletStorage(path) @@ -491,6 +505,7 @@ class ElectrumWindow(App): wizard.run(action) def on_stop(self): + Logger.info('on_stop') self.stop_wallet() def stop_wallet(self): @@ -604,6 +619,8 @@ class ElectrumWindow(App): @profiler def load_wallet(self, wallet): + if self.wallet: + self.stop_wallet() self.wallet = wallet self.update_wallet() # Once GUI has been initialized check if we want to announce something @@ -626,20 +643,22 @@ class ElectrumWindow(App): if not self.wallet.up_to_date or server_height == 0: status = _("Synchronizing...") elif server_lag > 1: - status = _("Server lagging (%d blocks)"%server_lag) + status = _("Server lagging") else: - c, u, x = self.wallet.get_balance() - text = self.format_amount(c+x+u) - status = str(text.strip() + ' ' + self.base_unit) + status = '' else: status = _("Disconnected") - - n = self.wallet.basename() - self.status = '[size=15dp]%s[/size]\n%s' %(n, status) - #fiat_balance = self.fx.format_amount_and_units(c+u+x) or '' + self.status = self.wallet.basename() + (' [size=15dp](%s)[/size]'%status if status else '') + # balance + c, u, x = self.wallet.get_balance() + text = self.format_amount(c+x+u) + self.balance = str(text.strip()) + ' [size=22dp]%s[/size]'% self.base_unit + self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy def get_max_amount(self): inputs = self.wallet.get_spendable_coins(None, self.electrum_config) + if not inputs: + return '' addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() outputs = [(TYPE_ADDRESS, addr, '!')] tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) @@ -671,17 +690,18 @@ class ElectrumWindow(App): Logger.Error('Notification: needs plyer; `sudo pip install plyer`') def on_pause(self): + self.pause_time = time.time() # pause nfc if self.nfcscanner: self.nfcscanner.nfc_disable() return True def on_resume(self): + now = time.time() + if self.wallet.has_password and now - self.pause_time > 60: + self.password_dialog(self.wallet, _('Enter PIN'), None, self.stop) if self.nfcscanner: self.nfcscanner.nfc_enable() - # workaround p4a bug: - # show an empty info bubble, to refresh the display - self.show_info_bubble('', duration=0.1, pos=(0,0), width=1, arrow_pos=None) def on_size(self, instance, value): width, height = value @@ -703,7 +723,7 @@ class ElectrumWindow(App): 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. + ''' Show an error Message Bubble. ''' self.show_info_bubble( text=error, icon=icon, width=width, pos=pos or Window.center, arrow_pos=arrow_pos, exit=exit, @@ -711,7 +731,7 @@ class ElectrumWindow(App): def show_info(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, duration=0, modal=False): - ''' Show a Info Message Bubble. + ''' Show an Info Message Bubble. ''' self.show_error(error, icon='atlas://gui/kivy/theming/light/important', duration=duration, modal=modal, exit=exit, pos=pos, @@ -719,7 +739,7 @@ class ElectrumWindow(App): 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 + '''Method to show an Information Bubble .. parameters:: text: Message to be displayed @@ -814,7 +834,6 @@ class ElectrumWindow(App): d = LabelDialog(_('Enter description'), text, callback) d.open() - @profiler def amount_dialog(self, screen, show_max): from .uix.dialogs.amount_dialog import AmountDialog amount = screen.amount @@ -826,9 +845,48 @@ class ElectrumWindow(App): popup = AmountDialog(show_max, amount, cb) popup.open() + def invoices_dialog(self, screen): + from .uix.dialogs.invoices import InvoicesDialog + if len(self.wallet.invoices.sorted_list()) == 0: + self.show_info(' '.join([ + _('No saved invoices.'), + _('Signed invoices are saved automatically when you scan them.'), + _('You may also save unsigned requests or contact addresses using the save button.') + ])) + return + popup = InvoicesDialog(self, screen, None) + popup.update() + popup.open() + + def requests_dialog(self, screen): + from .uix.dialogs.requests import RequestsDialog + if len(self.wallet.get_sorted_requests(self.electrum_config)) == 0: + self.show_info(_('No saved requests.')) + return + popup = RequestsDialog(self, screen, None) + popup.update() + popup.open() + + def addresses_dialog(self, screen): + from .uix.dialogs.addresses import AddressesDialog + popup = AddressesDialog(self, screen, None) + popup.update() + popup.open() + + def fee_dialog(self, label, dt): + from .uix.dialogs.fee_dialog import FeeDialog + def cb(): + self.fee_status = self.electrum_config.get_fee_status() + fee_dialog = FeeDialog(self, self.electrum_config, cb) + fee_dialog.open() + + def on_fee(self, event, *arg): + self.fee_status = self.electrum_config.get_fee_status() + def protected(self, msg, f, args): if self.wallet.has_password(): - self.password_dialog(msg, f, args) + on_success = lambda pw: f(*(args + (pw,))) + self.password_dialog(self.wallet, msg, on_success, lambda: None) else: f(*(args + (None,))) @@ -840,8 +898,8 @@ class ElectrumWindow(App): def _delete_wallet(self, b): if b: - basename = os.path.basename(self.wallet.storage.path) - self.protected(_("Enter your PIN code to confirm deletion of %s") % basename, self.__delete_wallet, ()) + basename = self.wallet.basename() + self.protected(_("Enter your PIN code to confirm deletion of {}").format(basename), self.__delete_wallet, ()) def __delete_wallet(self, pw): wallet_path = self.get_wallet_path() @@ -878,40 +936,23 @@ class ElectrumWindow(App): if passphrase: label.text += '\n\n' + _('Passphrase') + ': ' + passphrase - def change_password(self, cb): - if self.wallet.has_password(): - self.protected(_("Changing PIN code.") + '\n' + _("Enter your current PIN:"), self._change_password, (cb,)) - else: - self._change_password(cb, None) - - def _change_password(self, cb, old_password): - if self.wallet.has_password(): - if old_password is None: - return - try: - self.wallet.check_password(old_password) - except InvalidPassword: - self.show_error("Invalid PIN") - return - self.password_dialog(_('Enter new PIN'), self._change_password2, (cb, old_password,)) - - def _change_password2(self, cb, old_password, new_password): - self.password_dialog(_('Confirm new PIN'), self._change_password3, (cb, old_password, new_password)) - - def _change_password3(self, cb, old_password, new_password, confirmed_password): - if new_password == confirmed_password: - self.wallet.update_password(old_password, new_password) - cb() - else: - self.show_error("PIN numbers do not match") - - def password_dialog(self, msg, f, args): + def password_dialog(self, wallet, msg, on_success, on_failure): from .uix.dialogs.password_dialog import PasswordDialog - def callback(pw): - Clock.schedule_once(lambda x: f(*(args + (pw,))), 0.1) if self._password_dialog is None: self._password_dialog = PasswordDialog() - self._password_dialog.init(msg, callback) + self._password_dialog.init(self, wallet, msg, on_success, on_failure) + self._password_dialog.open() + + def change_password(self, cb): + from .uix.dialogs.password_dialog import PasswordDialog + if self._password_dialog is None: + self._password_dialog = PasswordDialog() + message = _("Changing PIN code.") + '\n' + _("Enter your current PIN:") + def on_success(old_password, new_password): + self.wallet.update_password(old_password, new_password) + self.show_info(_("Your PIN code was updated")) + on_failure = lambda: self.show_error(_("PIN codes do not match")) + self._password_dialog.init(self, self.wallet, message, on_success, on_failure, is_change=1) self._password_dialog.open() def export_private_keys(self, pk_label, addr): diff --git a/gui/kivy/nfc_scanner/__init__.py b/gui/kivy/nfc_scanner/__init__.py index 44819b36..a2c6e0d6 100644 --- a/gui/kivy/nfc_scanner/__init__.py +++ b/gui/kivy/nfc_scanner/__init__.py @@ -3,7 +3,7 @@ __all__ = ('NFCBase', 'NFCScanner') class NFCBase(Widget): ''' This is the base Abstract definition class that the actual hardware dependent implementations would be based on. If you want to define a feature that is - accissible and implemented by every platform implementation then define that + accessible and implemented by every platform implementation then define that method in this class. ''' @@ -39,6 +39,6 @@ class NFCBase(Widget): # load NFCScanner implementation NFCScanner = core_select_lib('nfc_manager', ( - # keep the dummy implementtation as the last one to make it the fallback provider.NFCScanner = core_select_lib('nfc_scanner', ( + # keep the dummy implementation as the last one to make it the fallback provider.NFCScanner = core_select_lib('nfc_scanner', ( ('android', 'scanner_android', 'ScannerAndroid'), ('dummy', 'scanner_dummy', 'ScannerDummy')), True, 'electrum_zcash_gui.kivy') diff --git a/gui/kivy/nfc_scanner/scanner_android.py b/gui/kivy/nfc_scanner/scanner_android.py index c9cce5a3..110c416a 100644 --- a/gui/kivy/nfc_scanner/scanner_android.py +++ b/gui/kivy/nfc_scanner/scanner_android.py @@ -1,4 +1,4 @@ -'''This is the Android implementatoin of NFC Scanning using the +'''This is the Android implementation of NFC Scanning using the built in NFC adapter of some android phones. ''' @@ -33,8 +33,8 @@ app = None class ScannerAndroid(NFCBase): - ''' This is the class responsible for handling the interace with the - Android NFC adapter. See Module Documentation for deatils. + ''' This is the class responsible for handling the interface with the + Android NFC adapter. See Module Documentation for details. ''' name = 'NFCAndroid' @@ -56,7 +56,7 @@ class ScannerAndroid(NFCBase): if not self.nfc_adapter: return False - # specify that we want our activity to remain on top whan a new intent + # specify that we want our activity to remain on top when a new intent # is fired self.nfc_pending_intent = PendingIntent.getActivity(context, 0, Intent(context, context.getClass()).addFlags( @@ -128,7 +128,7 @@ class ScannerAndroid(NFCBase): return details def on_new_intent(self, intent): - ''' This functions is called when the application receives a + ''' This function is called when the application receives a new intent, for the ones the application has registered previously, either in the manifest or in the foreground dispatch setup in the nfc_init function above. @@ -184,7 +184,7 @@ class ScannerAndroid(NFCBase): return extRecord def create_ndef_message(self, *recs): - ''' Create the Ndef message that will written to tag + ''' Create the Ndef message that will be written to tag ''' records = [] for record in recs: diff --git a/gui/kivy/theming/light/share.png b/gui/kivy/theming/light/share.png new file mode 100644 index 0000000000000000000000000000000000000000..d0dc761d4544fc091d2e59a22109fc60ffaa0a61 GIT binary patch literal 3325 zcmZu!cTm&Y68dJZqAM%fplM?oB zd7D9kBYmc+rVL#Fv+`O?QwbRg4|UU*06+!#$3Q?<4l5x^=B=TnLbggoN^uX2q&##5 z0P0u`WkmzO`Tg8LlQcul{&q)Jx9-c}l|937Nic=ZjRUdzOI8*Vt+4;ZdyhD zyc#(_Z)yKMB(s_GPWF6MS@?cxNv7r^#s+L6g#e&HI7NpwSQrgZ|4-P> zuw1R57wB?Y#|p`TtC8L1?s`ovn}f1;biIxnJ*Is!Ha;2K#Ga0I5mLMyrc(ZBv@I*q z6?|jrRwWIlnUm}H@x^o9>J_I=*`n+HJ=gBPsFqgzhJ!`(l0`rW3K%C>b)Qh4j+Wn* zvGE@y{qI-YrGC5vY)KGk2*vWsmSC{`3|l?PZO`?yudI(EDT=lDjmmt zE(=gN$ib-``qJhj0HUt?)wN8G!5dyKFJSEV5vUkLKuMHGHYT8%S`3HhqR5ShCm{*N zbd##X5__lNhr;4Jz7=3rurXN@EpJqUxE}|8Wm1vc2E3Jx4Z(l<8NH(OzBKg#%97|F z1U?#Z7XL8Iq${YJn z-eprKsY_IaBq`4n!_phUZg%#IgsJid(9B5|B9^EIg|qE7!`~jNf_?viDrao zTIV$^KSZ`gpHQckBD?x-_Dvkg{3aII;a>Cu~+{X;=X{7rSn612tY-ySUv3PSa> zbkGo0V-#hDRF>fj^2%2d&jRtP+D4RkK^XokG_ZGRrEZ;hX7l6DTNqg<@#j23b84en z72n#yw>6?lC+_@Fig8N#L}Zu!HUx9+6>!XP813UpEntW9^?}hC9&iiX+GEN4Io*lkfwD5wxux(zsdW zStXJ5CH?yRxdiS7u~ve47(QP=(t@6pNtDr2Ho7HtpH9P#-mv*D>(%Pnf-KF(v>b8dVCCQklKd}ykD2dMrY<}SgK{tUGVZx@ z1pj;~rzaxxy`t;Qi&FgB#_b6O{&9aUDV7e?<&2Bto_9Y>l}9E{<*_9KInVif65oyv zHvf5K``U6x$;sIwvXkM#&TdC%?7o!l%j=$NW+Peo1rFJ==2XeO3X(j78<{O!^>_An z1Bxxnrwu;Nws{D^U2ugD#kLc8+@ri66dlQv$QEFP)GuW>Gg5^YxLdW$4^oQMyN zd5DtpMOZ(549#VK2Ld0s8H3r??!Q0dynWG+lwk*Hwun%|$vaoeU|aDbKfRgl^blE! z+KJe|nU4nQ*X&#?=k8#{KtK8%wjQ2E!WU*k7ydzE_ztC*mh4?{4{`70aRked0RD&B zjxC?plq|$=mmum%SEo8Z9&6p#)2WTn=g|Z)(Xu#MU%s_6cxThNUQ8 zTXGwA)zNrA=Yx#hUxaMPS6;{QK)GR22RF_6lt3Xel~?}C`Dx+#`9Mt9&60PgM;wgP zKb{|(6G4`Ra!B01cDM2(uv3oJFpMTBB3R_CPyXn-Y|60^dyqW$I%&2U=)~}vl-1jT zs2c_6gEWKunPZ)4t9o|L09&?8O}p2u_u4ge+b$%K|7p-_P=)0wQxKoG=(K~cD--}4y@2yk`%DW*rIrYj zv-KtX;)T{jC_g3!5R{0$UvY(b=qrJ?EA0+avw5{c+LC)EeM{Pfqt*^oW5&v`O#kG% z$y&^%7A5x~zsyZKPR`zZ3}>W;29)>|(aKhtjeYBPqx6s#PV(X3i?he4JY$4Gk#Ic( zo2960vFQ+TFLtS^1+vud1{JCdzCEy zzR>-^$p1km`clDd(yyD8CG06J3c1gg< zNycS2;<`lE6?U!5?@G%Xdt`wxK=2SyBX#auiJDind4GM(I=*pCbC!`RzvAB$sYNpW z6Yz%fC@D^Jt{b8t(3DqJm)Gqw^Nrks!S{mh2PF2by^E}KM*05Gv}n;)M3@1VVhb{1 zB|3f}TP5h@C7s`d+s7f@P89|7z4Fz3cY3((y?w0EIk%{F{;4#rpy5imDpsDS>fAsj zj(M4e_o86CB?6&L*0+6$yCiCrGWm;0h~@w-?Z64RskrPqcm7#0blPpxCJftp{j;Yx zfqLRsrYkKbc$sX!Uokbz%&8NOc363$Y2piiGKkV2$PBprng0HuE9?6M&tK_NE+h@+ z5uQA$P61t1vGS2uli&xq%Om9hjA zDr96)B6W}KeZQLXf}Ri6*eRvnJtToUD+YoiUF6P0dV+32XQK$19C{oPgoPjyyUvfD z*EDjF)AEXyEHD=k&BT55GeWb9gc|}v$uKg{2CuuOjjo_M|36gfc5+bq-?-!$1R!)1 z`6W3}D7z&?{Qcg&Z{Tg~_qzS;JGaWFIjEr>a>+EmY~1IVtDb#Bc`aKolX@R=V5kU^ z?$8TghyZSlOOA!R>s%A6Ypv&2267wD-_Z4iC20cXKvRjDz+#$yOG?F%87|$bmdu%V z{%&XIObkWd148o$rYhImdU7qpYx$|z-gz}>2r0IVt*@pVD?wxk4Tj3ds0G*Ml_RNo+6EuUVWScx*mxX0j1SG{>?nKlWP_FH6xM|ft z1D~2*8BqyD05k-Q4?Pb)*f(*~V#_GADdpkQwW1@;k-{LU0s<5@KBGrzu>IOc&#m$~ zdK}djdudP9q%7I`h?kR}J~!^1Z~c_qwG)(=)*NbMZcMfKt(Dq7ppjI&&- ziqmarPfd+Og>3D>TNI9g_3H%Dc(xK;xV*7W7RV0rlZ7FlB=j)f2I v4=P)UB@*=B(_-Ac|D)^wOYL`8oYAK9o$vOycUKcWDu9NHwsPfT>#% tag android.manifest.intent_filters = gui/kivy/tools/bitcoin_intent.xml +# (str) launchMode to set for the main activity +android.manifest.launch_mode = singleTask + # (list) Android additionnal libraries to copy into libs/armeabi #android.add_libs_armeabi = lib/android/*.so diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py index 3d4e8a56..9dd0c166 100644 --- a/gui/kivy/uix/context_menu.py +++ b/gui/kivy/uix/context_menu.py @@ -47,7 +47,6 @@ class ContextMenu(Bubble): l = MenuItem() l.text = _(k) def func(f=v): - Clock.schedule_once(lambda dt: self.hide(), 0.1) Clock.schedule_once(lambda dt: f(obj), 0.15) l.on_release = func self.ids.buttons.add_widget(l) diff --git a/gui/kivy/uix/dialogs/__init__.py b/gui/kivy/uix/dialogs/__init__.py index 82a34093..31e9d5b9 100644 --- a/gui/kivy/uix/dialogs/__init__.py +++ b/gui/kivy/uix/dialogs/__init__.py @@ -143,7 +143,7 @@ class InfoBubble(Factory.Bubble): else: Window.add_widget(self) - # wait for the bubble to adjust it's size according to text then animate + # wait for the bubble to adjust its size according to text then animate Clock.schedule_once(lambda dt: self._show(pos, duration)) def _show(self, pos, duration): diff --git a/gui/kivy/uix/dialogs/addresses.py b/gui/kivy/uix/dialogs/addresses.py new file mode 100644 index 00000000..5c50408f --- /dev/null +++ b/gui/kivy/uix/dialogs/addresses.py @@ -0,0 +1,183 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from decimal import Decimal + +Builder.load_string(''' + + text_size: self.width, None + halign: 'left' + valign: 'top' + + + address: '' + memo: '' + amount: '' + status: '' + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + AddressLabel: + text: root.address + shorten: True + Widget + AddressLabel: + text: (root.amount if root.status == 'Funded' else root.status) + ' ' + root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + + + id: popup + title: _('Addresses') + message: '' + pr_status: 'Pending' + show_change: 0 + show_used: 0 + on_message: + self.update() + BoxLayout: + id:box + padding: '12dp', '70dp', '12dp', '12dp' + spacing: '12dp' + orientation: 'vertical' + size_hint: 1, 1.1 + BoxLayout: + spacing: '6dp' + size_hint: 1, None + orientation: 'horizontal' + AddressFilter: + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + AddressButton: + id: search + text: {0:_('Receiving'), 1:_('Change'), 2:_('All')}[root.show_change] + on_release: + root.show_change = (root.show_change + 1) % 3 + Clock.schedule_once(lambda dt: root.update()) + AddressFilter: + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + AddressButton: + id: search + text: {0:_('All'), 1:_('Unused'), 2:_('Funded'), 3:_('Used')}[root.show_used] + on_release: + root.show_used = (root.show_used + 1) % 4 + Clock.schedule_once(lambda dt: root.update()) + AddressFilter: + opacity: 1 + size_hint: 1, None + height: self.minimum_height + spacing: '5dp' + canvas.before: + Color: + rgba: 0.9, 0.9, 0.9, 1 + AddressButton: + id: change + text: root.message if root.message else _('Search') + on_release: Clock.schedule_once(lambda dt: app.description_dialog(popup)) + ScrollView: + GridLayout: + cols: 1 + id: search_container + size_hint_y: None + height: self.minimum_height +''') + + +from electrum_zcash_gui.kivy.i18n import _ +from electrum_zcash_gui.kivy.uix.context_menu import ContextMenu + + +class EmptyLabel(Factory.Label): + pass + + +class AddressesDialog(Factory.Popup): + + def __init__(self, app, screen, callback): + Factory.Popup.__init__(self) + self.app = app + self.screen = screen + self.callback = callback + self.cards = {} + self.context_menu = None + + def get_card(self, addr, balance, is_used, label): + ci = self.cards.get(addr) + if ci is None: + ci = Factory.AddressItem() + ci.screen = self + ci.address = addr + self.cards[addr] = ci + ci.memo = label + ci.amount = self.app.format_amount_and_units(balance) + ci.status = _('Used') if is_used else _('Funded') if balance > 0 else _('Unused') + return ci + + def update(self): + self.menu_actions = [(_('Use'), self.do_use), (_('Details'), self.do_view)] + wallet = self.app.wallet + if self.show_change == 0: + _list = wallet.get_receiving_addresses() + elif self.show_change == 1: + _list = wallet.get_change_addresses() + else: + _list = wallet.get_addresses() + search = self.message + container = self.ids.search_container + container.clear_widgets() + n = 0 + for address in _list: + label = wallet.labels.get(address, '') + balance = sum(wallet.get_addr_balance(address)) + is_used = wallet.is_used(address) + if self.show_used == 1 and (balance or is_used): + continue + if self.show_used == 2 and balance == 0: + continue + if self.show_used == 3 and not is_used: + continue + card = self.get_card(address, balance, is_used, label) + if search and not self.ext_search(card, search): + continue + container.add_widget(card) + n += 1 + if not n: + msg = _('No address matching your search') + container.add_widget(EmptyLabel(text=msg)) + + def do_use(self, obj): + self.hide_menu() + self.dismiss() + self.app.show_request(obj.address) + + def do_view(self, obj): + req = { 'address': obj.address, 'status' : obj.status } + status = obj.status + c, u, x = self.app.wallet.get_addr_balance(obj.address) + balance = c + u + x + if balance > 0: + req['fund'] = balance + self.app.show_addr_details(req, status) + + def ext_search(self, card, search): + return card.memo.find(search) >= 0 or card.amount.find(search) >= 0 + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/gui/kivy/uix/dialogs/amount_dialog.py b/gui/kivy/uix/dialogs/amount_dialog.py index cdb8ae44..244f8e61 100644 --- a/gui/kivy/uix/dialogs/amount_dialog.py +++ b/gui/kivy/uix/dialogs/amount_dialog.py @@ -13,21 +13,35 @@ Builder.load_string(''' anchor_x: 'center' BoxLayout: orientation: 'vertical' - size_hint: 0.8, 1 + size_hint: 0.9, 1 + Widget: + size_hint: 1, 0.2 BoxLayout: size_hint: 1, None height: '80dp' - Label: - id: a - btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else '' - fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else '' - text1: ((self.fiat_text if kb.is_fiat else self.btc_text) if app.fiat_unit else self.btc_text) if self.btc_text else '' - text2: ((self.btc_text if kb.is_fiat else self.fiat_text) if app.fiat_unit else '') if self.btc_text else '' - text: self.text1 + "\\n" + "[color=#8888ff]" + self.text2 + "[/color]" + Button: + background_color: 0, 0, 0, 0 + id: btc + text: kb.amount + ' ' + app.base_unit + color: (0.7, 0.7, 1, 1) if kb.is_fiat else (1, 1, 1, 1) halign: 'right' size_hint: 1, None - font_size: '22dp' - height: '80dp' + font_size: '20dp' + height: '48dp' + on_release: + kb.is_fiat = False + Button: + background_color: 0, 0, 0, 0 + id: fiat + text: kb.fiat_amount + ' ' + app.fiat_unit + color: (1, 1, 1, 1) if kb.is_fiat else (0.7, 0.7, 1, 1) + halign: 'right' + size_hint: 1, None + font_size: '20dp' + height: '48dp' + disabled: not app.fx.is_enabled() + on_release: + kb.is_fiat = True Widget: size_hint: 1, 0.2 GridLayout: @@ -65,6 +79,9 @@ Builder.load_string(''' text: '0' KButton: text: '<' + Widget: + size_hint: 1, None + height: '48dp' Button: id: but_max opacity: 1 if root.show_max else 0 @@ -75,13 +92,6 @@ Builder.load_string(''' on_release: kb.is_fiat = False kb.amount = app.get_max_amount() - Button: - id: button_fiat - size_hint: 1, None - height: '48dp' - text: (app.base_unit if not kb.is_fiat else app.fiat_unit) if app.fiat_unit else '' - on_release: - if app.fiat_unit: popup.toggle_fiat(kb) Button: size_hint: 1, None height: '48dp' @@ -102,7 +112,7 @@ Builder.load_string(''' height: '48dp' text: _('OK') on_release: - root.callback(a.btc_text) + root.callback(btc.text if kb.amount else '') popup.dismiss() ''') @@ -117,9 +127,6 @@ class AmountDialog(Factory.Popup): if amount: self.ids.kb.amount = amount - def toggle_fiat(self, a): - a.is_fiat = not a.is_fiat - def update_amount(self, c): kb = self.ids.kb amount = kb.fiat_amount if kb.is_fiat else kb.amount diff --git a/gui/kivy/uix/dialogs/fee_dialog.py b/gui/kivy/uix/dialogs/fee_dialog.py index 20922112..9d2f2ad9 100644 --- a/gui/kivy/uix/dialogs/fee_dialog.py +++ b/gui/kivy/uix/dialogs/fee_dialog.py @@ -3,7 +3,6 @@ from kivy.factory import Factory from kivy.properties import ObjectProperty from kivy.lang import Builder -from electrum_zcash.util import fee_levels from electrum_zcash_gui.kivy.i18n import _ Builder.load_string(''' @@ -12,29 +11,46 @@ Builder.load_string(''' title: _('Transaction Fees') size_hint: 0.8, 0.8 pos_hint: {'top':0.9} + method: 0 BoxLayout: orientation: 'vertical' BoxLayout: orientation: 'horizontal' size_hint: 1, 0.5 Label: - id: fee_per_kb + text: _('Method') + ':' + Button: + text: _('Mempool') if root.method == 2 else _('ETA') if root.method == 1 else _('Static') + background_color: (0,0,0,0) + bold: True + on_release: + root.method = (root.method + 1) % 3 + root.update_slider() + root.update_text() + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: (_('Target') if root.method > 0 else _('Fee')) + ':' + Label: + id: fee_target text: '' Slider: id: slider range: 0, 4 step: 1 on_value: root.on_slider(self.value) + Widget: + size_hint: 1, 0.5 BoxLayout: orientation: 'horizontal' size_hint: 1, 0.5 - Label: - text: _('Dynamic Fees') - CheckBox: - id: dynfees - on_active: root.on_checkbox(self.active) + TopLabel: + id: fee_estimate + text: '' + font_size: '14dp' Widget: - size_hint: 1, 1 + size_hint: 1, 0.5 BoxLayout: orientation: 'horizontal' size_hint: 1, 0.5 @@ -58,55 +74,58 @@ class FeeDialog(Factory.Popup): Factory.Popup.__init__(self) self.app = app self.config = config - self.fee_rate = self.config.fee_per_kb() self.callback = callback - self.dynfees = self.config.get('dynamic_fees', True) - self.ids.dynfees.active = self.dynfees + mempool = self.config.use_mempool_fees() + dynfees = self.config.is_dynfee() + self.method = (2 if mempool else 1) if dynfees else 0 self.update_slider() self.update_text() def update_text(self): - value = int(self.ids.slider.value) - self.ids.fee_per_kb.text = self.get_fee_text(value) + pos = int(self.ids.slider.value) + dynfees, mempool = self.get_method() + if self.method == 2: + fee_rate = self.config.depth_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate) + elif self.method == 1: + fee_rate = self.config.eta_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate) + else: + fee_rate = self.config.static_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate) + msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate) + + self.ids.fee_target.text = target + self.ids.fee_estimate.text = msg + + def get_method(self): + dynfees = self.method > 0 + mempool = self.method == 2 + return dynfees, mempool def update_slider(self): slider = self.ids.slider - if self.dynfees: - slider.range = (0, 4) - slider.step = 1 - slider.value = self.config.get('fee_level', 2) - else: - slider.range = (0, 9) - slider.step = 1 - slider.value = self.config.static_fee_index(self.fee_rate) - - def get_fee_text(self, value): - if self.ids.dynfees.active: - tooltip = fee_levels[value] - if self.config.has_fee_estimates(): - dynfee = self.config.dynfee(value) - tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB' - else: - fee_rate = self.config.static_fee(value) - tooltip = self.app.format_amount_and_units(fee_rate) + '/kB' - if self.config.has_fee_estimates(): - i = self.config.reverse_dynfee(fee_rate) - tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i) - return tooltip + dynfees, mempool = self.get_method() + maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) + slider.range = (0, maxp) + slider.step = 1 + slider.value = pos def on_ok(self): value = int(self.ids.slider.value) - self.config.set_key('dynamic_fees', self.dynfees, False) - if self.dynfees: - self.config.set_key('fee_level', value, True) + dynfees, mempool = self.get_method() + self.config.set_key('dynamic_fees', dynfees, False) + self.config.set_key('mempool_fees', mempool, False) + if dynfees: + if mempool: + self.config.set_key('depth_level', value, True) + else: + self.config.set_key('fee_level', value, True) else: self.config.set_key('fee_per_kb', self.config.static_fee(value), True) self.callback() def on_slider(self, value): self.update_text() - - def on_checkbox(self, b): - self.dynfees = b - self.update_slider() - self.update_text() diff --git a/gui/kivy/uix/dialogs/fx_dialog.py b/gui/kivy/uix/dialogs/fx_dialog.py index fafbf4dc..d582c54d 100644 --- a/gui/kivy/uix/dialogs/fx_dialog.py +++ b/gui/kivy/uix/dialogs/fx_dialog.py @@ -106,4 +106,6 @@ class FxDialog(Factory.Popup): if ccy != self.fx.get_currency(): self.fx.set_currency(ccy) self.app.fiat_unit = ccy + else: + self.app.is_fiat = False Clock.schedule_once(lambda dt: self.add_exchanges()) diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py index e519d3e8..527195a0 100644 --- a/gui/kivy/uix/dialogs/installwizard.py +++ b/gui/kivy/uix/dialogs/installwizard.py @@ -135,7 +135,7 @@ Builder.load_string(''' height: self.minimum_height Label: color: root.text_color - text: _('From %d cosigners')%n.value + text: _('From {} cosigners').format(n.value) Slider: id: n range: 2, 5 @@ -143,7 +143,7 @@ Builder.load_string(''' value: 2 Label: color: root.text_color - text: _('Require %d signatures')%m.value + text: _('Require {} signatures').format(m.value) Slider: id: m range: 1, n.value @@ -613,7 +613,7 @@ class RestoreSeedDialog(WizardDialog): for c in line.children: if isinstance(c, Button): if c.text in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': - c.disabled = (c.text.lower() not in p) and last_word + c.disabled = (c.text.lower() not in p) and bool(last_word) elif c.text == ' ': c.disabled = not enable_space @@ -702,6 +702,7 @@ class AddXpubDialog(WizardDialog): self.is_valid = kwargs['is_valid'] self.title = kwargs['title'] self.message = kwargs['message'] + self.allow_multi = kwargs.get('allow_multi', False) def check_text(self, dt): self.ids.next.disabled = not bool(self.is_valid(self.get_text())) @@ -715,7 +716,10 @@ class AddXpubDialog(WizardDialog): def scan_xpub(self): def on_complete(text): - self.ids.text_input.text = text + if self.allow_multi: + self.ids.text_input.text += text + '\n' + else: + self.ids.text_input.text = text self.app.scan_qr(on_complete) def do_paste(self): @@ -798,28 +802,18 @@ class InstallWizard(BaseWizard, Widget): app = App.get_running_app() Clock.schedule_once(lambda dt: app.show_error(msg)) - def password_dialog(self, message, callback): + def request_password(self, run_next, force_disable_encrypt_cb=False): + def on_success(old_pin, pin): + assert old_pin is None + run_next(pin, False) + def on_failure(): + self.show_error(_('PIN mismatch')) + self.run('request_password', run_next) popup = PasswordDialog() - popup.init(message, callback) + app = App.get_running_app() + popup.init(app, None, _('Choose PIN code'), on_success, on_failure, is_change=2) popup.open() - def request_password(self, run_next): - def callback(pin): - if pin: - self.run('confirm_password', pin, run_next) - else: - run_next(None, None) - self.password_dialog('Choose a PIN code', callback) - - def confirm_password(self, pin, run_next): - def callback(conf): - if conf == pin: - run_next(pin, False) - else: - self.show_error(_('PIN mismatch')) - self.run('request_password', run_next) - self.password_dialog('Confirm your PIN code', callback) - def action_dialog(self, action, run_next): f = getattr(self, action) f() diff --git a/gui/kivy/uix/dialogs/invoices.py b/gui/kivy/uix/dialogs/invoices.py new file mode 100644 index 00000000..515f7acc --- /dev/null +++ b/gui/kivy/uix/dialogs/invoices.py @@ -0,0 +1,169 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from decimal import Decimal + +Builder.load_string(''' + + #color: .305, .309, .309, 1 + text_size: self.width, None + halign: 'left' + valign: 'top' + + + requestor: '' + memo: '' + amount: '' + status: '' + date: '' + icon: 'atlas://gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + InvoicesLabel: + text: root.requestor + shorten: True + Widget + InvoicesLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + InvoicesLabel: + text: root.amount + font_size: '15sp' + halign: 'right' + width: '110sp' + Widget + InvoicesLabel: + text: root.status + font_size: '13sp' + halign: 'right' + color: .699, .699, .699, 1 + Widget + + + + id: popup + title: _('Invoices') + BoxLayout: + id: box + orientation: 'vertical' + spacing: '1dp' + ScrollView: + GridLayout: + cols: 1 + id: invoices_container + size_hint: 1, None + height: self.minimum_height + spacing: '2dp' + padding: '12dp' +''') + +from kivy.properties import BooleanProperty +from electrum_zcash_gui.kivy.i18n import _ +from electrum_zcash.util import format_time +from electrum_zcash.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from electrum_zcash_gui.kivy.uix.context_menu import ContextMenu + +invoice_text = { + PR_UNPAID:_('Pending'), + PR_UNKNOWN:_('Unknown'), + PR_PAID:_('Paid'), + PR_EXPIRED:_('Expired') +} +pr_icon = { + PR_UNPAID: 'atlas://gui/kivy/theming/light/important', + PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', + PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', + PR_EXPIRED: 'atlas://gui/kivy/theming/light/close' +} + + +class InvoicesDialog(Factory.Popup): + + def __init__(self, app, screen, callback): + Factory.Popup.__init__(self) + self.app = app + self.screen = screen + self.callback = callback + self.cards = {} + self.context_menu = None + + def get_card(self, pr): + key = pr.get_id() + ci = self.cards.get(key) + if ci is None: + ci = Factory.InvoiceItem() + ci.key = key + ci.screen = self + self.cards[key] = ci + ci.requestor = pr.get_requestor() + ci.memo = pr.get_memo() + amount = pr.get_amount() + if amount: + ci.amount = self.app.format_amount_and_units(amount) + status = self.app.wallet.invoices.get_status(ci.key) + ci.status = invoice_text[status] + ci.icon = pr_icon[status] + else: + ci.amount = _('No Amount') + ci.status = '' + exp = pr.get_expiration_date() + ci.date = format_time(exp) if exp else _('Never') + return ci + + def update(self): + self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)] + invoices_list = self.ids.invoices_container + invoices_list.clear_widgets() + _list = self.app.wallet.invoices.sorted_list() + for pr in _list: + ci = self.get_card(pr) + invoices_list.add_widget(ci) + + def do_pay(self, obj): + self.hide_menu() + self.dismiss() + pr = self.app.wallet.invoices.get(obj.key) + self.app.on_pr(pr) + + def do_view(self, obj): + pr = self.app.wallet.invoices.get(obj.key) + pr.verify(self.app.wallet.contacts) + self.app.show_pr_details(pr.get_dict(), obj.status, True) + + def do_delete(self, obj): + from .question import Question + def cb(result): + if result: + self.app.wallet.invoices.remove(obj.key) + self.hide_menu() + self.update() + d = Question(_('Delete invoice?'), cb) + d.open() + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/gui/kivy/uix/dialogs/password_dialog.py b/gui/kivy/uix/dialogs/password_dialog.py index 274b8a18..430d0389 100644 --- a/gui/kivy/uix/dialogs/password_dialog.py +++ b/gui/kivy/uix/dialogs/password_dialog.py @@ -5,35 +5,42 @@ from kivy.lang import Builder from decimal import Decimal from kivy.clock import Clock +from electrum_zcash.util import InvalidPassword +from electrum_zcash_gui.kivy.i18n import _ + Builder.load_string(''' id: popup - title: _('PIN Code') + title: 'Electrum-Zcash' message: '' - size_hint: 0.9, 0.9 BoxLayout: + size_hint: 1, 1 orientation: 'vertical' Widget: - size_hint: 1, 1 + size_hint: 1, 0.05 Label: + font_size: '20dp' text: root.message text_size: self.width, None size: self.texture_size Widget: - size_hint: 1, 1 + size_hint: 1, 0.05 Label: id: a - text: ' * '*len(kb.password) + ' o '*(6-len(kb.password)) + font_size: '50dp' + text: '*'*len(kb.password) + '-'*(6-len(kb.password)) + size: self.texture_size Widget: - size_hint: 1, 1 + size_hint: 1, 0.05 GridLayout: id: kb + size_hint: 1, None + height: self.minimum_height update_amount: popup.update_password password: '' on_password: popup.on_password(self.password) - size_hint: 1, None - height: '200dp' + spacing: '2dp' cols: 3 KButton: text: '1' @@ -59,30 +66,44 @@ Builder.load_string(''' text: '0' KButton: text: '<' - BoxLayout: - size_hint: 1, None - height: '48dp' - Widget: - size_hint: 0.5, None - Button: - size_hint: 0.5, None - height: '48dp' - text: _('Cancel') - on_release: - popup.dismiss() - popup.callback(None) ''') class PasswordDialog(Factory.Popup): - #def __init__(self, message, callback): - # Factory.Popup.__init__(self) - - def init(self, message, callback): + def init(self, app, wallet, message, on_success, on_failure, is_change=0): + self.app = app + self.wallet = wallet self.message = message - self.callback = callback + self.on_success = on_success + self.on_failure = on_failure self.ids.kb.password = '' + self.success = False + self.is_change = is_change + self.pw = None + self.new_password = None + self.title = 'Electrum-Zcash' + (' - ' + self.wallet.basename() if self.wallet else '') + + def check_password(self, password): + if self.is_change > 1: + return True + try: + self.wallet.check_password(password) + return True + except InvalidPassword as e: + return False + + def on_dismiss(self): + if not self.success: + if self.on_failure: + self.on_failure() + else: + # keep dialog open + return True + else: + if self.on_success: + args = (self.pw, self.new_password) if self.is_change else (self.pw,) + Clock.schedule_once(lambda dt: self.on_success(*args), 0.1) def update_password(self, c): kb = self.ids.kb @@ -97,5 +118,25 @@ class PasswordDialog(Factory.Popup): def on_password(self, pw): if len(pw) == 6: - self.dismiss() - Clock.schedule_once(lambda dt: self.callback(pw), 0.1) + if self.check_password(pw): + if self.is_change == 0: + self.success = True + self.pw = pw + self.message = _('Please wait...') + self.dismiss() + elif self.is_change == 1: + self.pw = pw + self.message = _('Enter new PIN') + self.ids.kb.password = '' + self.is_change = 2 + elif self.is_change == 2: + self.new_password = pw + self.message = _('Confirm new PIN') + self.ids.kb.password = '' + self.is_change = 3 + elif self.is_change == 3: + self.success = pw == self.new_password + self.dismiss() + else: + self.app.show_error(_('Wrong PIN')) + self.ids.kb.password = '' diff --git a/gui/kivy/uix/dialogs/requests.py b/gui/kivy/uix/dialogs/requests.py new file mode 100644 index 00000000..1da1491f --- /dev/null +++ b/gui/kivy/uix/dialogs/requests.py @@ -0,0 +1,157 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from decimal import Decimal + +Builder.load_string(''' + + #color: .305, .309, .309, 1 + text_size: self.width, None + halign: 'left' + valign: 'top' + + + address: '' + memo: '' + amount: '' + status: '' + date: '' + icon: 'atlas://gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.address + shorten: True + Widget + RequestLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.amount + halign: 'right' + font_size: '15sp' + Widget + RequestLabel: + text: root.status + halign: 'right' + font_size: '13sp' + color: .699, .699, .699, 1 + Widget + + + id: popup + title: _('Requests') + BoxLayout: + id:box + orientation: 'vertical' + spacing: '1dp' + ScrollView: + GridLayout: + cols: 1 + id: requests_container + size_hint: 1, None + height: self.minimum_height + spacing: '2dp' + padding: '12dp' +''') + +from kivy.properties import BooleanProperty +from electrum_zcash_gui.kivy.i18n import _ +from electrum_zcash.util import format_time +from electrum_zcash.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED +from electrum_zcash_gui.kivy.uix.context_menu import ContextMenu + +pr_icon = { + PR_UNPAID: 'atlas://gui/kivy/theming/light/important', + PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', + PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', + PR_EXPIRED: 'atlas://gui/kivy/theming/light/close' +} +request_text = { + PR_UNPAID: _('Pending'), + PR_UNKNOWN: _('Unknown'), + PR_PAID: _('Received'), + PR_EXPIRED: _('Expired') +} + + +class RequestsDialog(Factory.Popup): + + def __init__(self, app, screen, callback): + Factory.Popup.__init__(self) + self.app = app + self.screen = screen + self.callback = callback + self.cards = {} + self.context_menu = None + + def get_card(self, req): + address = req['address'] + ci = self.cards.get(address) + if ci is None: + ci = Factory.RequestItem() + ci.address = address + ci.screen = self + self.cards[address] = ci + + amount = req.get('amount') + ci.amount = self.app.format_amount_and_units(amount) if amount else '' + ci.memo = req.get('memo', '') + status, conf = self.app.wallet.get_request_status(address) + ci.status = request_text[status] + ci.icon = pr_icon[status] + #exp = pr.get_expiration_date() + #ci.date = format_time(exp) if exp else _('Never') + return ci + + def update(self): + self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)] + requests_list = self.ids.requests_container + requests_list.clear_widgets() + _list = self.app.wallet.get_sorted_requests(self.app.electrum_config) + for pr in _list: + ci = self.get_card(pr) + requests_list.add_widget(ci) + + def do_show(self, obj): + self.hide_menu() + self.dismiss() + self.app.show_request(obj.address) + + def do_delete(self, req): + from .question import Question + def cb(result): + if result: + self.app.wallet.remove_payment_request(req.address, self.app.electrum_config) + self.hide_menu() + self.update() + d = Question(_('Delete request'), cb) + d.open() + + def show_menu(self, obj): + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) + self.ids.box.add_widget(self.context_menu) + + def hide_menu(self): + if self.context_menu is not None: + self.ids.box.remove_widget(self.context_menu) + self.context_menu = None diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py index d43225b3..9aebd633 100644 --- a/gui/kivy/uix/dialogs/settings.py +++ b/gui/kivy/uix/dialogs/settings.py @@ -8,7 +8,6 @@ from electrum_zcash.i18n import languages from electrum_zcash_gui.kivy.i18n import _ from electrum_zcash.plugins import run_hook from electrum_zcash import coinchooser -from electrum_zcash.util import fee_levels from .choice_dialog import ChoiceDialog @@ -37,9 +36,8 @@ Builder.load_string(''' action: partial(root.language_dialog, self) CardSeparator SettingsItem: - status: '' if root.disable_pin else ('ON' if root.use_encryption else 'OFF') disabled: root.disable_pin - title: _('PIN code') + ': ' + self.status + title: _('PIN code') description: _("Change your PIN code.") action: partial(root.change_password, self) CardSeparator @@ -49,12 +47,6 @@ Builder.load_string(''' description: _("Base unit for Zcash amounts.") action: partial(root.unit_dialog, self) CardSeparator - SettingsItem: - status: root.fee_status() - title: _('Fees') + ': ' + self.status - description: _("Fees paid to the Zcash miners.") - action: partial(root.fee_dialog, self) - CardSeparator SettingsItem: status: root.fx_status() title: _('Fiat Currency') + ': ' + self.status @@ -80,12 +72,14 @@ Builder.load_string(''' description: _("Send your change to separate addresses.") message: _('Send excess coins to change addresses') action: partial(root.boolean_dialog, 'use_change', _('Use change addresses'), self.message) - CardSeparator - SettingsItem: - status: root.coinselect_status() - title: _('Coin selection') + ': ' + self.status - description: "Coin selection method" - action: partial(root.coinselect_dialog, self) + + # disabled: there is currently only one coin selection policy + #CardSeparator + #SettingsItem: + # status: root.coinselect_status() + # title: _('Coin selection') + ': ' + self.status + # description: "Coin selection method" + # action: partial(root.coinselect_dialog, self) ''') @@ -101,7 +95,6 @@ class SettingsDialog(Factory.Popup): layout.bind(minimum_height=layout.setter('height')) # cached dialogs self._fx_dialog = None - self._fee_dialog = None self._proxy_dialog = None self._language_dialog = None self._unit_dialog = None @@ -192,18 +185,7 @@ class SettingsDialog(Factory.Popup): d.open() def fee_status(self): - if self.config.get('dynamic_fees', True): - return fee_levels[self.config.get('fee_level', 2)] - else: - return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB' - - def fee_dialog(self, label, dt): - if self._fee_dialog is None: - from .fee_dialog import FeeDialog - def cb(): - label.status = self.fee_status() - self._fee_dialog = FeeDialog(self.app, self.config, cb) - self._fee_dialog.open() + return self.config.get_fee_status() def boolean_dialog(self, name, title, message, dt): from .checkbox_dialog import CheckBoxDialog diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py index 5fe487cd..62a15e63 100644 --- a/gui/kivy/uix/dialogs/tx_dialog.py +++ b/gui/kivy/uix/dialogs/tx_dialog.py @@ -19,6 +19,7 @@ Builder.load_string(''' can_broadcast: False fee_str: '' date_str: '' + date_label:'' amount_str: '' tx_hash: '' status_str: '' @@ -45,7 +46,7 @@ Builder.load_string(''' text: _('Description') if root.description else '' value: root.description BoxLabel: - text: _('Date') if root.date_str else '' + text: root.date_label value: root.date_str BoxLabel: text: _('Amount sent') if root.is_mine else _('Amount received') @@ -108,10 +109,13 @@ class TxDialog(Factory.Popup): tx_hash, self.status_str, self.description, self.can_broadcast, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) self.tx_hash = tx_hash or '' if timestamp: + self.date_label = _('Date') self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] elif exp_n: - self.date_str = _('Within %d blocks') % exp_n if exp_n > 0 else _('unknown (low fee)') + self.date_label = _('Mempool depth') + self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000)) else: + self.date_label = '' self.date_str = '' if amount is None: diff --git a/gui/kivy/uix/menus.py b/gui/kivy/uix/menus.py index 10ca5624..65aa6037 100644 --- a/gui/kivy/uix/menus.py +++ b/gui/kivy/uix/menus.py @@ -7,7 +7,7 @@ from kivy.uix.bubble import Bubble, BubbleButton from kivy.properties import ListProperty from kivy.uix.widget import Widget -from electrum_zcash_gui.i18n import _ +from electrum_zcash_gui.kivy.i18n import _ class ContextMenuItem(Widget): '''abstract class diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index a32f7a02..87f981f5 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -17,7 +17,7 @@ from kivy.lang import Builder from kivy.factory import Factory from kivy.utils import platform -from electrum_zcash.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds +from electrum_zcash.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum_zcash import bitcoin from electrum_zcash.util import timestamp_to_datetime from electrum_zcash.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED @@ -27,8 +27,6 @@ from .context_menu import ContextMenu from electrum_zcash_gui.kivy.i18n import _ -class EmptyLabel(Factory.Label): - pass class CScreen(Factory.Screen): __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave') @@ -87,9 +85,9 @@ class CScreen(Factory.Screen): self.add_widget(self.context_menu) +# note: this list needs to be kept in sync with another in qt TX_ICONS = [ - "close", - "close", + "unconfirmed", "close", "unconfirmed", "close", @@ -133,7 +131,6 @@ class HistoryScreen(CScreen): status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp) icon = "atlas://gui/kivy/theming/light/" + TX_ICONS[status] label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs') - date = timestamp_to_datetime(timestamp) ri = self.cards.get(tx_hash) if ri is None: ri = Factory.HistoryItem() @@ -143,14 +140,16 @@ class HistoryScreen(CScreen): ri.icon = icon ri.date = status_str ri.message = label - ri.value = value or 0 - ri.amount = self.app.format_amount(value, True) if value is not None else '--' ri.confirmations = conf - if self.app.fiat_unit and date: - rate = self.app.fx.history_rate(date) - if rate: - s = self.app.fx.value_str(value, rate) - ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit + if value is not None: + ri.is_mine = value < 0 + if value < 0: value = - value + ri.amount = self.app.format_amount_and_units(value) + if self.app.fiat_unit: + fx = self.app.fx + fiat_value = value / Decimal(bitcoin.COIN) * self.app.wallet.price_at_timestamp(tx_hash, fx.timestamp_rate) + fiat_value = Fiat(fiat_value, fx.ccy) + ri.quote_text = str(fiat_value) return ri def update(self, see_all=False): @@ -162,13 +161,8 @@ class HistoryScreen(CScreen): count = 0 for item in history: ri = self.get_card(*item) - count += 1 history_card.add_widget(ri) - if count == 0: - msg = _('This screen shows your list of transactions. It is currently empty.') - history_card.add_widget(EmptyLabel(text=msg)) - class SendScreen(CScreen): @@ -215,7 +209,7 @@ class SendScreen(CScreen): if not self.screen.address: return if self.screen.is_pr: - # it sould be already saved + # it should be already saved return # save address as invoice from electrum_zcash.paymentrequest import make_unsigned_request, PaymentRequest @@ -225,7 +219,6 @@ class SendScreen(CScreen): pr = make_unsigned_request(req).SerializeToString() pr = PaymentRequest(pr) self.app.wallet.invoices.add(pr) - self.app.update_tab('invoices') self.app.show_info(_("Invoice saved")) if pr.is_pr(): self.screen.is_pr = True @@ -294,7 +287,7 @@ class SendScreen(CScreen): def on_success(tx): if tx.is_complete(): self.app.broadcast(tx, self.payment_request) - self.app.wallet.set_label(tx.hash(), message) + self.app.wallet.set_label(tx.txid(), message) else: self.app.tx_dialog(tx) def on_failure(error): @@ -372,215 +365,33 @@ class ReceiveScreen(CScreen): def save_request(self): addr = self.screen.address + if not addr: + return False amount = self.screen.amount message = self.screen.message amount = self.app.get_amount(amount) if amount else 0 req = self.app.wallet.make_payment_request(addr, amount, message, None) - self.app.wallet.add_payment_request(req, self.app.electrum_config) - self.app.update_tab('requests') + try: + self.app.wallet.add_payment_request(req, self.app.electrum_config) + added_request = True + except Exception as e: + self.app.show_error(_('Error adding payment request') + ':\n' + str(e)) + added_request = False + finally: + self.app.update_tab('requests') + return added_request def on_amount_or_message(self): - self.save_request() Clock.schedule_once(lambda dt: self.update_qr()) def do_new(self): addr = self.get_new_address() if not addr: self.app.show_info(_('Please use the existing requests first.')) - else: - self.save_request() - self.app.show_info(_('New request added to your list.')) - - -invoice_text = { - PR_UNPAID:_('Pending'), - PR_UNKNOWN:_('Unknown'), - PR_PAID:_('Paid'), - PR_EXPIRED:_('Expired') -} -request_text = { - PR_UNPAID: _('Pending'), - PR_UNKNOWN: _('Unknown'), - PR_PAID: _('Received'), - PR_EXPIRED: _('Expired') -} -pr_icon = { - PR_UNPAID: 'atlas://gui/kivy/theming/light/important', - PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', - PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', - PR_EXPIRED: 'atlas://gui/kivy/theming/light/close' -} - - -class InvoicesScreen(CScreen): - kvname = 'invoices' - cards = {} - - def get_card(self, pr): - key = pr.get_id() - ci = self.cards.get(key) - if ci is None: - ci = Factory.InvoiceItem() - ci.key = key - ci.screen = self - self.cards[key] = ci - - ci.requestor = pr.get_requestor() - ci.memo = pr.get_memo() - amount = pr.get_amount() - if amount: - ci.amount = self.app.format_amount_and_units(amount) - status = self.app.wallet.invoices.get_status(ci.key) - ci.status = invoice_text[status] - ci.icon = pr_icon[status] - else: - ci.amount = _('No Amount') - ci.status = '' - exp = pr.get_expiration_date() - ci.date = format_time(exp) if exp else _('Never') - return ci - - def update(self): - self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)] - invoices_list = self.screen.ids.invoices_container - invoices_list.clear_widgets() - _list = self.app.wallet.invoices.sorted_list() - for pr in _list: - ci = self.get_card(pr) - invoices_list.add_widget(ci) - if not _list: - msg = _('This screen shows the list of payment requests that have been sent to you. You may also use it to store contact addresses.') - invoices_list.add_widget(EmptyLabel(text=msg)) - - def do_pay(self, obj): - pr = self.app.wallet.invoices.get(obj.key) - self.app.on_pr(pr) - - def do_view(self, obj): - pr = self.app.wallet.invoices.get(obj.key) - pr.verify(self.app.wallet.contacts) - self.app.show_pr_details(pr.get_dict(), obj.status, True) - - def do_delete(self, obj): - from .dialogs.question import Question - def cb(result): - if result: - self.app.wallet.invoices.remove(obj.key) - self.app.update_tab('invoices') - d = Question(_('Delete invoice?'), cb) - d.open() - - -address_icon = { - 'Pending' : 'atlas://gui/kivy/theming/light/important', - 'Paid' : 'atlas://gui/kivy/theming/light/confirmed' -} - -class AddressScreen(CScreen): - kvname = 'address' - cards = {} - - def get_card(self, addr, balance, is_used, label): - ci = self.cards.get(addr) - if ci is None: - ci = Factory.AddressItem() - ci.screen = self - ci.address = addr - self.cards[addr] = ci - - ci.memo = label - ci.amount = self.app.format_amount_and_units(balance) - request = self.app.wallet.get_payment_request(addr, self.app.electrum_config) - if is_used: - ci.status = _('Used') - elif request: - status, conf = self.app.wallet.get_request_status(addr) - requested_amount = request.get('amount') - # make sure that requested amount is > 0 - if status == PR_PAID: - s = _('Request paid') - elif status == PR_UNPAID: - s = _('Request pending') - elif status == PR_EXPIRED: - s = _('Request expired') - else: - s = '' - ci.status = s + ': ' + self.app.format_amount_and_units(requested_amount) - else: - ci.status = _('Funded') if balance>0 else _('Unused') - return ci - - - def update(self): - self.menu_actions = [('Receive', self.do_show), ('Details', self.do_view)] - wallet = self.app.wallet - _list = wallet.get_change_addresses() if self.screen.show_change else wallet.get_receiving_addresses() - search = self.screen.message - container = self.screen.ids.search_container - container.clear_widgets() - n = 0 - for address in _list: - label = wallet.labels.get(address, '') - balance = sum(wallet.get_addr_balance(address)) - is_used = wallet.is_used(address) - if self.screen.show_used == 1 and (balance or is_used): - continue - if self.screen.show_used == 2 and balance == 0: - continue - if self.screen.show_used == 3 and not is_used: - continue - card = self.get_card(address, balance, is_used, label) - if search and not self.ext_search(card, search): - continue - container.add_widget(card) - n += 1 - if not n: - msg = _('No address matching your search') - container.add_widget(EmptyLabel(text=msg)) - - def do_show(self, obj): - self.app.show_request(obj.address) - - def do_view(self, obj): - req = self.app.wallet.get_payment_request(obj.address, self.app.electrum_config) - if req: - c, u, x = self.app.wallet.get_addr_balance(obj.address) - balance = c + u + x - if balance > 0: - req['fund'] = balance - status = req.get('status') - amount = req.get('amount') - address = req['address'] - if amount: - status = req.get('status') - status = request_text[status] - else: - received_amount = self.app.wallet.get_addr_received(address) - status = self.app.format_amount_and_units(received_amount) - self.app.show_pr_details(req, status, False) - - else: - req = { 'address': obj.address, 'status' : obj.status } - status = obj.status - c, u, x = self.app.wallet.get_addr_balance(obj.address) - balance = c + u + x - if balance > 0: - req['fund'] = balance - self.app.show_addr_details(req, status) - - def do_delete(self, obj): - from .dialogs.question import Question - def cb(result): - if result: - self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config) - self.update() - d = Question(_('Delete request?'), cb) - d.open() - - def ext_search(self, card, search): - return card.memo.find(search) >= 0 or card.amount.find(search) >= 0 - + def do_save(self): + if self.save_request(): + self.app.show_info(_('Request was saved.')) class TabbedCarousel(Factory.TabbedPanel): @@ -642,7 +453,7 @@ class TabbedCarousel(Factory.TabbedPanel): self.current_tab.state = "normal" header.state = 'down' self._current_tab = header - # set the carousel to load the appropriate slide + # set the carousel to load the appropriate slide # saved in the screen attribute of the tab head slide = carousel.slides[header.slide] if carousel.current_slide != slide: diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index dee4c4bf..2be32eb2 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -19,56 +19,57 @@ icon: 'atlas://gui/kivy/theming/light/important' message: '' - value: 0 + is_mine: True amount: '--' - amount_color: '#FF6657' if self.value < 0 else '#2EA442' + action: _('Sent') if self.is_mine else _('Received') + amount_color: '#FF6657' if self.is_mine else '#2EA442' confirmations: 0 date: '' quote_text: '' - spacing: '9dp' Image: id: icon source: root.icon size_hint: None, 1 - width: self.height *.54 + allow_stretch: True + width: self.height*1.5 mipmap: True BoxLayout: orientation: 'vertical' Widget CardLabel: - text: root.date - font_size: '14sp' + text: + u'[color={color}]{s}[/color]'.format(s='<<' if root.is_mine else '>>', color=root.amount_color)\ + + ' ' + root.action + ' ' + (root.quote_text if app.is_fiat else root.amount) + font_size: '15sp' CardLabel: color: .699, .699, .699, 1 - font_size: '13sp' + font_size: '14sp' shorten: True - text: root.message + text: root.date + ' ' + root.message Widget - CardLabel: - halign: 'right' - font_size: '15sp' - size_hint: None, 1 - width: '110sp' - markup: True - font_name: font_light - text: - u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ - u'[color=#B2B3B3][size=13sp]{qt}[/size]'\ - u'[/color]'.format(amount_color=root.amount_color,\ - amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ - unit=app.base_unit) + HistoryScreen: name: 'history' content: content - ScrollView: - id: content - do_scroll_x: False - GridLayout - id: history_container - cols: 1 - size_hint: 1, None - height: self.minimum_height - padding: '12dp' - spacing: '2dp' + BoxLayout: + orientation: 'vertical' + Button: + background_color: 0, 0, 0, 0 + text: app.fiat_balance if app.is_fiat else app.balance + markup: True + color: .9, .9, .9, 1 + font_size: '30dp' + bold: True + size_hint: 1, 0.25 + on_release: app.is_fiat = not app.is_fiat if app.fx.is_enabled() else False + ScrollView: + id: content + do_scroll_x: False + size_hint: 1, 0.75 + GridLayout + id: history_container + cols: 1 + size_hint: 1, None + height: self.minimum_height diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv deleted file mode 100644 index 9621a8af..00000000 --- a/gui/kivy/uix/ui_screens/invoices.kv +++ /dev/null @@ -1,66 +0,0 @@ - - #color: .305, .309, .309, 1 - text_size: self.width, None - halign: 'left' - valign: 'top' - - - requestor: '' - memo: '' - amount: '' - status: '' - date: '' - icon: 'atlas://gui/kivy/theming/light/important' - Image: - id: icon - source: root.icon - size_hint: None, 1 - width: self.height *.54 - mipmap: True - BoxLayout: - spacing: '8dp' - height: '32dp' - orientation: 'vertical' - Widget - InvoicesLabel: - text: root.requestor - shorten: True - Widget - InvoicesLabel: - text: root.memo - color: .699, .699, .699, 1 - font_size: '13sp' - shorten: True - Widget - BoxLayout: - spacing: '8dp' - height: '32dp' - orientation: 'vertical' - Widget - InvoicesLabel: - text: root.amount - font_size: '15sp' - halign: 'right' - width: '110sp' - Widget - InvoicesLabel: - text: root.status - font_size: '13sp' - halign: 'right' - color: .699, .699, .699, 1 - Widget - - -InvoicesScreen: - name: 'invoices' - BoxLayout: - orientation: 'vertical' - spacing: '1dp' - ScrollView: - GridLayout: - cols: 1 - id: invoices_container - size_hint: 1, None - height: self.minimum_height - spacing: '2dp' - padding: '12dp' diff --git a/gui/kivy/uix/ui_screens/network.kv b/gui/kivy/uix/ui_screens/network.kv index 6a0fbcc6..fd5c01ad 100644 --- a/gui/kivy/uix/ui_screens/network.kv +++ b/gui/kivy/uix/ui_screens/network.kv @@ -11,7 +11,7 @@ Popup: height: self.minimum_height padding: '10dp' SettingsItem: - value: _("%d connections.")% app.num_nodes if app.num_nodes else _("Not connected") + value: _("{} connections.").format(app.num_nodes) if app.num_nodes else _("Not connected") title: _("Status") + ': ' + self.value description: _("Connections with Electrum-Zcash servers") action: lambda x: None @@ -46,7 +46,7 @@ Popup: CardSeparator SettingsItem: - title: _('Fork detected at block %d')%app.blockchain_checkpoint if app.num_chains>1 else _('No fork detected') + title: _('Fork detected at block {}').format(app.blockchain_checkpoint) if app.num_chains>1 else _('No fork detected') fork_description: (_('You are following branch') if app.auto_connect else _("Your server is on branch")) + ' ' + app.blockchain_name description: self.fork_description if app.num_chains>1 else _('Connected nodes are on the same chain') action: app.choose_blockchain_dialog diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index fd01d910..51a4ee59 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -70,7 +70,7 @@ ReceiveScreen: id: address_label text: s.address if s.address else _('Zcash Address') shorten: True - disabled: True + on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s)) CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color @@ -110,16 +110,31 @@ ReceiveScreen: BoxLayout: size_hint: 1, None height: '48dp' + IconButton: + icon: 'atlas://gui/kivy/theming/light/save' + size_hint: 0.6, None + height: '48dp' + on_release: s.parent.do_save() + Button: + text: _('Requests') + size_hint: 1, None + height: '48dp' + on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s)) Button: text: _('Copy') size_hint: 1, None height: '48dp' on_release: s.parent.do_copy() - Button: - text: _('Share') - size_hint: 1, None + IconButton: + icon: 'atlas://gui/kivy/theming/light/share' + size_hint: 0.6, None height: '48dp' on_release: s.parent.do_share() + BoxLayout: + size_hint: 1, None + height: '48dp' + Widget + size_hint: 2, 1 Button: text: _('New') size_hint: 1, None diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv deleted file mode 100644 index 1e39ec7d..00000000 --- a/gui/kivy/uix/ui_screens/requests.kv +++ /dev/null @@ -1,66 +0,0 @@ - - #color: .305, .309, .309, 1 - text_size: self.width, None - halign: 'left' - valign: 'top' - - - address: '' - memo: '' - amount: '' - status: '' - date: '' - icon: 'atlas://gui/kivy/theming/light/important' - Image: - id: icon - source: root.icon - size_hint: None, 1 - width: self.height *.54 - mipmap: True - BoxLayout: - spacing: '8dp' - height: '32dp' - orientation: 'vertical' - Widget - RequestLabel: - text: root.address - shorten: True - Widget - RequestLabel: - text: root.memo - color: .699, .699, .699, 1 - font_size: '13sp' - shorten: True - Widget - BoxLayout: - spacing: '8dp' - height: '32dp' - orientation: 'vertical' - Widget - RequestLabel: - text: root.amount - halign: 'right' - font_size: '15sp' - Widget - RequestLabel: - text: root.status - halign: 'right' - font_size: '13sp' - color: .699, .699, .699, 1 - Widget - - - -RequestsScreen: - name: 'requests' - BoxLayout: - orientation: 'vertical' - spacing: '1dp' - ScrollView: - GridLayout: - cols: 1 - id: requests_container - size_hint_y: None - height: self.minimum_height - spacing: '2dp' - padding: '12dp' diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index eb3fdb70..58334344 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -34,6 +34,7 @@ SendScreen: text: s.address if s.address else _('Recipient') shorten: True on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the recipient address using the Paste button, or use the camera to scan a QR code.'))) + #on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts')) CardSeparator: opacity: int(not root.is_pr) color: blue_bottom.foreground_color @@ -71,29 +72,51 @@ SendScreen: text: s.message if s.message else (_('No Description') if root.is_pr else _('Description')) disabled: root.is_pr on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) + CardSeparator: + opacity: int(not root.is_pr) + color: blue_bottom.foreground_color + BoxLayout: + size_hint: 1, None + height: blue_bottom.item_height + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/star_big_inactive' + opacity: 0.7 + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + BlueButton: + id: fee_e + default_text: _('Fee') + text: app.fee_status + on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) BoxLayout: size_hint: 1, None height: '48dp' + IconButton: + size_hint: 0.6, 1 + on_release: s.parent.do_save() + icon: 'atlas://gui/kivy/theming/light/save' + Button: + text: _('Invoices') + size_hint: 1, 1 + on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s)) + Button: + text: _('Paste') + on_release: s.parent.do_paste() IconButton: id: qr size_hint: 0.6, 1 on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr)) icon: 'atlas://gui/kivy/theming/light/camera' - Button: - text: _('Paste') - on_release: s.parent.do_paste() - Button: - text: _('Clear') - on_release: s.parent.do_clear() - IconButton: - size_hint: 0.6, 1 - on_release: s.parent.do_save() - icon: 'atlas://gui/kivy/theming/light/save' BoxLayout: size_hint: 1, None height: '48dp' + Button: + text: _('Clear') + on_release: s.parent.do_clear() Widget: - size_hint: 2, 1 + size_hint: 1, 1 Button: text: _('Pay') size_hint: 1, 1