electrum

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

commit 65ecbf990d712fd152ef6d1f8e5a3a7401839df1
parent d68042e76e3f194c9ec43524a7f79844a4703ce7
Author: ThomasV <thomasv@electrum.org>
Date:   Tue,  6 Oct 2015 09:59:29 +0200

kivy: split mainscreen.kv into dynamically loaded .kv files

Diffstat:
Mgui/kivy/main.kv | 452++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mgui/kivy/main_window.py | 288+++++++++++++++++++++++++++----------------------------------------------------
Mgui/kivy/theming/light-0.png | 0
Mgui/kivy/theming/light.atlas | 4++--
Mgui/kivy/theming/light/logo.png | 0
Dgui/kivy/uix/console.py | 319-------------------------------------------------------------------------------
Mgui/kivy/uix/dialogs/carousel_dialog.py | 5+++--
Mgui/kivy/uix/screens.py | 146++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Agui/kivy/uix/ui_screens/contacts.kv | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/history.kv | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/network.kv | 10++++++++++
Agui/kivy/uix/ui_screens/receive.kv | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/send.kv | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/settings.kv | 27+++++++++++++++++++++++++++
Agui/kivy/uix/ui_screens/wallet.kv | 14++++++++++++++
15 files changed, 1430 insertions(+), 566 deletions(-)

diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -1,4 +1,6 @@ #:import Window kivy.core.window.Window +#:import Factory kivy.factory.Factory +#:import _ electrum.i18n._ # Custom Global Widgets @@ -7,6 +9,79 @@ size_hint: 1, None height: self.minimum_height +<LightOptions@SpinnerOption> + font_size: '14sp' + border: 4, 4, 4, 4 + color: 0.439, 0.439, 0.439, .8 + background_normal: 'atlas://gui/kivy/theming/light/action_button_group' + background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' + size_hint_y: None + height: '48dp' + text_size: self.size[0] - dp(20), self.size[1] + halign: 'left' + valign: 'middle' + shorten: True + on_press: + ddn = self.parent.parent + Factory.Animation(opacity=0, d=.25).start(ddn) + +<OppositeDropDown@DropDown> + #auto_width: False + size_hint: None, None + size: self.container.minimum_size if self.container else (0, 0) + on_container: if args[1]: self.container.padding = '4dp', '4dp', '4dp', '4dp' + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + pos:self.pos + border: 20, 20, 20, 20 + source: 'atlas://gui/kivy/theming/light/dropdown_background' + size: self.size + +<OppositeSpinner@CSpinner> + dropdown_cls: Factory.OppositeDropDown + option_cls: Factory.LightOptions + border: 20, 20, 9, 9 + background_normal: 'atlas://gui/kivy/theming/light/action_group_dark' + background_down: self.background_normal + values: ('Copy to clipboard', 'Send Payment') + size_hint: None, 1 + width: '12dp' + on_release: + ddn = self._dropdown + ddn.opacity = 0 + Factory.Animation(opacity=1, d=.25).start(ddn) + + +<BlueSpinner@BoxLayout> + foreground_color: 1, 1, 1, 1 + spacing: '9dp' + text: '' + values: ('', ) + icon: '' + Image: + source: root.icon + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + OppositeSpinner: + color: root.foreground_color + background_normal: 'atlas://gui/kivy/theming/light/action_group_light' + markup: False + shorten: True + font_size: '16dp' + size_hint: 1, .7 + pos_hint: {'center_y': .5} + text: root.text + text_size: self.size + halign: 'left' + valign: 'middle' + on_text: + root.text = args[1] + values: root.values + + <IconButton@ButtonBehavior+Image> allow_stretch: True size_hint_x: None @@ -50,39 +125,8 @@ background_normal: 'atlas://gui/kivy/theming/light/textinput_active' -<CreateAccountButtonBlue@Button> - canvas.after: - Color - rgba: 1, 1, 1, 1 if self.disabled else 0 - Rectangle: - texture: self.texture - size: self.size - pos: self.pos - Color - rgba: .5, .5, .5, .5 if self.disabled else 0 - Rectangle: - texture: self.texture - size: self.size - pos: self.x - dp(1), self.y + dp(1) - border: 15, 5, 5, 5 - background_color: (1, 1, 1, 1) if self.disabled else (.203, .490, .741, 1 if self.state == 'normal' else .75) - size_hint: 1, None - height: '48sp' - text_size: self.size - halign: 'center' - valign: 'middle' - 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_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 +# Global Defaults ########################### <TextInput> on_focus: app._focused_widget = root @@ -127,11 +171,346 @@ size_hint: 1, 1 width: 0 if root.fs else (root.width - img.width) -StencilView: - manager: None + + +<WalletActionPrevious@ActionPrevious> + app_icon: 'atlas://gui/kivy/theming/light/' + ('wallets' if app.ui_mode[0] != 't' else 'tab_btn') + with_previous: False + size_hint: None, 1 + mipmap: True + on_release: app.root.children[0].toggle_drawer() + + +<SendReceiveToggle@BoxLayout> + padding: '5dp', '5dp' + size_hint: 1, None + height: '45dp' + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + border: 12, 12, 12, 12 + source: 'atlas://gui/kivy/theming/light/card' + size: self.width + dp(3), self.height + pos: self.x - dp(1.5), self.y + +<SendReceiveCardTop@GridLayout> + canvas.before: + BorderImage: + border: 9, 9, 9, 9 + source: 'atlas://gui/kivy/theming/light/card_top' + size: self.size + pos:self.pos + padding: '12dp', '22dp', '12dp', 0 + cols: 1 + size_hint: 1, None + height: '120dp' + spacing: '4dp' + +<SendReceiveBlueBottom@GridLayout> + canvas.before: + Color: + rgba: .238, .585, .878, 1 + BorderImage: + border: 9, 9, 9, 9 + source: 'atlas://gui/kivy/theming/light/card_bottom' + size: self.size + pos: self.pos + Color: + rgba: 1, 1, 1, 1 + + item_height: dp(42) + foreground_color: .843, .914, .972, 1 + cols: 1 + padding: '12dp', 0 + + +<SendToggle@ToggleButton> + source: '' + group: 'transfer_type' + markup: False + bold: True + border: 4, 4, 4, 4 + background_normal: self.background_down + color: + (.140, .140, .140, 1) if self.state == 'down' else (.796, .796, .796, 1) + canvas.after: + Color: + rgba: 1, 1, 1, 1 + Image: + source: root.source + color: root.color + size: '30dp', '30dp' + center_x: root.center_x - ((root.texture_size[0]/2)+(self.width/1.5)) + center_y: root.center_y + + +<CardSeparator@Widget> + size_hint: 1, None + height: dp(1) + color: .909, .909, .909, 1 + canvas: + Color: + rgba: root.color if root.color else (0, 0, 0, 0) + Rectangle: + size: self.size + pos: self.pos + +<AddressSelector@BlueSpinner> + icon: 'atlas://gui/kivy/theming/light/globe' + values: app.wallet.addresses() if app.wallet else [] + text: _("Select Your address") + + +<ElectrumScreen> + ScrollView: + do_scroll_x: False + do_scroll_y: False if root.fullscreen else (content.height > root.height - dp(16)) + AnchorLayout: + size_hint_y: None + height: root.height if root.fullscreen else max(root.height, content.height) + GridLayout: + id: content + cols: 1 + spacing: '8dp' + padding: '8dp' + size_hint: (1, 1) if root.fullscreen else (.8, None) + height: self.height if root.fullscreen else self.minimum_height + + +<TabbedCarousel> + carousel: carousel + do_default_tab: False + Carousel: + scroll_timeout: 190 + anim_type: 'out_quart' + min_move: .05 + anim_move_duration: .1 + anim_cancel_duration: .54 + scroll_distance: '10dp' + on_index: root.on_index(*args) + id: carousel + + +<CarouselIndicator@TabbedCarousel> + tab_pos: 'bottom_mid' + tab_height: '32dp' + tab_width: self.tab_height + #background_image: 'atlas://data/images/defaulttheme/action_item' + strip_border: 0, 0, 0, 0 + +<CloseButton@IconButton> + source: 'atlas://gui/kivy/theming/light/closebutton' + opacity: 1 if self.state == 'normal' else .75 + size_hint: None, None + size: '27dp', '27dp' + +<-CarouselDialog> + header_color: '#707070ff' + text_color: 0.701, 0.701, 0.701, 1 + title_size: '13sp' + title: '' + separator_color: 0.89, 0.89, 0.89, 1 + background: 'atlas://gui/kivy/theming/light/tab_btn' + carousel_content: carousel_content canvas.before: Color: + rgba: 0, 0, 0, .9 + Rectangle: + size: Window.size + pos: 0, 0 + Color: rgba: 1, 1, 1, 1 - Rectangle + BorderImage: + border: 12, 12, 12, 12 + source: 'atlas://gui/kivy/theming/light/dialog' + size: root.width, root.height - self.carousel_content.tab_height if self.carousel_content else 0 + pos: root.x, self.y + self.carousel_content.tab_height if self.carousel_content else 10 + BoxLayout: + orientation: 'vertical' + GridLayout: + cols: 1 + size_hint: 1, None + height: self.minimum_height + padding: 0, '7sp' + Label: + font_size: root.title_size + text: u'[color={}]{}[/color]'.format(root.header_color, root.title) + text_size: self.width, None + halign: 'left' + size_hint: 1, None + height: self.texture_size[1] + CardSeparator: + color: root.separator_color + height: root.separator_height + FloatLayout: + size_hint: None, None + size: 0, 0 + CloseButton: + id: but_close + top: root.top - dp(10) + right: root.right - dp(10) + on_release: root.dismiss() + CarouselIndicator: + id: carousel_content + + +<CleanHeader@TabbedPanelHeader> + border: 0, 0, 16, 0 + markup: False + text_size: self.size + halign: 'center' + valign: 'middle' + bold: True + font_size: '12.5sp' + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_disabled_normal: 'atlas://gui/kivy/theming/light/tab_btn_disabled' + background_down: 'atlas://gui/kivy/theming/light/tab_btn_pressed' + #canvas.before: + # Color: + # rgba: .6, .6, .6, .7 + # Rectangle: + # size: self.size + # pos: self.x + 1, self.y - 1 + # texture: self.texture + + +<ColoredLabel@Label>: + font_size: '48sp' + color: (.6, .6, .6, 1) + canvas.before: + Color: + rgb: (.9, .9, .9) + Rectangle: + pos: self.x + sp(2), self.y + sp(2) + size: self.width - sp(4), self.height - sp(4) + + + +<ScreenTabs@Screen> + TabbedCarousel: + id: panel + tab_height: '48dp' + default_tab: send_tab + strip_border: 0, 0, 0, 0 + + HistoryScreen: + id: history_screen + tab: history_tab + + SendScreen: + id: send_screen + tab: send_tab + + ReceiveScreen: + id: receive_screen + tab: receive_tab + + ContactsScreen: + id: contacts_screen + tab: contacts_tab + + CleanHeader: + id: history_tab + text: _('History') + slide: 0 + CleanHeader: + id: send_tab + text: _('Send') + slide: 1 + CleanHeader: + id: receive_tab + text: _('Receive') + slide: 2 + CleanHeader: + id: contacts_tab + text: _('Contacts') + slide: 3 + + +<ActionButton>: + border: 4, 0, 0, 0 + #background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' + +<OverflowButton@ActionButton> + text_size: dp(50), None + last: False + halign: 'left' + valign: 'middle' + overflow: None + #background_normal: + # 'atlas://gui/kivy/theming/light/' +\ + # ('action_button_group'\ + # if (self.inside_group and not self.last) else 'tab_btn') + + #on_press: + # ddn = self.overflow._dropdown + # Factory.Animation.cancel_all(ddn) + # anim = Factory.Animation(opacity=0, d=.25) + # anim.bind(on_complete=ddn.dismiss) + # anim.start(ddn) + +BoxLayout: + + orientation: 'vertical' + + canvas.before: + Color: + rgb: .6, .6, .6 + Rectangle: size: self.size - pos: self.pos- \ No newline at end of file + source: 'gui/kivy/data/background.png' + + ActionBar: + + ActionView: + id: av + + ActionPrevious: + app_icon: 'atlas://gui/kivy/theming/light/logo' + with_previous: False + on_release: app.on_back() + + ActionButton: + id: action_status + important: True + size_hint: 1, 1 + markup: True + mipmap: True + bold: True + markup: True + color: 1, 1, 1, 1 + text: + "[color=#777777]{}[/color]"\ + .format(app.status) + font_size: '22dp' + minimum_width: '1dp' + + ActionOverflow: + id: action_overflow + width: '60dp' + OverflowButton: + text: _('Network') + overflow: action_overflow + on_release: app.popup_dialog('network') + OverflowButton: + text: _('Wallet') + overflow: action_overflow + on_release: app.popup_dialog('wallet') + OverflowButton: + text: _('Preferences') + overflow: action_overflow + on_release: app.popup_dialog('settings') + + ScreenManager: + id: manager + #tabs: Factory.ScreenTabs() + ScreenTabs: + id: tabs + name: "tabs" + + #on_current_screen: + #spnr.text = args[1].name + #idx = app.screen_names.index(args[1].name) + #if idx > -1: app.hierarchy.append(idx) + #args diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -9,11 +9,6 @@ from electrum.contacts import Contacts from electrum import bitcoin from electrum.util import profiler, print_error -from kivy.config import Config -Config.set('modules', 'screen', 'droid2') -Config.set('graphics', 'width', '480') -Config.set('graphics', 'height', '840') - from kivy.app import App from kivy.core.window import Window from kivy.logger import Logger @@ -23,8 +18,7 @@ from kivy.properties import (OptionProperty, AliasProperty, ObjectProperty, from kivy.cache import Cache from kivy.clock import Clock from kivy.factory import Factory - -from electrum_gui.kivy.uix.drawer import Drawer +from kivy.metrics import inch, metrics # lazy imports for factory so that widgets can be used in kv Factory.register('InstallWizard', @@ -34,15 +28,31 @@ Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens') # delayed imports: for startup speed on android -notification = app = Decimal = ref = format_satoshis = Builder = None -inch = None +notification = app = ref = format_satoshis = Builder = None util = False -re = None + +from decimal import Decimal +import re # register widget cache for keeping memory down timeout to forever to cache # the data Cache.register('electrum_widgets', timeout=0) +from kivy.uix.screenmanager import Screen +from kivy.uix.tabbedpanel import TabbedPanel + +class ElectrumScreen(Screen): + fullscreen = BooleanProperty(False) + #def add_widget(self, *args): + # if 'content' in self.ids: + # return self.ids.content.add_widget(*args) + # return super(ElectrumScreen, self).add_widget(*args) + + +Factory.register('TabbedCarousel', module='electrum_gui.kivy.uix.screens') + + + class ElectrumWindow(App): def _get_bu(self): @@ -102,11 +112,9 @@ class ElectrumWindow(App): :attr:`electrum_config` is a `ObjectProperty`, defaults to None. ''' - status = StringProperty(_('Uninitialised')) - '''The status of the connection should show the balance when connected + status = StringProperty(_('Not Connected')) + balance = StringProperty('') - :attr:`status` is a `StringProperty` defaults to 'uninitialised' - ''' def _get_num_zeros(self): try: @@ -136,11 +144,8 @@ class ElectrumWindow(App): return int(p * x) - navigation_higherarchy = ListProperty([]) - '''This is a list of the current navigation higherarchy of the app used to - navigate using back button. - - :attr:`navigation_higherarchy` is s `ListProperty` defaults to [] + hierarchy = ListProperty([]) + '''used to navigate with the back button. ''' _orientation = OptionProperty('landscape', @@ -239,6 +244,8 @@ class ElectrumWindow(App): global Builder if not Builder: from kivy.lang import Builder + + return Builder.load_file('gui/kivy/main.kv') def _pause(self): @@ -252,6 +259,7 @@ class ElectrumWindow(App): def on_start(self): ''' This is the start point of the kivy ui ''' + Logger.info("dpi: {} {}".format(metrics.dpi, metrics.dpi_rounded)) win = Window win.bind(size=self.on_size, on_keyboard=self.on_keyboard) @@ -299,6 +307,14 @@ class ElectrumWindow(App): if self.wallet: self.wallet.stop_threads() + def on_back(self): + try: + self.hierarchy.pop()() + except IndexError: + # capture back button and pause app. + self._pause() + + def on_keyboard_height(self, window, height): win = window active_widg = win.children[0] @@ -351,6 +367,12 @@ class ElectrumWindow(App): self.init_ui() self.load_wallet(wallet) + def popup_dialog(self, name): + popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv') + popup.open() + + + @profiler def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic @@ -366,13 +388,15 @@ class ElectrumWindow(App): self.completions = [] # setup UX - self.screens = ['mainscreen',] + self.screens = {} #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('TabbedCarousel', + # module='electrum_gui.kivy.uix.screens') + Factory.register('ScreenDashboard', module='electrum_gui.kivy.uix.screens') #Factory.register('EffectWidget', @@ -386,17 +410,26 @@ class ElectrumWindow(App): # preload widgets. Remove this if you want to load the widgets on demand Cache.append('electrum_widgets', 'AnimatedPopup', Factory.AnimatedPopup()) - Cache.append('electrum_widgets', 'TabbedCarousel', Factory.TabbedCarousel()) + + #Cache.append('electrum_widgets', 'TabbedCarousel', Factory.TabbedCarousel()) + Cache.append('electrum_widgets', 'QRCodeWidget', Factory.QRCodeWidget()) Cache.append('electrum_widgets', 'CSpinner', Factory.CSpinner()) # load and focus the ui #Load mainscreen - 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 + + #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 + + self.root.manager = self.root.ids['manager'] + self.recent_activity_card = None + self.history_screen = None + self.contacts_screen = None + self.wallet_screen = None #TODO # load left_menu @@ -411,6 +444,7 @@ class ElectrumWindow(App): self.wallet = None + def create_quote_text(self, btc_balance, mode='normal'): ''' ''' @@ -473,10 +507,6 @@ class ElectrumWindow(App): if not self.wallet: return - global Decimal - if not Decimal: - from decimal import Decimal - unconfirmed = '' quote_text = '' @@ -487,34 +517,38 @@ class ElectrumWindow(App): server_height = self.network.get_server_height() server_lag = self.network.get_local_height() - server_height if not self.wallet.up_to_date or server_height == 0: - text = _("Synchronizing...") + self.status = _("Synchronizing...") elif server_lag > 1: - text = _("Server is lagging (%d blocks)"%server_lag) + self.status = _("Server lagging (%d blocks)"%server_lag) else: c, u, x = self.wallet.get_account_balance(self.current_account) text = self.format_amount(c) + self.balance = text if u: unconfirmed = " [%s unconfirmed]" %( self.format_amount(u, True).strip()) if x: unmatured = " [%s unmatured]"%(self.format_amount(x, True).strip()) + self.balance = text.strip() quote_text = self.create_quote_text(Decimal(c+u+x)/100000000, mode='symbol') or '' + self.status = self.balance else: - text = _("Not connected") - try: - status_card = self.root.main_screen.ids.tabs.ids.\ - screen_dashboard.ids.status_card - except AttributeError: - return - self.status = text.strip() + self.status = _("Not connected") + + return + + print self.root.manager.ids + + #try: + status_card = self.root.main_screen.ids.tabs.ids.\ + screen_dashboard.ids.status_card + #except AttributeError: + # return + status_card.quote_text = quote_text.strip() status_card.uncomfirmed = unconfirmed.strip() def format_amount(self, x, is_diff=False, whitespaces=False): - ''' - ''' - global format_satoshis - if not format_satoshis: - from electrum.util import format_satoshis + from electrum.util import format_satoshis return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces) @@ -532,116 +566,16 @@ class ElectrumWindow(App): self.update_history_tab() self.update_contacts_tab() - def parse_histories(self, items): - for item in items: - tx_hash, conf, value, timestamp, balance = 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).replace(',','.') - else: - v_str = '--' - - balance_str = self.format_amount(balance).replace(',','.') - - if tx_hash: - label, is_default_label = self.wallet.get_label(tx_hash) - else: - label = _('Pruned transaction outputs') - is_default_label = False - - yield (conf, icon, time_str, label, v_str, balance_str, tx_hash) @profiler def update_history_tab(self, see_all=False): - try: - history_card = self.root.main_screen.ids.tabs.ids.\ - screen_dashboard.ids.recent_activity_card - except AttributeError: - return - histories = self.parse_histories(reversed( - self.wallet.get_history(self.current_account))) - - # repopulate History Card - last_widget = history_card.ids.content.children[-1] - history_card.ids.content.clear_widgets() - history_add = history_card.ids.content.add_widget - history_add(last_widget) - RecentActivityItem = Factory.RecentActivityItem - 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 = 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) - + if self.history_screen: + self.history_screen.update(see_all) def update_contacts_tab(self): - contact_list = self.root.main_screen.ids.tabs.ids.\ - screen_contacts.ids.contact_container - #contact_list.clear_widgets() + if self.contacts_screen: + self.contacts_screen.update() - child = -1 - children = contact_list.children - - for key in sorted(self.contacts.keys()): - _type, address = self.contacts[key] - label = self.wallet.labels.get(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 do_send(self): app = App.get_running_app() @@ -650,9 +584,6 @@ class ElectrumWindow(App): label = unicode(scrn.message_e.text) 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 @@ -774,46 +705,23 @@ class ElectrumWindow(App): 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, switch=True): - ''' Load the appropriate screen as mentioned in the parameters. - ''' + #Logger.info("size: {} {}".format(width, height)) + #Logger.info('orientation: {}'.format(self._orientation)) + #Logger.info('ui_mode: {}'.format(self._ui_mode)) + + def load_screen(self, name, direction='left', manager=None): + screen = self.screens.get(name) + if screen is None: + screen = Builder.load_file('gui/kivy/uix/ui_screens/' + name + '.kv') + screen.name = name + self.screens[name] = screen manager = manager or self.root.manager - screen = Builder.load_file('gui/kivy/uix/ui_screens/'\ - + self.screens[index] + '.kv') - screen.name = self.screens[index] - if switch: - manager.switch_to(screen, direction=direction) - return screen - - def load_next_screen(self): - ''' - ''' - manager = root.manager - try: - self.load_screen(self.screens.index(manager.current_screen.name)+1, - manager=manager) - except IndexError: - self.load_screen() + manager.switch_to(screen, direction=direction) - def load_previous_screen(self): - ''' Load the previous screen from disk. - ''' - manager = root.manager - try: - self.load_screen(self.screens.index(manager.current_screen.name)-1, - direction='right', - manager=manager) - except IndexError: - pass + def load_history(self): + #Builder.load_file('gui/kivy/uix/ui_screens/history.kv') + print "load history", self.root.manager.ids.history def save_new_contact(self, address, label): address = unicode(address) diff --git a/gui/kivy/theming/light-0.png b/gui/kivy/theming/light-0.png Binary files differ. diff --git a/gui/kivy/theming/light.atlas b/gui/kivy/theming/light.atlas @@ -1 +1 @@ -{"light-0.png": {"closebutton": [641, 591, 60, 43], "card_top": [901, 792, 32, 16], "tab_btn_disabled": [867, 483, 32, 32], "tab_btn_pressed": [901, 483, 32, 32], "bit_logo": [589, 728, 44, 51], "globe": [686, 267, 72, 72], "btn_send_nfc": [955, 793, 18, 15], "shadow_right": [975, 803, 32, 5], "logo_atom_dull": [773, 517, 64, 64], "create_act_text_active": [995, 638, 22, 10], "action_group_light": [431, 344, 33, 48], "tab": [390, 715, 64, 64], "logo": [296, 211, 128, 128], "qrcode": [2, 194, 145, 145], "close": [834, 810, 88, 88], "btn_create_act_disabled": [985, 911, 32, 32], "white_bg_round_top": [834, 788, 31, 20], "card_bottom": [867, 792, 32, 16], "confirmed": [839, 636, 64, 64], "overflow_btn_dn": [989, 520, 16, 10], "carousel_deselected": [760, 275, 64, 64], "network": [692, 467, 48, 48], "blue_bg_round_rb": [935, 495, 31, 20], "dropdown_background": [765, 599, 29, 35], "action_bar": [795, 479, 36, 36], "pen": [905, 517, 64, 64], "overflow_background": [796, 599, 29, 35], "arrow_back": [971, 650, 50, 50], "clock3": [641, 636, 64, 64], "contact": [971, 532, 49, 49], "star_big_inactive": [426, 211, 128, 128], "lightblue_bg_round_lb": [968, 495, 31, 20], "manualentry": [149, 205, 145, 134], "stepper_restore_password": [247, 464, 392, 117], "tab_disabled": [752, 233, 96, 32], "mail_icon": [522, 725, 65, 54], "tab_strip": [850, 233, 96, 32], "tab_btn": [833, 483, 32, 32], "btn_create_account": [948, 233, 64, 32], "btn_send_address": [935, 793, 18, 15], "add_contact": [742, 472, 51, 43], "gear": [2, 33, 105, 159], "wallets": [703, 594, 60, 40], "stepper_left": [247, 583, 392, 117], "nfc_stage_one": [324, 900, 489, 122], "nfc_clock": [2, 460, 243, 240], "btn_nfc": [752, 219, 13, 12], "textinput_active": [718, 784, 114, 114], "clock2": [958, 275, 64, 64], "nfc_phone": [556, 213, 128, 126], "clock4": [707, 636, 64, 64], "paste_icon": [945, 945, 75, 77], "shadow": [324, 715, 64, 64], "carousel_selected": [826, 275, 64, 64], "card": [686, 216, 64, 49], "unconfirmed": [456, 715, 64, 64], "info": [707, 517, 64, 64], "electrum_icon640": [2, 702, 320, 320], "action_button_group": [971, 520, 16, 10], "action_group_dark": [396, 344, 33, 48], "nfc": [839, 517, 64, 64], "contact_avatar": [641, 466, 49, 49], "clock1": [892, 275, 64, 64], "create_act_text": [971, 638, 22, 10], "icon_border": [641, 517, 64, 64], "stepper_full": [324, 781, 392, 117], "card_btn": [945, 911, 38, 32], "wallet": [635, 735, 49, 44], "important": [924, 810, 88, 88], "dialog": [1001, 495, 18, 20], "error": [815, 908, 128, 114], "stepper_restore_seed": [2, 341, 392, 117], "contact_overlay": [905, 636, 64, 64], "settings": [396, 394, 54, 64], "clock5": [773, 636, 64, 64]}}- \ No newline at end of file +{"light-0.png": {"closebutton": [964, 855, 60, 43], "card_top": [964, 786, 32, 16], "tab_btn_disabled": [788, 483, 32, 32], "tab_btn_pressed": [856, 483, 32, 32], "bit_logo": [396, 407, 44, 51], "globe": [821, 628, 72, 72], "btn_send_nfc": [755, 290, 18, 15], "shadow_right": [895, 629, 32, 5], "logo_atom_dull": [654, 715, 64, 64], "action_group_light": [396, 357, 33, 48], "tab": [918, 715, 64, 64], "logo": [815, 906, 128, 116], "qrcode": [2, 194, 145, 145], "close": [641, 612, 88, 88], "btn_create_act_disabled": [754, 483, 32, 32], "white_bg_round_top": [956, 495, 31, 20], "card_bottom": [989, 499, 32, 16], "confirmed": [390, 715, 64, 64], "overflow_btn_dn": [995, 519, 16, 10], "carousel_deselected": [895, 636, 64, 64], "network": [556, 225, 48, 48], "blue_bg_round_rb": [890, 495, 31, 20], "dropdown_background": [659, 238, 29, 35], "action_bar": [945, 907, 36, 36], "pen": [786, 715, 64, 64], "overflow_background": [690, 238, 29, 35], "arrow_back": [971, 531, 50, 50], "clock3": [839, 517, 64, 64], "contact": [641, 466, 49, 49], "star_big_inactive": [296, 211, 128, 128], "lightblue_bg_round_lb": [923, 495, 31, 20], "manualentry": [149, 205, 145, 134], "stepper_restore_password": [247, 464, 392, 117], "tab_disabled": [755, 307, 96, 32], "mail_icon": [622, 285, 65, 54], "tab_strip": [853, 307, 96, 32], "tab_btn": [822, 483, 32, 32], "btn_create_account": [951, 307, 64, 32], "btn_send_address": [998, 787, 18, 15], "add_contact": [606, 230, 51, 43], "gear": [2, 33, 105, 159], "wallets": [692, 475, 60, 40], "stepper_left": [247, 583, 392, 117], "nfc_stage_one": [324, 900, 489, 122], "nfc_clock": [2, 460, 243, 240], "btn_nfc": [821, 614, 13, 12], "textinput_active": [848, 784, 114, 114], "clock2": [773, 517, 64, 64], "nfc_phone": [426, 213, 128, 126], "clock4": [905, 517, 64, 64], "paste_icon": [945, 945, 75, 77], "shadow": [852, 715, 64, 64], "carousel_selected": [641, 517, 64, 64], "card": [689, 290, 64, 49], "unconfirmed": [556, 275, 64, 64], "info": [588, 715, 64, 64], "electrum_icon640": [2, 702, 320, 320], "action_button_group": [1008, 719, 16, 10], "action_group_dark": [984, 731, 33, 48], "nfc": [720, 715, 64, 64], "contact_avatar": [964, 804, 49, 49], "clock1": [707, 517, 64, 64], "create_act_text_active": [984, 719, 22, 10], "icon_border": [522, 715, 64, 64], "stepper_full": [324, 781, 392, 117], "card_btn": [983, 911, 38, 32], "wallet": [442, 414, 49, 44], "important": [731, 612, 88, 88], "dialog": [641, 590, 18, 20], "error": [718, 784, 128, 114], "stepper_restore_seed": [2, 341, 392, 117], "contact_overlay": [456, 715, 64, 64], "settings": [961, 636, 54, 64], "create_act_text": [971, 519, 22, 10], "clock5": [324, 715, 64, 64]}}+ \ No newline at end of file diff --git a/gui/kivy/theming/light/logo.png b/gui/kivy/theming/light/logo.png Binary files differ. diff --git a/gui/kivy/uix/console.py b/gui/kivy/uix/console.py @@ -1,319 +0,0 @@ -# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget - -import sys, os, re -import traceback, platform -from kivy.core.window import Keyboard -from kivy.uix.textinput import TextInput -from kivy.properties import StringProperty, ListProperty, DictProperty -from kivy.clock import Clock - -from electrum import util - - -if platform.system() == 'Windows': - MONOSPACE_FONT = 'Lucida Console' -elif platform.system() == 'Darwin': - MONOSPACE_FONT = 'Monaco' -else: - MONOSPACE_FONT = 'monospace' - - -class Console(TextInput): - - prompt = StringProperty('>> ') - '''String representing the Prompt message''' - - startup_message = StringProperty('') - '''Startup Message to be displayed in the Console if any''' - - history = ListProperty([]) - '''History of the console''' - - namespace = DictProperty({}) - '''Dict representing the current namespace of the console''' - - def __init__(self, **kwargs): - super(Console, self).__init__(**kwargs) - self.construct = [] - self.showMessage(self.startup_message) - self.updateNamespace({'run':self.run_script}) - self.set_json(False) - - def set_json(self, b): - self.is_json = b - - def run_script(self, filename): - with open(filename) as f: - script = f.read() - result = eval(script, self.namespace, self.namespace) - - def updateNamespace(self, namespace): - self.namespace.update(namespace) - - def showMessage(self, message): - self.appendPlainText(message) - self.newPrompt() - - def clear(self): - self.setPlainText('') - self.newPrompt() - - def newPrompt(self): - if self.construct: - prompt = '.' * len(self.prompt) - else: - prompt = self.prompt - - self.completions_pos = self.cursor_index() - self.completions_visible = False - - self.appendPlainText(prompt) - self.move_cursor_to('end') - - def getCommand(self): - curr_line = self._lines[-1] - curr_line = curr_line.rstrip() - curr_line = curr_line[len(self.prompt):] - return curr_line - - def setCommand(self, command): - if self.getCommand() == command: - return - curr_line = self._lines[-1] - last_pos = len(self.text) - self.select_text(last_pos - len(curr_line) + len(self.prompt), last_pos) - self.delete_selection() - self.insert_text(command) - - def show_completions(self, completions): - if self.completions_visible: - self.hide_completions() - - self.move_cursor_to(self.completions_pos) - - completions = map(lambda x: x.split('.')[-1], completions) - t = '\n' + ' '.join(completions) - if len(t) > 500: - t = t[:500] + '...' - self.insert_text(t) - self.completions_end = self.cursor_index() - - self.move_cursor_to('end') - self.completions_visible = True - - - def hide_completions(self): - if not self.completions_visible: - return - self.move_cursor_to(self.completions_pos) - l = self.completions_end - self.completions_pos - for x in range(l): - self.move_cursor_to('cursor_right') - self.do_backspace() - - self.move_cursor_to('end') - self.completions_visible = False - - def getConstruct(self, command): - if self.construct: - prev_command = self.construct[-1] - self.construct.append(command) - if not prev_command and not command: - ret_val = '\n'.join(self.construct) - self.construct = [] - return ret_val - else: - return '' - else: - if command and command[-1] == (':'): - self.construct.append(command) - return '' - else: - return command - - def getHistory(self): - return self.history - - def setHisory(self, history): - self.history = history - - def addToHistory(self, command): - if command and (not self.history or self.history[-1] != command): - self.history.append(command) - self.history_index = len(self.history) - - def getPrevHistoryEntry(self): - if self.history: - self.history_index = max(0, self.history_index - 1) - return self.history[self.history_index] - return '' - - def getNextHistoryEntry(self): - if self.history: - hist_len = len(self.history) - self.history_index = min(hist_len, self.history_index + 1) - if self.history_index < hist_len: - return self.history[self.history_index] - return '' - - def getCursorPosition(self): - return self.cursor[0] - len(self.prompt) - - def setCursorPosition(self, position): - self.cursor = (len(self.prompt) + position, self.cursor[1]) - - def register_command(self, c, func): - methods = { c: func} - self.updateNamespace(methods) - - - def runCommand(self): - command = self.getCommand() - self.addToHistory(command) - - command = self.getConstruct(command) - - if command: - tmp_stdout = sys.stdout - - class stdoutProxy(): - def __init__(self, write_func): - self.write_func = write_func - self.skip = False - - def flush(self): - pass - - def write(self, text): - if not self.skip: - stripped_text = text.rstrip('\n') - self.write_func(stripped_text) - self.skip = not self.skip - - if type(self.namespace.get(command)) == type(lambda:None): - self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command)) - self.newPrompt() - return - - sys.stdout = stdoutProxy(self.appendPlainText) - try: - try: - result = eval(command, self.namespace, self.namespace) - if result != None: - if self.is_json: - util.print_json(result) - else: - self.appendPlainText(repr(result)) - except SyntaxError: - exec command in self.namespace - except SystemExit: - pass - except: - traceback_lines = traceback.format_exc().split('\n') - # Remove traceback mentioning this file, and a linebreak - for i in (3,2,1,-1): - traceback_lines.pop(i) - self.appendPlainText('\n'.join(traceback_lines)) - sys.stdout = tmp_stdout - self.newPrompt() - self.set_json(False) - - def _keyboard_on_key_down(self, window, keycode, text, modifiers): - self._hide_cut_copy_paste() - is_osx = sys.platform == 'darwin' - # Keycodes on OSX: - ctrl, cmd = 64, 1024 - key, key_str = keycode - - if key == Keyboard.keycodes['tab']: - self.completions() - return - - self.hide_completions() - - if key == Keyboard.keycodes['enter']: - self.runCommand() - return - if key == Keyboard.keycodes['home']: - self.setCursorPosition(0) - return - if key == Keyboard.keycodes['pageup']: - return - elif key in (Keyboard.keycodes['left'], Keyboard.keycodes['backspace']): - if self.getCursorPosition() == 0: - return - elif key == Keyboard.keycodes['up']: - self.setCommand(self.getPrevHistoryEntry()) - return - elif key == Keyboard.keycodes['down']: - self.setCommand(self.getNextHistoryEntry()) - return - elif key == Keyboard.keycodes['l'] and modifiers == ['ctrl']: - self.clear() - - super(Console, self)._keyboard_on_key_down(window, keycode, text, modifiers) - - def completions(self): - cmd = self.getCommand() - lastword = re.split(' |\(|\)',cmd)[-1] - beginning = cmd[0:-len(lastword)] - - path = lastword.split('.') - ns = self.namespace.keys() - - if len(path) == 1: - ns = ns - prefix = '' - else: - obj = self.namespace.get(path[0]) - prefix = path[0] + '.' - ns = dir(obj) - - - completions = [] - for x in ns: - if x[0] == '_':continue - xx = prefix + x - if xx.startswith(lastword): - completions.append(xx) - completions.sort() - - if not completions: - self.hide_completions() - elif len(completions) == 1: - self.hide_completions() - self.setCommand(beginning + completions[0]) - else: - # find common prefix - p = os.path.commonprefix(completions) - if len(p)>len(lastword): - self.hide_completions() - self.setCommand(beginning + p) - else: - self.show_completions(completions) - - # NEW - def setPlainText(self, message): - """Equivalent to QT version""" - self.text = message - - # NEW - def appendPlainText(self, message): - """Equivalent to QT version""" - if len(self.text) == 0: - self.text = message - else: - if message: - self.text += '\n' + message - - # NEW - def move_cursor_to(self, pos): - """Aggregate all cursor moving functions""" - if isinstance(pos, int): - self.cursor = self.get_cursor_from_index(pos) - elif pos in ('end', 'pgend', 'pageend'): - def updt_cursor(*l): - self.cursor = self.get_cursor_from_index(self.text) - Clock.schedule_once(updt_cursor) - else: # cursor_home, cursor_end, ... (see docs) - self.do_cursor_movement(pos) diff --git a/gui/kivy/uix/dialogs/carousel_dialog.py b/gui/kivy/uix/dialogs/carousel_dialog.py @@ -91,9 +91,10 @@ class RecentActivityDialog(CarouselDialog): ''' ''' def on_activate(self): + # animate to first slide - carousel = self.carousel_content.carousel - carousel.load_slide(carousel.slides[0]) + #carousel = self.carousel_content.carousel + #carousel.load_slide(carousel.slides[0]) item = self.item try: diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py @@ -7,6 +7,7 @@ from kivy.properties import (ObjectProperty, DictProperty, NumericProperty, from kivy.lang import Builder from kivy.factory import Factory +from electrum.i18n import _ # Delayed imports app = None @@ -17,6 +18,9 @@ class CScreen(Factory.Screen): __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave') action_view = ObjectProperty(None) + loaded = False + kvname = None + app = App.get_running_app() def _change_action_view(self): app = App.get_running_app() @@ -31,26 +35,43 @@ class CScreen(Factory.Screen): def on_enter(self): # FIXME: use a proper event don't use animation time of screen Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25) + pass + + def update(self): + pass def on_activate(self): - Clock.schedule_once(lambda dt: self._change_action_view()) + + if self.kvname and not self.loaded: + print "loading:" + self.kvname + self.screen = Builder.load_file('gui/kivy/uix/ui_screens/' + self.kvname + '.kv') + self.add_widget(self.screen) + self.loaded = True + self.update() + setattr(self.app, self.kvname + '_screen', self) + + #app.history_screen = screen + #app.recent_activity_card = screen.ids.recent_activity_card + #app.update_history_tab() + + #Clock.schedule_once(lambda dt: self._change_action_view()) def on_leave(self): self.dispatch('on_deactivate') def on_deactivate(self): - Clock.schedule_once(lambda dt: self._change_action_view()) + pass + #Clock.schedule_once(lambda dt: self._change_action_view()) -class ScreenDashboard(CScreen): - ''' Dashboard screen: Used to display the main dashboard. - ''' +class HistoryScreen(CScreen): tab = ObjectProperty(None) + kvname = 'history' def __init__(self, **kwargs): self.ra_dialog = None - super(ScreenDashboard, self).__init__(**kwargs) + super(HistoryScreen, self).__init__(**kwargs) def show_tx_details(self, item): ra_dialog = Cache.get('electrum_widgets', 'RecentActivityDialog') @@ -64,6 +85,83 @@ class ScreenDashboard(CScreen): ra_dialog.item = item ra_dialog.open() + def parse_histories(self, items): + for item in items: + tx_hash, conf, value, timestamp, balance = 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.app.format_amount(value, True).replace(',','.') + else: + v_str = '--' + + balance_str = self.app.format_amount(balance).replace(',','.') + + if tx_hash: + label, is_default_label = self.app.wallet.get_label(tx_hash) + else: + label = _('Pruned transaction outputs') + is_default_label = False + + yield (conf, icon, time_str, label, v_str, balance_str, tx_hash) + + def update(self, see_all=False): + + history_card = self.screen.ids.recent_activity_card + histories = self.parse_histories(reversed( + self.app.wallet.get_history(self.app.current_account))) + # repopulate History Card + last_widget = history_card.ids.content.children[-1] + history_card.ids.content.clear_widgets() + history_add = history_card.ids.content.add_widget + history_add(last_widget) + RecentActivityItem = Factory.RecentActivityItem + + from weakref import ref + from decimal import Decimal + + get_history_rate = self.app.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 = 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) + class ScreenAddress(CScreen): '''This is the dialog that shows a carousel of the currently available @@ -95,22 +193,19 @@ class ScreenPassword(Factory.Screen): pass -class MainScreen(Factory.Screen): - pass - - -class ScreenSend(CScreen): +class SendScreen(CScreen): + kvname = 'send' def set_qr_data(self, uri): self.ids.payto_e.text = uri.get('address', '') self.ids.message_e.text = uri.get('message', '') self.ids.amount_e.text = uri.get('amount', '') -class ScreenReceive(CScreen): - pass +class ReceiveScreen(CScreen): + kvname = 'receive' - -class ScreenContacts(CScreen): +class ContactsScreen(CScreen): + kvname = 'contacts' def add_new_contact(self): dlg = Cache.get('electrum_widgets', 'NewContactDialog') @@ -119,6 +214,25 @@ class ScreenContacts(CScreen): Cache.append('electrum_widgets', 'NewContactDialog', dlg) dlg.open() + def update(self): + contact_list = self.screen.ids.contact_container + contact_list.clear_widgets() + child = -1 + children = contact_list.children + for key in sorted(self.app.contacts.keys()): + _type, value = self.app.contacts[key] + child += 1 + try: + if children[child].label == value: + continue + except IndexError: + pass + ci = Factory.ContactItem() + ci.address = key + ci.label = value + contact_list.add_widget(ci) + + class CSpinner(Factory.Spinner): '''CustomDropDown that allows fading out the dropdown @@ -139,7 +253,7 @@ class CSpinner(Factory.Spinner): class TabbedCarousel(Factory.TabbedPanel): - '''Custom TabbedOanel using a carousel used in the Main Screen + '''Custom TabbedPanel using a carousel used in the Main Screen ''' carousel = ObjectProperty(None) diff --git a/gui/kivy/uix/ui_screens/contacts.kv b/gui/kivy/uix/ui_screens/contacts.kv @@ -0,0 +1,93 @@ +<ContactImage@Widget>: + source: 'atlas://gui/kivy/theming/light/contact_avatar' + size_hint_x: None + width: self.height + canvas: + Color: + rgba: 1, 1, 1, 1 + Ellipse: + source: root.source + size: self.width + dp(6), self.height + dp(6) + pos: self.x - dp(3), self.y - dp(3) + Ellipse: + source: 'atlas://gui/kivy/theming/light/contact_overlay' + size: self.width + dp(11), self.height + dp(11) + pos: self.x - dp(5.5), self.y - dp(5.5) + +<ContactLabel@Label> + color: .305, .309, .309, 1 + text_size: self.size + halign: 'left' + valign: 'middle' + +<ContactSeperator@Widget> + canvas.before: + Color: + rgba: .890, .890, .890, 1 + Rectangle: + size: self.size + pos: self.x, self.y + dp(9) + size_hint: None, None + size: '1dp', '22dp' + pos_hint_y: .5 + +<ContactTextInput@TextInput> + background_normal: self.background_down + background_down: 'atlas://gui/kivy/theming/light/tab_btn' + size_hint_y: None + height: '22dp' + +<ContactBitLogo@Image> + source: 'atlas://gui/kivy/theming/light/bit_logo' + size_hint_x: None + width: '32dp' + +<ContactItem@BoxLayout> + address: '' + label: '' + tx_amt: 0 + size_hint_y: None + height: '65dp' + padding: dp(12) + spacing: dp(5) + canvas.before: + #Color: + # rgba: 1, 1, 1, 1 + Rectangle: + size: self.size + pos: self.pos + ContactImage: + id: contact_image + Widget: + size_hint_x: None + width: '9dp' + ContactLabel: + id: contact_address + text: root.address + ContactSeperator: + ContactLabel: + text: root.label + #ContactBitLogo: + +ContactsScreen: + name: 'contacts' + on_activate: + if not self.action_view:\ + self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view + BoxLayout: + orientation: 'vertical' + spacing: '1dp' + ContactTextInput: + ScrollView: + canvas.before: + #Color: + # rgba: .8901, .8901, .8901, 1 + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols: 1 + id: contact_container + size_hint_y: None + height: self.minimum_height + spacing: '1dp' diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv @@ -0,0 +1,224 @@ +#:import _ electrum.i18n._ +#:import Factory kivy.factory.Factory +#:set font_light 'data/fonts/Roboto-Condensed.ttf' +#:set btc_symbol unichr(171) +#:set mbtc_symbol unichr(187) + + +<Card@GridLayout> + cols: 1 + padding: '12dp' , '22dp', '12dp' , '12dp' + spacing: '12dp' + size_hint: 1, None + height: max(100, self.minimum_height) + canvas.before: + Color: + rgba: 1, 1, 1, 1 + BorderImage: + border: 18, 18, 18, 18 + source: 'atlas://gui/kivy/theming/light/card' + size: self.size + pos: self.pos + +<CardLabel@Label> + color: 0.45, 0.45, 0.45, 1 + size_hint: 1, None + text: '' + text_size: self.width, None + height: self.texture_size[1] + halign: 'left' + valign: 'top' + +<CardButton@Button> + background_normal: 'atlas://gui/kivy/theming/light/card_btn' + bold: True + font_size: '10sp' + color: 0.699, 0.699, 0.699, 1 + size_hint: None, None + size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) + + +<CardItem@ButtonBehavior+GridLayout> + canvas.before: + Color: + rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 + Rectangle + size: self.size + pos: self.x, self.y + dp(5) + cols: 1 + padding: '2dp', '2dp' + spacing: '2dp' + size_hint: 1, None + height: self.minimum_height + +<RecentActivityItem@CardItem> + icon: 'atlas://gui/kivy/theming/light/important' + address:'no address set' + amount: '+0.00' + balance: 'xyz'# balance_after + amount_color: '#DB3627' if float(self.amount) < 0 else '#2EA442' + confirmations: 0 + date: '0/0/0' + quote_text: '.' + + spacing: '9dp' + on_release: + app.history_screen.show_tx_details(root) + BoxLayout: + size_hint: 1, None + spacing: '8dp' + height: '32dp' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + orientation: 'vertical' + Widget + CardLabel: + shorten: True + text: root.address + markup: False + text_size: self.size + CardLabel: + color: .699, .699, .699, 1 + text: root.date + font_size: '12sp' + Widget + CardLabel: + halign: 'right' + font_size: '13sp' + size_hint: None, 1 + width: '90sp' + markup: True + font_name: font_light + text: + u'[color={amount_color}]{sign}{symbol}{amount}[/color]\n'\ + u'[color=#B2B3B3][size=12sp]{qt}[/size]'\ + u'[/color]'.format(amount_color=root.amount_color,\ + amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ + symbol=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol) + CardSeparator + +<CardRecentActivity@Card> + BoxLayout: + size_hint: 1, None + height: lbl.height + CardLabel: + id: lbl + text: _('RECENT ACTIVITY') + CardButton: + id: btn_see_all + disabled: True if not self.opacity else False + text: _('SEE ALL') + font_size: '12sp' + on_release: app.update_history_tab(see_all=True) + GridLayout: + id: content + spacing: '7dp' + cols: 1 + size_hint: 1, None + height: self.minimum_height + CardSeparator + +<CardPaymentRequest@Card> + CardLabel: + text: _('PAYMENT REQUEST') + CardSeparator: + +<CardStatusInfo@Card> + padding: '12dp' , '12dp' + status: app.status + quote_text: '' + unconfirmed: '' + cols: 2 + FloatLayout + anchor_x: 'left' + size_hint: 1, None + height: '82dp' + IconButton: + mipmap: True + pos_hint: {'x': 0, 'center_y': .45} + color: .90, .90, .90, 1 + source: 'atlas://gui/kivy/theming/light/qrcode' + size_hint: None, .85 + width: self.height + on_release: + dlg = Cache.get('electrum_widgets', 'WalletAddressesDialog') + + if not dlg:\ + Factory.register('WalletAddressesDialog', module='electrum_gui.kivy.uix.dialogs.carousel_dialog');\ + dlg = Factory.WalletAddressesDialog();\ + Cache.append('electrum_widgets', 'WalletAddressesDialog', dlg) + + dlg.open() + CardLabel: + id: top_label + halign: 'right' + valign: 'top' + bold: True + pos_hint: {'top': 1, 'right': 1} + font_name: font_light + #balance_in_numbers: bool(ord(root.status[0]) not in range(ord('A'), ord('z'))) + balance_in_numbers: True + font_size: '50sp' if self.balance_in_numbers else '30sp' + text_size: self.width, root.height/2 + text: + u'[color=#4E4F4F]{}{}[/color]'\ + .format('' if not self.balance_in_numbers else\ + (btc_symbol if app.base_unit == 'BTC' else mbtc_symbol), root.status) + BoxLayout + pos_hint: {'y': 0, 'right': 1} + spacing: '5dp' + CardLabel + halign: 'right' + markup: True + font_size: '22dp' + font_name: font_light + text: u'[color=#c3c3c3]{}[/color]'.format(root.quote_text) + IconButton + color: .698, .698, .698, 1 + source: 'atlas://gui/kivy/theming/light/gear' + size_hint_y: None + height: '28dp' + opacity: .5 if self.state == 'down' else 1 + on_release: + dlg = Cache.get('electrum_widgets', 'CurrencySelectionDialog') + + if not dlg:\ + Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ + dlg = Factory.CurrencySelectionDialog();\ + Cache.append('electrum_widgets', 'CurrencySelectionDialog', dlg) + + dlg.open() + + + +HistoryScreen: + name: 'history' + content: content + ScrollView: + id: content + do_scroll_x: False + GridLayout + id: grid + cols: 1 #if root.width < root.height else 2 + size_hint: 1, None + height: self.minimum_height + padding: '12dp' + spacing: '12dp' + #GridLayout: + # cols: 1 + # size_hint: 1, None + # height: self.minimum_height + # spacing: '12dp' + # orientation: 'vertical' + # CardStatusInfo: + # id: status_card + # CardPaymentRequest: + # id: payment_card + CardRecentActivity: + id: recent_activity_card + diff --git a/gui/kivy/uix/ui_screens/network.kv b/gui/kivy/uix/ui_screens/network.kv @@ -0,0 +1,10 @@ +Popup: + id: network + title: _('Network') + BoxLayout: + + Button: + size_hint_y: None + height: '48dp' + text: 'close' + on_release: network.dismiss() diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv @@ -0,0 +1,171 @@ +#:import _ electrum.i18n._ +#:import Decimal decimal.Decimal +#:set btc_symbol unichr(171) +#:set mbtc_symbol unichr(187) +#:set font_light 'data/fonts/Roboto-Condensed.ttf' + + + +ReceiveScreen: + name: 'receive' + mode: 'qr' + on_mode: if args[1] == 'nfc': from electrum_gui.kivy.nfc_scanner import NFCScanner + action_view: Factory.ReceiveActionView() + + #on_activate: + # self.ids.toggle_qr.state = 'down' + # first_address = app.wallet.addresses()[0] + # qr.data = app.encode_uri(first_address, + # amount=amount_e.text, + # label=app.wallet.labels.get(first_address, first_address), + # message='') if app.wallet and app.wallet.addresses() else '' + #on_deactivate: + # self.ids.amount_e.focus = False + + BoxLayout + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + mode: 'qr' + orientation: 'vertical' + SendReceiveToggle + SendToggle: + id: toggle_qr + text: 'QR' + state: 'down' if root.mode == 'qr' else 'normal' + source: 'atlas://gui/kivy/theming/light/qrcode' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'qr': root.mode = 'nr' + root.mode = 'qr' + SendToggle: + id: toggle_nfc + text: 'NFC' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'nr' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + #size_hint: 1, None + #height: self.minimum_height + SendReceiveCardTop + height: '110dp' + BoxLayout: + size_hint: 1, None + height: '42dp' + rows: 1 + Label: + color: amount_e.foreground_color + bold: True + text_size: self.size + valign: 'bottom' + font_size: '22sp' + text: + u'[font={fnt}]{smbl}[/font]'.\ + format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + CardSeparator + BoxLayout: + size_hint: 1, None + height: '32dp' + spacing: '5dp' + Label: + color: lbl_quote.color + font_size: '12dp' + text: 'Ask to scan the QR below' + text_size: self.size + halign: 'left' + valign: 'middle' + Label: + id: lbl_quote + font_size: '12dp' + size_hint: .5, 1 + color: .761, .761, .761, 1 + text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' + text_size: self.size + halign: 'right' + valign: 'middle' + SendReceiveBlueBottom + id: blue_bottom + padding: '12dp', 0, '12dp', '12dp' + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + on_text: + if not args[1].startswith('Select'):\ + qr.data = app.encode_uri(args[1],\ + amount=amount_e.text,\ + label=app.wallet.labels.get(args[1], args[1]),\ + message='') + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + Widget: + size_hint_y: None + height: dp(10) + FloatLayout + id: bl + QRCodeWidget: + id: qr + size_hint: None, 1 + width: min(self.height, bl.width) + pos_hint: {'center': (.5, .5)} + on_touch_down: + if self.collide_point(*args[1].pos):\ + app.show_info_bubble(icon=self.ids.qrimage.texture, text='texture') + Button: + background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Goto next step') if app.wallet and app.wallet.seed else _('Create unsigned transaction') + size_hint_y: None + height: '38dp' + #disabled: True if wallet_selection.opacity == 0 else False + on_release: + message = 'sending {} {} to {}'.format(\ + app.base_unit, amount_e.text, payto_e.text) + app.gui.main_gui.do_send(self, message=message) + +<ReceiveActionView@ActionView> + WalletActionPrevious: + id: action_previous + width: '32dp' + ActionButton: + id: action_logo + important: True + size_hint: 1, 1 + markup: True + mipmap: True + bold: True + markup: True + color: 1, 1, 1, 1 + text: + "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ + .format(app.base_unit, app.status) + font_size: '22dp' + minimum_width: '1dp' + Butt_star: + id: but_star + on_release: + if self.state == 'down':\ + app.show_info_bubble(\ + text='[b]Expert mode on[/b]\n you can now select your address',\ + icon='atlas://gui/kivy/theming/light/star_big_inactive',\ + duration=1, arrow_pos='', width='250dp') diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv @@ -0,0 +1,243 @@ +#:import _ electrum.i18n._ +#:import Decimal decimal.Decimal + +#:import Factory kivy.factory.Factory +#:set btc_symbol unichr(171) +#:set mbtc_symbol unichr(187) +#:set font_light 'data/fonts/Roboto-Condensed.ttf' + +<SendActionView@ActionView> + foreground_color: (.466, .466, .466, 1) + color_active: (0.235, .588, .89, 1) + WalletActionPrevious: + id: action_previous + width: but_star.width + ActionButton: + id: action_logo + important: True + size_hint: 1, 1 + markup: True + mipmap: True + bold: True + markup: True + color: 1, 1, 1, 1 + text: + "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ + .format(app.base_unit, app.status) + font_size: '22dp' + minimum_width: '1dp' + Butt_star: + id: but_star + on_release: + if self.state == 'down':\ + app.show_info_bubble(\ + text='[b]Expert mode on[/b]\n you can now select your address',\ + icon='atlas://gui/kivy/theming/light/star_big_inactive',\ + duration=1, arrow_pos='', width='250dp') + + + + +<TextInputSendBlue@TextInput> + padding: '5dp' + size_hint: 1, None + height: '27dp' + pos_hint: {'center_y':.5} + multiline: False + hint_text_color: self.foreground_color + foreground_color: .843, .914, .972, 1 + background_color: 1, 1, 1, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_active: 'atlas://gui/kivy/theming/light/textinput_active' + + +SendScreen: + + mode: 'address' + name: 'send' + action_view: Factory.SendActionView() + #on_deactivate: + # self.ids.amount_e.focus = False + # self.ids.payto_e.focus = False + # self.ids.message_e.focus = False + BoxLayout + padding: '12dp', '12dp', '12dp', '12dp' + spacing: '12dp' + orientation: 'vertical' + mode: 'address' + SendReceiveToggle: + SendToggle: + id: toggle_address + text: 'ADDRESS' + group: 'send_type' + state: 'down' if root.mode == 'address' else 'normal' + source: 'atlas://gui/kivy/theming/light/globe' + background_down: 'atlas://gui/kivy/theming/light/btn_send_address' + on_release: + if root.mode == 'address': root.mode = 'fc' + root.mode = 'address' + SendToggle: + id: toggle_nfc + text: 'NFC' + group: 'send_type' + state: 'down' if root.mode == 'nfc' else 'normal' + source: 'atlas://gui/kivy/theming/light/nfc' + background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' + on_release: + if root.mode == 'nfc': root.mode = 'str' + root.mode = 'nfc' + GridLayout: + id: grid + cols: 1 + size_hint: 1, None + height: self.minimum_height + SendReceiveCardTop + id: card_address + BoxLayout + size_hint: 1, None + height: '42dp' + rows: 1 + Label + id: lbl_symbl + bold: True + color: amount_e.foreground_color + text_size: self.size + valign: 'bottom' + halign: 'left' + font_size: '22sp' + text: + u'[font={fnt}]{smbl}[/font]'.\ + format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) + size_hint_x: .25 + ELTextInput: + id: amount_e + input_type: 'number' + multiline: False + bold: True + font_size: '50sp' + foreground_color: .308, .308, .308, 1 + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + pos_hint: {'top': 1.5} + size_hint: .7, None + height: '67dp' + hint_text: 'Amount' + text: '0.0' + on_text_validate: payto_e.focus = True + CardSeparator + BoxLayout: + size_hint: 1, None + height: '42dp' + spacing: '5dp' + Label: + id: fee_e + color: .761, .761, .761, 1 + font_size: '12dp' + amt: app.format_amount(app.wallet.fee_per_kb(app.gui_object.config)) if app.wallet else 0 + text: + u'[b]{sign}{symbl}{amt}[/b] of fee'.\ + format(symbl=lbl_symbl.text,\ + sign='+' if self.amt > 0 else '-', amt=self.amt) + size_hint_x: None + width: self.texture_size[0] + halign: 'left' + valign: 'middle' + IconButton: + color: 0.694, 0.694, 0.694, 1 + source: 'atlas://gui/kivy/theming/light/gear' + pos_hint: {'center_y': .5} + size_hint: None, None + size: '22dp', '22dp' + on_release: + dlg = Cache.get('electrum_widgets', 'TransactionFeeDialog') + + if not dlg:\ + Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ + dlg = Factory.TransactionFeeDialog();\ + Cache.append('electrum_widgets', 'TransactionDialog', dlg) + + dlg.return_obj = fee_e + dlg.open() + Label: + font_size: '12dp' + color: fee_e.color + text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' + text_size: self.size + halign: 'right' + valign: 'middle' + SendReceiveBlueBottom: + id: blue_bottom + size_hint: 1, None + height: self.minimum_height + BoxLayout + size_hint: 1, None + height: blue_bottom.item_height + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/contact' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: payto_e + hint_text: "Enter Contact or adress" + on_text_validate: + Factory.Animation(opacity=1,\ + height=blue_bottom.item_height)\ + .start(message_selection) + message_e.focus = True + Widget: + size_hint: None, None + width: dp(2) + height: qr.height + pos_hint: {'center_y':.5} + canvas.after: + Rectangle: + size: self.size + pos: self.pos + IconButton: + id: qr + source: 'atlas://gui/kivy/theming/light/qrcode' + pos_hint: {'center_y': .5} + size_hint: None, None + size: '22dp', '22dp' + on_release: app.scan_qr(on_complete=root.set_qr_data) + CardSeparator + opacity: message_selection.opacity + color: blue_bottom.foreground_color + BoxLayout: + id: message_selection + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + spacing: '5dp' + Image: + source: 'atlas://gui/kivy/theming/light/pen' + size_hint: None, None + size: '22dp', '22dp' + pos_hint: {'center_y': .5} + TextInputSendBlue: + id: message_e + hint_text: 'Enter description here' + on_text_validate: + anim = Factory.Animation(opacity=1, height=blue_bottom.item_height) + anim.start(wallet_selection) + #anim.start(address_selection) + CardSeparator + opacity: address_selection.opacity + color: blue_bottom.foreground_color + AddressSelector: + id: address_selection + foreground_color: blue_bottom.foreground_color + opacity: 1 if app.expert_mode else 0 + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0 + Button: + #background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) + text: _('Send') + size_hint_y: None + height: '38dp' + disabled: False + on_release: app.do_send() + Widget + + diff --git a/gui/kivy/uix/ui_screens/settings.kv b/gui/kivy/uix/ui_screens/settings.kv @@ -0,0 +1,27 @@ +Popup: + id: settings + title: _('Settings') + BoxLayout: + + Button: + size_hint_y: None + height: '48dp' + text: 'Button normal' + + Button: + size_hint_y: None + height: '48dp' + text: 'Button down' + state: 'down' + + Button: + size_hint_y: None + height: '48dp' + text: 'Button disabled' + disabled: True + + Button: + size_hint_y: None + height: '48dp' + text: 'close' + on_release: settings.dismiss() diff --git a/gui/kivy/uix/ui_screens/wallet.kv b/gui/kivy/uix/ui_screens/wallet.kv @@ -0,0 +1,14 @@ + + +<WalletSelector@BlueSpinner> + icon: 'atlas://gui/kivy/theming/light/wallet' + values: ('default Wallet',) + text: _('Select your wallet') + + +ElectrumScreen: + + WalletSelector: + id: wallet_selection + size_hint: 1, None + height: blue_bottom.item_height if app.expert_mode else 0