From 30126c544b81bcb8ac6a510da9c9d42acf82ee28 Mon Sep 17 00:00:00 2001 From: qua-non Date: Tue, 18 Feb 2014 12:42:57 +0530 Subject: [PATCH] new combined tablet&mobile design on top of 1.9.x branch WIP --- gui/kivy/Makefile | 29 + gui/kivy/Readme.txt | 5 + gui/kivy/__init__.py | 87 ++ gui/kivy/carousel.py | 40 + gui/kivy/combobox.py | 93 ++ gui/kivy/console.py | 319 +++++ gui/kivy/dialog.py | 611 +++++++++ gui/kivy/drawer.py | 173 +++ gui/kivy/gridview.py | 203 +++ gui/kivy/installwizard.py | 224 ++++ gui/kivy/main.kv | 401 ++++++ gui/kivy/main_window.py | 294 +++++ gui/kivy/menus.py | 95 ++ gui/kivy/nfc_scanner/__init__.py | 43 + gui/kivy/nfc_scanner/scanner_android.py | 86 ++ gui/kivy/nfc_scanner/scanner_dummy.py | 37 + gui/kivy/qr_scanner/__init__.py | 105 ++ gui/kivy/qr_scanner/scanner_android.py | 354 ++++++ gui/kivy/qr_scanner/scanner_camera.py | 89 ++ gui/kivy/qrcodewidget.py | 179 +++ gui/kivy/screens.py | 1095 +++++++++++++++++ gui/kivy/statusbar.py | 7 + gui/kivy/textinput.py | 14 + gui/kivy/theming/light-0.png | Bin 0 -> 732948 bytes gui/kivy/theming/light-1.png | Bin 0 -> 81355 bytes gui/kivy/theming/light.atlas | 1 + gui/kivy/theming/light/action_bar.png | Bin 0 -> 1022 bytes gui/kivy/theming/light/action_group_dark.png | Bin 0 -> 380 bytes gui/kivy/theming/light/add_contact.png | Bin 0 -> 4154 bytes gui/kivy/theming/light/arrow_back.png | Bin 0 -> 3760 bytes gui/kivy/theming/light/blue_bg_round_rb.png | Bin 0 -> 242 bytes gui/kivy/theming/light/btn_create_account.png | Bin 0 -> 311 bytes .../theming/light/btn_create_act_disabled.png | Bin 0 -> 427 bytes gui/kivy/theming/light/btn_nfc.png | Bin 0 -> 362 bytes gui/kivy/theming/light/btn_send_address.png | Bin 0 -> 210 bytes gui/kivy/theming/light/btn_send_nfc.png | Bin 0 -> 209 bytes gui/kivy/theming/light/card.png | Bin 0 -> 561 bytes gui/kivy/theming/light/card_bottom.png | Bin 0 -> 383 bytes gui/kivy/theming/light/card_btn.png | Bin 0 -> 357 bytes gui/kivy/theming/light/card_top.png | Bin 0 -> 481 bytes .../theming/light/carousel_deselected.png | Bin 0 -> 1160 bytes gui/kivy/theming/light/carousel_selected.png | Bin 0 -> 1026 bytes gui/kivy/theming/light/clock1.png | Bin 0 -> 3089 bytes gui/kivy/theming/light/clock2.png | Bin 0 -> 4725 bytes gui/kivy/theming/light/clock3.png | Bin 0 -> 4467 bytes gui/kivy/theming/light/clock4.png | Bin 0 -> 3190 bytes gui/kivy/theming/light/clock5.png | Bin 0 -> 3524 bytes gui/kivy/theming/light/close.png | Bin 0 -> 8489 bytes gui/kivy/theming/light/closebutton.png | Bin 0 -> 3521 bytes gui/kivy/theming/light/confirmed.png | Bin 0 -> 5113 bytes gui/kivy/theming/light/contact.png | Bin 0 -> 838 bytes gui/kivy/theming/light/create_act_text.png | Bin 0 -> 330 bytes .../theming/light/create_act_text_active.png | Bin 0 -> 308 bytes gui/kivy/theming/light/dialog.png | Bin 0 -> 393 bytes gui/kivy/theming/light/electrum_icon640.png | Bin 0 -> 538383 bytes gui/kivy/theming/light/error.png | Bin 0 -> 13454 bytes gui/kivy/theming/light/gear.png | Bin 0 -> 17838 bytes gui/kivy/theming/light/globe.png | Bin 0 -> 5938 bytes gui/kivy/theming/light/icon_border.png | Bin 0 -> 514 bytes gui/kivy/theming/light/important.png | Bin 0 -> 6451 bytes gui/kivy/theming/light/info.png | Bin 0 -> 5793 bytes .../theming/light/lightblue_bg_round_lb.png | Bin 0 -> 244 bytes gui/kivy/theming/light/logo.png | Bin 0 -> 28124 bytes gui/kivy/theming/light/logo_atom_dull.png | Bin 0 -> 3902 bytes gui/kivy/theming/light/mail_icon.png | Bin 0 -> 4548 bytes gui/kivy/theming/light/manualentry.png | Bin 0 -> 4839 bytes gui/kivy/theming/light/network.png | Bin 0 -> 2412 bytes gui/kivy/theming/light/nfc.png | Bin 0 -> 2467 bytes gui/kivy/theming/light/nfc_clock.png | Bin 0 -> 14527 bytes gui/kivy/theming/light/nfc_phone.png | Bin 0 -> 2742 bytes gui/kivy/theming/light/nfc_stage_one.png | Bin 0 -> 12943 bytes gui/kivy/theming/light/paste_icon.png | Bin 0 -> 3637 bytes gui/kivy/theming/light/pen.png | Bin 0 -> 1641 bytes gui/kivy/theming/light/qrcode.png | Bin 0 -> 15453 bytes gui/kivy/theming/light/settings.png | Bin 0 -> 199 bytes gui/kivy/theming/light/shadow.png | Bin 0 -> 884 bytes gui/kivy/theming/light/shadow_right.png | Bin 0 -> 243 bytes gui/kivy/theming/light/star_big_inactive.png | Bin 0 -> 6843 bytes gui/kivy/theming/light/stepper_full.png | Bin 0 -> 14998 bytes gui/kivy/theming/light/stepper_left.png | Bin 0 -> 14388 bytes gui/kivy/theming/light/tab.png | Bin 0 -> 2837 bytes gui/kivy/theming/light/tab_btn.png | Bin 0 -> 3065 bytes gui/kivy/theming/light/tab_btn_disabled.png | Bin 0 -> 3065 bytes gui/kivy/theming/light/tab_btn_pressed.png | Bin 0 -> 3114 bytes gui/kivy/theming/light/tab_disabled.png | Bin 0 -> 2919 bytes gui/kivy/theming/light/tab_strip.png | Bin 0 -> 2874 bytes gui/kivy/theming/light/textinput_active.png | Bin 0 -> 488 bytes gui/kivy/theming/light/unconfirmed.png | Bin 0 -> 4700 bytes gui/kivy/theming/light/wallet.png | Bin 0 -> 824 bytes gui/kivy/theming/light/wallets.png | Bin 0 -> 222 bytes gui/kivy/theming/light/white_bg_round_top.png | Bin 0 -> 280 bytes gui/kivy/theming/loading.gif | Bin 0 -> 42478 bytes gui/kivy/theming/splash.png | Bin 0 -> 75024 bytes gui/kivy/utils.py | 2 + 94 files changed, 4586 insertions(+) create mode 100644 gui/kivy/Makefile create mode 100644 gui/kivy/Readme.txt create mode 100644 gui/kivy/__init__.py create mode 100644 gui/kivy/carousel.py create mode 100644 gui/kivy/combobox.py create mode 100644 gui/kivy/console.py create mode 100644 gui/kivy/dialog.py create mode 100644 gui/kivy/drawer.py create mode 100644 gui/kivy/gridview.py create mode 100644 gui/kivy/installwizard.py create mode 100644 gui/kivy/main.kv create mode 100644 gui/kivy/main_window.py create mode 100644 gui/kivy/menus.py create mode 100644 gui/kivy/nfc_scanner/__init__.py create mode 100644 gui/kivy/nfc_scanner/scanner_android.py create mode 100644 gui/kivy/nfc_scanner/scanner_dummy.py create mode 100644 gui/kivy/qr_scanner/__init__.py create mode 100644 gui/kivy/qr_scanner/scanner_android.py create mode 100644 gui/kivy/qr_scanner/scanner_camera.py create mode 100644 gui/kivy/qrcodewidget.py create mode 100644 gui/kivy/screens.py create mode 100644 gui/kivy/statusbar.py create mode 100644 gui/kivy/textinput.py create mode 100644 gui/kivy/theming/light-0.png create mode 100644 gui/kivy/theming/light-1.png create mode 100644 gui/kivy/theming/light.atlas create mode 100644 gui/kivy/theming/light/action_bar.png create mode 100644 gui/kivy/theming/light/action_group_dark.png create mode 100644 gui/kivy/theming/light/add_contact.png create mode 100644 gui/kivy/theming/light/arrow_back.png create mode 100644 gui/kivy/theming/light/blue_bg_round_rb.png create mode 100644 gui/kivy/theming/light/btn_create_account.png create mode 100644 gui/kivy/theming/light/btn_create_act_disabled.png create mode 100644 gui/kivy/theming/light/btn_nfc.png create mode 100644 gui/kivy/theming/light/btn_send_address.png create mode 100644 gui/kivy/theming/light/btn_send_nfc.png create mode 100644 gui/kivy/theming/light/card.png create mode 100644 gui/kivy/theming/light/card_bottom.png create mode 100644 gui/kivy/theming/light/card_btn.png create mode 100644 gui/kivy/theming/light/card_top.png create mode 100644 gui/kivy/theming/light/carousel_deselected.png create mode 100644 gui/kivy/theming/light/carousel_selected.png create mode 100644 gui/kivy/theming/light/clock1.png create mode 100644 gui/kivy/theming/light/clock2.png create mode 100644 gui/kivy/theming/light/clock3.png create mode 100644 gui/kivy/theming/light/clock4.png create mode 100644 gui/kivy/theming/light/clock5.png create mode 100644 gui/kivy/theming/light/close.png create mode 100644 gui/kivy/theming/light/closebutton.png create mode 100644 gui/kivy/theming/light/confirmed.png create mode 100644 gui/kivy/theming/light/contact.png create mode 100644 gui/kivy/theming/light/create_act_text.png create mode 100644 gui/kivy/theming/light/create_act_text_active.png create mode 100644 gui/kivy/theming/light/dialog.png create mode 100644 gui/kivy/theming/light/electrum_icon640.png create mode 100644 gui/kivy/theming/light/error.png create mode 100644 gui/kivy/theming/light/gear.png create mode 100644 gui/kivy/theming/light/globe.png create mode 100644 gui/kivy/theming/light/icon_border.png create mode 100644 gui/kivy/theming/light/important.png create mode 100644 gui/kivy/theming/light/info.png create mode 100644 gui/kivy/theming/light/lightblue_bg_round_lb.png create mode 100644 gui/kivy/theming/light/logo.png create mode 100644 gui/kivy/theming/light/logo_atom_dull.png create mode 100644 gui/kivy/theming/light/mail_icon.png create mode 100644 gui/kivy/theming/light/manualentry.png create mode 100644 gui/kivy/theming/light/network.png create mode 100644 gui/kivy/theming/light/nfc.png create mode 100644 gui/kivy/theming/light/nfc_clock.png create mode 100644 gui/kivy/theming/light/nfc_phone.png create mode 100644 gui/kivy/theming/light/nfc_stage_one.png create mode 100644 gui/kivy/theming/light/paste_icon.png create mode 100644 gui/kivy/theming/light/pen.png create mode 100644 gui/kivy/theming/light/qrcode.png create mode 100644 gui/kivy/theming/light/settings.png create mode 100644 gui/kivy/theming/light/shadow.png create mode 100644 gui/kivy/theming/light/shadow_right.png create mode 100644 gui/kivy/theming/light/star_big_inactive.png create mode 100644 gui/kivy/theming/light/stepper_full.png create mode 100644 gui/kivy/theming/light/stepper_left.png create mode 100644 gui/kivy/theming/light/tab.png create mode 100644 gui/kivy/theming/light/tab_btn.png create mode 100644 gui/kivy/theming/light/tab_btn_disabled.png create mode 100644 gui/kivy/theming/light/tab_btn_pressed.png create mode 100644 gui/kivy/theming/light/tab_disabled.png create mode 100644 gui/kivy/theming/light/tab_strip.png create mode 100644 gui/kivy/theming/light/textinput_active.png create mode 100644 gui/kivy/theming/light/unconfirmed.png create mode 100644 gui/kivy/theming/light/wallet.png create mode 100644 gui/kivy/theming/light/wallets.png create mode 100644 gui/kivy/theming/light/white_bg_round_top.png create mode 100644 gui/kivy/theming/loading.gif create mode 100644 gui/kivy/theming/splash.png create mode 100644 gui/kivy/utils.py diff --git a/gui/kivy/Makefile b/gui/kivy/Makefile new file mode 100644 index 00000000..fb11c319 --- /dev/null +++ b/gui/kivy/Makefile @@ -0,0 +1,29 @@ +PYTHON = python +# needs kivy installed or in PYTHONPATH + +.PHONY: theming apk clean + +theming: + $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png +apk: + # running pre build setup + @cp build/buildozer.spec ../../buildozer.spec + # get aes.py + @cd ../..; wget -4 https://raw.github.com/devrandom/slowaes/master/python/aes.py + # rename electrum to main.py + @mv ../../electrum ../../main.py + @-if [ ! -d "../../.buildozer" ];then \ + cd ../..; buildozer android debug;\ + cp -f gui/kivy/build/blacklist.txt .buildozer/android/platform/python-for-android/src/blacklist.txt;\ + rm -rf ./.buildozer/android/platform/python-for-android/dist;\ + fi + @-cd ../..; buildozer android debug deploy run + @make clean +clean: + # Cleaning up + # remove aes + @-rm ../../aes.py + # rename main.py to electrum + @-mv ../../main.py ../../electrum + # remove buildozer.spec + @-rm ../../buildozer.spec diff --git a/gui/kivy/Readme.txt b/gui/kivy/Readme.txt new file mode 100644 index 00000000..57746d25 --- /dev/null +++ b/gui/kivy/Readme.txt @@ -0,0 +1,5 @@ +Commands:: + + `make theming` to make a atlas out of a list of pngs + + `make apk` to make a apk diff --git a/gui/kivy/__init__.py b/gui/kivy/__init__.py new file mode 100644 index 00000000..cdaf3bcc --- /dev/null +++ b/gui/kivy/__init__.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2012 thomasv@gitorious +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Kivy GUI + +import sys +#, time, datetime, re, threading +#from electrum.i18n import _, set_language +#from electrum.util import print_error, print_msg, parse_url + +#:TODO: replace this with kivy's own plugin managment +#from electrum.plugins import run_hook +#import os.path, json, ast, traceback +#import shutil + +try: + sys.argv = [''] + import kivy +except ImportError: + # This error ideally shouldn't raised with pre-built packages + sys.exit("Error: Could not import kivy. Please install it using the" + \ + "instructions mentioned here `http://kivy.org/#download` .") + +# minimum required version for kivy +kivy.require('1.8.0') +from kivy.logger import Logger + +from electrum.bitcoin import MIN_RELAY_TX_FEE + +#:TODO main window +from main_window import ElectrumWindow +from electrum.plugins import init_plugins + +#:TODO find a equivalent method to register to `bitcoin:` uri +#: ref: http://stackoverflow.com/questions/30931/register-file-extensions-mime-types-in-linux +#class OpenFileEventFilter(object): +# def __init__(self, windows): +# self.windows = windows +# super(OpenFileEventFilter, self).__init__() +# +# def eventFilter(self, obj, event): +# if event.type() == QtCore.QEvent.FileOpen: +# if len(self.windows) >= 1: +# self.windows[0].set_url(event.url().toEncoded()) +# return True +# return False + + +class ElectrumGui: + + def __init__(self, config, network, app=None): + Logger.debug('ElectrumGUI: initialising') + self.network = network + self.config = config + + #:TODO + # implement kivy plugin mechanism that needs to be more extensible + # and integrated into the ui so can't be common with existing plugin + # base + #init_plugins(self) + + + def main(self, url): + ''' The main entry point of the kivy ux + :param url: 'bitcoin:' uri as mentioned in bip0021 + :type url: str + :ref: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki + ''' + + self.main_window = w = ElectrumWindow(config=self.config, + network=self.network) + w.run() diff --git a/gui/kivy/carousel.py b/gui/kivy/carousel.py new file mode 100644 index 00000000..ed8f2e00 --- /dev/null +++ b/gui/kivy/carousel.py @@ -0,0 +1,40 @@ +from kivy.uix.carousel import Carousel +from kivy.clock import Clock + +class CCarousel(Carousel): + + def on_touch_move(self, touch): + if self._get_uid('cavoid') in touch.ud: + return + if self._touch is not touch: + super(Carousel, self).on_touch_move(touch) + return self._get_uid() in touch.ud + if touch.grab_current is not self: + return True + ud = touch.ud[self._get_uid()] + direction = self.direction + if ud['mode'] == 'unknown': + if direction[0] in ('r', 'l'): + distance = abs(touch.ox - touch.x) + else: + distance = abs(touch.oy - touch.y) + if distance > self.scroll_distance: + Clock.unschedule(self._change_touch_mode) + ud['mode'] = 'scroll' + else: + diff = 0 + if direction[0] in ('r', 'l'): + diff = touch.dx + if direction[0] in ('t', 'b'): + diff = touch.dy + + self._offset += diff * 1.27 + return True + +if __name__ == "__main__": + from kivy.app import runTouchApp + from kivy.uix.button import Button + cc = CCarousel() + for i in range(10): + cc.add_widget(Button(text=str(i))) + runTouchApp(cc) \ No newline at end of file diff --git a/gui/kivy/combobox.py b/gui/kivy/combobox.py new file mode 100644 index 00000000..26e9f1f6 --- /dev/null +++ b/gui/kivy/combobox.py @@ -0,0 +1,93 @@ +''' +ComboBox +======= + +Based on Spinner +''' + +__all__ = ('ComboBox', 'ComboBoxOption') + +from kivy.properties import ListProperty, ObjectProperty, BooleanProperty +from kivy.uix.button import Button +from kivy.uix.dropdown import DropDown +from kivy.lang import Builder + + +Builder.load_string(''' +: + size_hint_y: None + height: 44 + +: + background_normal: 'atlas://data/images/defaulttheme/spinner' + background_down: 'atlas://data/images/defaulttheme/spinner_pressed' + on_key: + if self.items: x, y = zip(*self.items); self.text = y[x.index(args[1])] +''') + + +class ComboBoxOption(Button): + pass + + +class ComboBox(Button): + items = ListProperty() + key = ObjectProperty() + + option_cls = ObjectProperty(ComboBoxOption) + + dropdown_cls = ObjectProperty(DropDown) + + is_open = BooleanProperty(False) + + def __init__(self, **kwargs): + self._dropdown = None + super(ComboBox, self).__init__(**kwargs) + self.items_dict = dict(self.items) + self.bind( + on_release=self._toggle_dropdown, + dropdown_cls=self._build_dropdown, + option_cls=self._build_dropdown, + items=self._update_dropdown, + key=self._update_text) + self._build_dropdown() + self._update_text() + + def _update_text(self, *largs): + try: + self.text = self.items_dict[self.key] + except KeyError: + pass + + def _build_dropdown(self, *largs): + if self._dropdown: + self._dropdown.unbind(on_select=self._on_dropdown_select) + self._dropdown.dismiss() + self._dropdown = None + self._dropdown = self.dropdown_cls() + self._dropdown.bind(on_select=self._on_dropdown_select) + self._update_dropdown() + + def _update_dropdown(self, *largs): + dp = self._dropdown + cls = self.option_cls + dp.clear_widgets() + for key, value in self.items: + item = cls(text=value) + # extra attribute + item.key = key + item.bind(on_release=lambda option: dp.select(option.key)) + dp.add_widget(item) + + def _toggle_dropdown(self, *largs): + self.is_open = not self.is_open + + def _on_dropdown_select(self, instance, data, *largs): + self.key = data + self.is_open = False + + def on_is_open(self, instance, value): + if value: + self._dropdown.open(self) + else: + self._dropdown.dismiss() diff --git a/gui/kivy/console.py b/gui/kivy/console.py new file mode 100644 index 00000000..a553d979 --- /dev/null +++ b/gui/kivy/console.py @@ -0,0 +1,319 @@ +# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget + +import sys, os, re +import traceback, platform +from kivy.core.window import Keyboard +from kivy.uix.textinput import TextInput +from kivy.properties import StringProperty, ListProperty, DictProperty +from kivy.clock import Clock + +from electrum import util + + +if platform.system() == 'Windows': + MONOSPACE_FONT = 'Lucida Console' +elif platform.system() == 'Darwin': + MONOSPACE_FONT = 'Monaco' +else: + MONOSPACE_FONT = 'monospace' + + +class Console(TextInput): + + prompt = StringProperty('>> ') + '''String representing the Prompt message''' + + startup_message = StringProperty('') + '''Startup Message to be displayed in the Console if any''' + + history = ListProperty([]) + '''History of the console''' + + namespace = DictProperty({}) + '''Dict representing the current namespace of the console''' + + def __init__(self, **kwargs): + super(Console, self).__init__(**kwargs) + self.construct = [] + self.showMessage(self.startup_message) + self.updateNamespace({'run':self.run_script}) + self.set_json(False) + + def set_json(self, b): + self.is_json = b + + def run_script(self, filename): + with open(filename) as f: + script = f.read() + result = eval(script, self.namespace, self.namespace) + + def updateNamespace(self, namespace): + self.namespace.update(namespace) + + def showMessage(self, message): + self.appendPlainText(message) + self.newPrompt() + + def clear(self): + self.setPlainText('') + self.newPrompt() + + def newPrompt(self): + if self.construct: + prompt = '.' * len(self.prompt) + else: + prompt = self.prompt + + self.completions_pos = self.cursor_index() + self.completions_visible = False + + self.appendPlainText(prompt) + self.move_cursor_to('end') + + def getCommand(self): + curr_line = self._lines[-1] + curr_line = curr_line.rstrip() + curr_line = curr_line[len(self.prompt):] + return curr_line + + def setCommand(self, command): + if self.getCommand() == command: + return + curr_line = self._lines[-1] + last_pos = len(self.text) + self.select_text(last_pos - len(curr_line) + len(self.prompt), last_pos) + self.delete_selection() + self.insert_text(command) + + def show_completions(self, completions): + if self.completions_visible: + self.hide_completions() + + self.move_cursor_to(self.completions_pos) + + completions = map(lambda x: x.split('.')[-1], completions) + t = '\n' + ' '.join(completions) + if len(t) > 500: + t = t[:500] + '...' + self.insert_text(t) + self.completions_end = self.cursor_index() + + self.move_cursor_to('end') + self.completions_visible = True + + + def hide_completions(self): + if not self.completions_visible: + return + self.move_cursor_to(self.completions_pos) + l = self.completions_end - self.completions_pos + for x in range(l): + self.move_cursor_to('cursor_right') + self.do_backspace() + + self.move_cursor_to('end') + self.completions_visible = False + + def getConstruct(self, command): + if self.construct: + prev_command = self.construct[-1] + self.construct.append(command) + if not prev_command and not command: + ret_val = '\n'.join(self.construct) + self.construct = [] + return ret_val + else: + return '' + else: + if command and command[-1] == (':'): + self.construct.append(command) + return '' + else: + return command + + def getHistory(self): + return self.history + + def setHisory(self, history): + self.history = history + + def addToHistory(self, command): + if command and (not self.history or self.history[-1] != command): + self.history.append(command) + self.history_index = len(self.history) + + def getPrevHistoryEntry(self): + if self.history: + self.history_index = max(0, self.history_index - 1) + return self.history[self.history_index] + return '' + + def getNextHistoryEntry(self): + if self.history: + hist_len = len(self.history) + self.history_index = min(hist_len, self.history_index + 1) + if self.history_index < hist_len: + return self.history[self.history_index] + return '' + + def getCursorPosition(self): + return self.cursor[0] - len(self.prompt) + + def setCursorPosition(self, position): + self.cursor = (len(self.prompt) + position, self.cursor[1]) + + def register_command(self, c, func): + methods = { c: func} + self.updateNamespace(methods) + + + def runCommand(self): + command = self.getCommand() + self.addToHistory(command) + + command = self.getConstruct(command) + + if command: + tmp_stdout = sys.stdout + + class stdoutProxy(): + def __init__(self, write_func): + self.write_func = write_func + self.skip = False + + def flush(self): + pass + + def write(self, text): + if not self.skip: + stripped_text = text.rstrip('\n') + self.write_func(stripped_text) + self.skip = not self.skip + + if type(self.namespace.get(command)) == type(lambda:None): + self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command)) + self.newPrompt() + return + + sys.stdout = stdoutProxy(self.appendPlainText) + try: + try: + result = eval(command, self.namespace, self.namespace) + if result != None: + if self.is_json: + util.print_json(result) + else: + self.appendPlainText(repr(result)) + except SyntaxError: + exec command in self.namespace + except SystemExit: + pass + except: + traceback_lines = traceback.format_exc().split('\n') + # Remove traceback mentioning this file, and a linebreak + for i in (3,2,1,-1): + traceback_lines.pop(i) + self.appendPlainText('\n'.join(traceback_lines)) + sys.stdout = tmp_stdout + self.newPrompt() + self.set_json(False) + + def _keyboard_on_key_down(self, window, keycode, text, modifiers): + self._hide_cut_copy_paste() + is_osx = sys.platform == 'darwin' + # Keycodes on OSX: + ctrl, cmd = 64, 1024 + key, key_str = keycode + + if key == Keyboard.keycodes['tab']: + self.completions() + return + + self.hide_completions() + + if key == Keyboard.keycodes['enter']: + self.runCommand() + return + if key == Keyboard.keycodes['home']: + self.setCursorPosition(0) + return + if key == Keyboard.keycodes['pageup']: + return + elif key in (Keyboard.keycodes['left'], Keyboard.keycodes['backspace']): + if self.getCursorPosition() == 0: + return + elif key == Keyboard.keycodes['up']: + self.setCommand(self.getPrevHistoryEntry()) + return + elif key == Keyboard.keycodes['down']: + self.setCommand(self.getNextHistoryEntry()) + return + elif key == Keyboard.keycodes['l'] and modifiers == ['ctrl']: + self.clear() + + super(Console, self)._keyboard_on_key_down(window, keycode, text, modifiers) + + def completions(self): + cmd = self.getCommand() + lastword = re.split(' |\(|\)',cmd)[-1] + beginning = cmd[0:-len(lastword)] + + path = lastword.split('.') + ns = self.namespace.keys() + + if len(path) == 1: + ns = ns + prefix = '' + else: + obj = self.namespace.get(path[0]) + prefix = path[0] + '.' + ns = dir(obj) + + + completions = [] + for x in ns: + if x[0] == '_':continue + xx = prefix + x + if xx.startswith(lastword): + completions.append(xx) + completions.sort() + + if not completions: + self.hide_completions() + elif len(completions) == 1: + self.hide_completions() + self.setCommand(beginning + completions[0]) + else: + # find common prefix + p = os.path.commonprefix(completions) + if len(p)>len(lastword): + self.hide_completions() + self.setCommand(beginning + p) + else: + self.show_completions(completions) + + # NEW + def setPlainText(self, message): + """Equivalent to QT version""" + self.text = message + + # NEW + def appendPlainText(self, message): + """Equivalent to QT version""" + if len(self.text) == 0: + self.text = message + else: + if message: + self.text += '\n' + message + + # NEW + def move_cursor_to(self, pos): + """Aggregate all cursor moving functions""" + if isinstance(pos, int): + self.cursor = self.get_cursor_from_index(pos) + elif pos in ('end', 'pgend', 'pageend'): + def updt_cursor(*l): + self.cursor = self.get_cursor_from_index(self.text) + Clock.schedule_once(updt_cursor) + else: # cursor_home, cursor_end, ... (see docs) + self.do_cursor_movement(pos) diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py new file mode 100644 index 00000000..6dc9aec6 --- /dev/null +++ b/gui/kivy/dialog.py @@ -0,0 +1,611 @@ +from functools import partial + +from kivy.app import App +from kivy.factory import Factory +from kivy.uix.button import Button +from kivy.uix.bubble import Bubble +from kivy.uix.popup import Popup +from kivy.uix.widget import Widget +from kivy.uix.carousel import Carousel +from kivy.uix.tabbedpanel import TabbedPanelHeader +from kivy.properties import (NumericProperty, StringProperty, ListProperty, + ObjectProperty, AliasProperty, OptionProperty, + BooleanProperty) + +from kivy.animation import Animation +from kivy.core.window import Window +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp, inch + +#from electrum.bitcoin import is_valid +from electrum.i18n import _ + +# Delayed inits +QRScanner = None +NFCSCanner = None +ScreenAddress = None +decode_uri = None + +DEFAULT_PATH = '/tmp/' +app = App.get_running_app() + +class CarouselHeader(TabbedPanelHeader): + + slide = NumericProperty(0) + ''' indicates the link to carousels slide''' + +class AnimatedPopup(Popup): + + def open(self): + self.opacity = 0 + super(AnimatedPopup, self).open() + anim = Animation(opacity=1, d=.5).start(self) + + def dismiss(self): + def on_complete(*l): + super(AnimatedPopup, self).dismiss() + anim = Animation(opacity=0, d=.5) + anim.bind(on_complete=on_complete) + anim.start(self) + + +class CarouselDialog(AnimatedPopup): + ''' A Popup dialog with a CarouselIndicator used as the content. + ''' + + carousel_content = ObjectProperty(None) + + def open(self): + self.opacity = 0 + super(CarouselDialog, self).open() + anim = Animation(opacity=1, d=.5).start(self) + + def dismiss(self): + def on_complete(*l): + super(CarouselDialog, self).dismiss() + anim = Animation(opacity=0, d=.5) + anim.bind(on_complete=on_complete) + anim.start(self) + + def add_widget(self, widget, index=0): + if isinstance(widget, Carousel): + super(CarouselDialog, self).add_widget(widget, index) + return + if 'carousel_content' not in self.ids.keys(): + super(CarouselDialog, self).add_widget(widget) + return + self.carousel_content.add_widget(widget, index) + + + +class NFCTransactionDialog(AnimatedPopup): + + mode = OptionProperty('send', options=('send','receive')) + + scanner = ObjectProperty(None) + + def __init__(self, **kwargs): + # Delayed Init + global NFCSCanner + if NFCSCanner is None: + from electrum_gui.kivy.nfc_scanner import NFCScanner + self.scanner = NFCSCanner + + super(NFCTransactionDialog, self).__init__(**kwargs) + self.scanner.nfc_init() + self.scanner.bind() + + def on_parent(self, instance, value): + sctr = self.ids.sctr + if value: + def _cmp(*l): + anim = Animation(rotation=2, scale=1, opacity=1) + anim.start(sctr) + anim.bind(on_complete=_start) + + def _start(*l): + anim = Animation(rotation=350, scale=2, opacity=0) + anim.start(sctr) + anim.bind(on_complete=_cmp) + _start() + return + Animation.cancel_all(sctr) + + +class InfoBubble(Bubble): + '''Bubble to be used to display short Help Information''' + + message = StringProperty(_('Nothing set !')) + '''Message to be displayed defaults to "nothing set"''' + + icon = StringProperty('') + ''' Icon to be displayed along with the message defaults to '' + ''' + + fs = BooleanProperty(False) + ''' Show Bubble in half screen mode + ''' + + modal = BooleanProperty(False) + ''' Allow bubble to be hidden on touch. + ''' + + dim_background = BooleanProperty(False) + ''' Whether to draw a background on the windows behind the bubble + ''' + + def on_touch_down(self, touch): + if self.modal: + return + self.hide() + if self.collide_point(*touch.pos): + return True + + def show(self, pos, duration, width=None, modal=False): + '''Animate the bubble into position''' + self.modal = modal + if width: + self.width = width + Window.add_widget(self) + # wait for the bubble to adjust it's size according to text then animate + Clock.schedule_once(lambda dt: self._show(pos, duration)) + + def _show(self, pos, duration): + + def on_stop(*l): + if duration: + Clock.schedule_once(self.hide, duration + .5) + + self.opacity = 0 + arrow_pos = self.arrow_pos + if arrow_pos[0] in ('l', 'r'): + pos = pos[0], pos[1] - (self.height/2) + else: + pos = pos[0] - (self.width/2), pos[1] + + self.limit_to = Window + + anim = Animation(opacity=1, pos=pos, d=.32) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) + + + def hide(self, *dt): + ''' Auto fade out the Bubble + ''' + def on_stop(*l): + Window.remove_widget(self) + anim = Animation(opacity=0, d=.25) + anim.bind(on_complete=on_stop) + anim.cancel_all(self) + anim.start(self) + + +class InfoContent(Widget): + '''Abstract class to be used to add to content to InfoDialog''' + pass + + +class InfoButton(Button): + '''Button that is auto added to the dialog when setting `buttons:` + property. + ''' + pass + + +class EventsDialog(AnimatedPopup): + ''' Abstract Popup that provides the following events + .. events:: + `on_release` + `on_press` + ''' + + __events__ = ('on_release', 'on_press') + + def __init__(self, **kwargs): + super(EventsDialog, self).__init__(**kwargs) + self._on_release = kwargs.get('on_release') + Window.bind(size=self.on_size, + rotation=self.on_size) + self.on_size(Window, Window.size) + + def on_size(self, instance, value): + if app.ui_mode[0] == 'p': + self.size = Window.size + else: + #tablet + if app.orientation[0] == 'p': + #portrait + self.size = Window.size[0]/1.67, Window.size[1]/1.4 + else: + self.size = Window.size[0]/2.5, Window.size[1] + + def on_release(self, instance): + pass + + def on_press(self, instance): + pass + + def close(self): + self._on_release = None + self.dismiss() + + +class InfoDialog(EventsDialog): + ''' A dialog box meant to display info along with buttons at the bottom + + ''' + + buttons = ListProperty([_('ok'), _('cancel')]) + '''List of Buttons to be displayed at the bottom''' + + def __init__(self, **kwargs): + self._old_buttons = self.buttons + super(InfoDialog, self).__init__(**kwargs) + self.on_buttons(self, self.buttons) + + def on_buttons(self, instance, value): + if 'buttons_layout' not in self.ids.keys(): + return + if value == self._old_buttons: + return + blayout = self.ids.buttons_layout + blayout.clear_widgets() + for btn in value: + ib = InfoButton(text=btn) + ib.bind(on_press=partial(self.dispatch, 'on_press')) + ib.bind(on_release=partial(self.dispatch, 'on_release')) + blayout.add_widget(ib) + self._old_buttons = value + pass + + def add_widget(self, widget, index=0): + if isinstance(widget, InfoContent): + self.ids.info_content.add_widget(widget, index=index) + else: + super(InfoDialog, self).add_widget(widget) + + +class TakeInputDialog(InfoDialog): + ''' A simple Dialog for displaying a message and taking a input + using a Textinput + ''' + + text = StringProperty('Nothing set yet') + + readonly = BooleanProperty(False) + + +class EditLabelDialog(TakeInputDialog): + pass + + + +class ImportPrivateKeysDialog(TakeInputDialog): + pass + + + +class ShowMasterPublicKeyDialog(TakeInputDialog): + pass + + +class EditDescriptionDialog(TakeInputDialog): + + pass + + +class PrivateKeyDialog(InfoDialog): + + private_key = StringProperty('') + ''' private key to be displayed in the TextInput + ''' + + address = StringProperty('') + ''' address to be displayed in the dialog + ''' + + +class SignVerifyDialog(InfoDialog): + + address = StringProperty('') + '''current address being verified''' + + + +class MessageBox(InfoDialog): + + image = StringProperty('atlas://gui/kivy/theming/light/info') + '''path to image to be displayed on the left''' + + message = StringProperty('Empty Message') + '''Message to be displayed on the dialog''' + + def __init__(self, **kwargs): + super(MessageBox, self).__init__(**kwargs) + self.title = kwargs.get('title', _('Message')) + + +class MessageBoxExit(MessageBox): + + def __init__(self, **kwargs): + super(MessageBox, self).__init__(**kwargs) + self.title = kwargs.get('title', _('Exiting')) + +class MessageBoxError(MessageBox): + + def __init__(self, **kwargs): + super(MessageBox, self).__init__(**kwargs) + self.title = kwargs.get('title', _('Error')) + + +class WalletAddressesDialog(CarouselDialog): + + def __init__(self, **kwargs): + super(WalletAddressesDialog, self).__init__(**kwargs) + CarouselHeader = Factory.CarouselHeader + ch = CarouselHeader() + ch.slide = 0 # idx + + # delayed init + global ScreenAddress + if not ScreenAddress: + from electrum_gui.kivy.screens import ScreenAddress + slide = ScreenAddress() + + slide.tab=ch + + labels = app.wallet.labels + addresses = app.wallet.addresses() + _labels = {} + for address in addresses: + _labels[labels.get(address, address)] = address + + slide.labels = _labels + + self.add_widget(slide) + self.add_widget(ch) + Clock.schedule_once(lambda dt: self.delayed_init(slide)) + + def delayed_init(self, slide): + # add a tab for each wallet + # for wallet in wallets + slide.ids.btn_address.values = values = slide.labels.keys() + slide.ids.btn_address.text = values[0] + + + +class RecentActivityDialog(CarouselDialog): + + def send_payment(self, address): + tabs = app.root.main_screen.ids.tabs + screen_send = tabs.ids.screen_send + # remove self + self.dismiss() + # switch_to the send screen + tabs.ids.panel.switch_to(tabs.ids.tab_send) + # populate + screen_send.ids.payto_e.text = address + + def populate_inputs_outputs(self, app, tx_hash): + if tx_hash: + tx = app.wallet.transactions.get(tx_hash) + self.ids.list_outputs.content_adapter.data = \ + [(address, app.gui.main_gui.format_amount(value))\ + for address, value in tx.outputs] + self.ids.list_inputs.content_adapter.data = \ + [(input['address'], input['prevout_hash'])\ + for input in tx.inputs] + + +class CreateAccountDialog(EventsDialog): + ''' Abstract dialog to be used as the base for all Create Account Dialogs + ''' + crcontent = ObjectProperty(None) + + def add_widget(self, widget, index=0): + if not self.crcontent: + super(CreateAccountDialog, self).add_widget(widget) + else: + self.crcontent.add_widget(widget, index=index) + + +class InitSeedDialog(CreateAccountDialog): + + seed_msg = StringProperty('') + '''Text to be displayed in the TextInput''' + + message = StringProperty('') + '''Message to be displayed under seed''' + + seed = ObjectProperty(None) + + def on_parent(self, instance, value): + if value: + stepper = self.ids.stepper + stepper.opacity = 1 + stepper.source = 'atlas://gui/kivy/theming/light/stepper_full' + self._back = _back = partial(self.ids.back.dispatch, 'on_release') + app.navigation_higherarchy.append(_back) + + def close(self): + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(InitSeedDialog, self).close() + +class CreateRestoreDialog(CreateAccountDialog): + ''' Initial Dialog for creating or restoring seed''' + + def on_parent(self, instance, value): + if value: + self.ids.but_close.disabled = True + self.ids.but_close.opacity = 0 + self._back = _back = partial(app.dispatch, 'on_back') + app.navigation_higherarchy.append(_back) + + def close(self): + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(CreateRestoreDialog, self).close() + + +class VerifySeedDialog(CreateAccountDialog): + + pass + +class RestoreSeedDialog(CreateAccountDialog): + + pass + +class NewContactDialog(Popup): + + qrscr = ObjectProperty(None) + _decoder = None + + def load_qr_scanner(self): + global QRScanner + if not QRScanner: + from electrum_gui.kivy.qr_scanner import QRScanner + qrscr = self.qrscr + if not qrscr: + self.qrscr = qrscr = QRScanner(opacity=0) + #pos=self.pos, size=self.size) + #self.bind(pos=qrscr.setter('pos'), + # size=qrscr.setter('size') + qrscr.bind(symbols=self.on_symbols) + bl = self.ids.bl + bl.clear_widgets() + bl.add_widget(qrscr) + qrscr.opacity = 1 + Animation(height=dp(280)).start(self) + Animation(opacity=1).start(self) + qrscr.start() + + def on_symbols(self, instance, value): + instance.stop() + self.remove_widget(instance) + self.ids.but_contact.dispatch('on_release') + global decode_uri + if not decode_uri: + from electrum_gui.kivy.qr_scanner import decode_uri + uri = decode_uri(value[0].data) + self.ids.ti.text = uri.get('address', 'empty') + self.ids.ti_lbl.text = uri.get('label', 'empty') + self.ids.ti_lbl.focus = True + + +class PasswordRequiredDialog(InfoDialog): + + pass + + +class ChangePasswordDialog(CreateAccountDialog): + + message = StringProperty(_('Empty Message')) + '''Message to be displayed.''' + + mode = OptionProperty('new', options=('new', 'confirm', 'create')) + ''' Defines the mode of the password dialog.''' + + def validate_new_password(self): + self.ids.confirm.dispatch('on_release') + + def on_parent(self, instance, value): + if value: + stepper = self.ids.stepper + stepper.opacity = 1 + stepper.source = 'atlas://gui/kivy/theming/light/stepper_left' + self._back = _back = partial(self.ids.back.dispatch, 'on_release') + app.navigation_higherarchy.append(_back) + + def close(self): + ids = self.ids + ids.ti_wallet_name.text = "" + ids.ti_wallet_name.focus = False + ids.ti_password.text = "" + ids.ti_password.focus = False + ids.ti_new_password.text = "" + ids.ti_new_password.focus = False + ids.ti_confirm_password.text = "" + ids.ti_confirm_password.focus = False + if self._back in app.navigation_higherarchy: + app.navigation_higherarchy.pop() + self._back = None + super(ChangePasswordDialog, self).close() + + + +class Dialog(Popup): + + content_padding = NumericProperty('2dp') + '''Padding for the content area of the dialog defaults to 2dp + ''' + + buttons_padding = NumericProperty('2dp') + '''Padding for the bottns area of the dialog defaults to 2dp + ''' + + buttons_height = NumericProperty('40dp') + '''Height to be used for the Buttons at the bottom + ''' + + def close(self): + self.dismiss() + + def add_content(self, widget, index=0): + self.ids.layout_content.add_widget(widget, index) + + def add_button(self, widget, index=0): + self.ids.layout_buttons.add_widget(widget, index) + + +class SaveDialog(Popup): + + filename = StringProperty('') + '''The default file name provided + ''' + + filters = ListProperty([]) + ''' list of files to be filtered and displayed defaults to allow all + ''' + + path = StringProperty(DEFAULT_PATH) + '''path to be loaded by default in this dialog + ''' + + file_chooser = ObjectProperty(None) + '''link to the file chooser object inside the dialog + ''' + + text_input = ObjectProperty(None) + ''' + ''' + + cancel_button = ObjectProperty(None) + ''' + ''' + + save_button = ObjectProperty(None) + ''' + ''' + + def close(self): + self.dismiss() + + +class LoadDialog(SaveDialog): + + def _get_load_btn(self): + return self.save_button + + load_button = AliasProperty(_get_load_btn, None, bind=('save_button', )) + '''Alias to the Save Button to be used as LoadButton + ''' + + def __init__(self, **kwargs): + super(LoadDialog, self).__init__(**kwargs) + self.load_button.text=_("Load") diff --git a/gui/kivy/drawer.py b/gui/kivy/drawer.py new file mode 100644 index 00000000..6fc15426 --- /dev/null +++ b/gui/kivy/drawer.py @@ -0,0 +1,173 @@ + +from kivy.uix.stencilview import StencilView +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import Image + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.properties import OptionProperty, NumericProperty, ObjectProperty + +# delayed import +app = None + + +class Drawer(StencilView): + + state = OptionProperty('closed', + options=('closed', 'open', 'opening', 'closing')) + '''This indicates the current state the drawer is in. + + :attr:`state` is a `OptionProperty` defaults to `closed`. Can be one of + `closed`, `open`, `opening`, `closing`. + ''' + + scroll_timeout = NumericProperty(200) + '''Timeout allowed to trigger the :data:`scroll_distance`, + in milliseconds. If the user has not moved :data:`scroll_distance` + within the timeout, the scrolling will be disabled and the touch event + will go to the children. + + :data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` + and defaults to 200 (milliseconds) + ''' + + scroll_distance = NumericProperty('4dp') + '''Distance to move before scrolling the :class:`Drawer` in pixels. + As soon as the distance has been traveled, the :class:`Drawer` will + start to scroll, and no touch event will go to children. + It is advisable that you base this value on the dpi of your target + device's screen. + + :data:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` + and defaults to 20dp. + ''' + + drag_area = NumericProperty(.1) + '''The percentage of area on the left edge that triggers the opening of + the drawer. from 0-1 + + :attr:`drag_area` is a `NumericProperty` defaults to 2 + ''' + + _hidden_widget = ObjectProperty(None) + _overlay_widget = ObjectProperty(None) + + def __init__(self, **kwargs): + super(Drawer, self).__init__(**kwargs) + self.bind(pos=self._do_layout, + size=self._do_layout, + children=self._do_layout) + + def _do_layout(self, instance, value): + if not self._hidden_widget or not self._overlay_widget: + return + self._overlay_widget.height = self._hidden_widget.height =\ + self.height + + def on_touch_down(self, touch): + if self.disabled: + return + + global app + if not app: + from kivy.app import App + app = App.get_running_app() + + # skip on tablet mode + if app.ui_mode[0] == 't': + return super(Drawer, self).on_touch_down(touch) + + touch.ud['send_touch_down'] = False + drag_area = ((self.width * self.drag_area) + if self.state[0] == 'c' else + self._hidden_widget.width) + if touch.x > drag_area: + return super(Drawer, self).on_touch_down(touch) + self._touch = touch + Clock.schedule_once(self._change_touch_mode, + self.scroll_timeout/1000.) + touch.ud['in_drag_area'] = True + touch.ud['send_touch_down'] = True + return + + def on_touch_move(self, touch): + global app + if not app: + from kivy.app import App + app = App.get_running_app() + # skip on tablet mode + if app.ui_mode[0] == 't': + return super(Drawer, self).on_touch_move(touch) + + if not touch.ud.get('in_drag_area', None): + return super(Drawer, self).on_touch_move(touch) + + self._overlay_widget.x = min(self._hidden_widget.width, + max(self._overlay_widget.x + touch.dx*2, 0)) + if abs(touch.x - touch.ox) < self.scroll_distance: + return + touch.ud['send_touch_down'] = False + Clock.unschedule(self._change_touch_mode) + self._touch = None + self.state = 'opening' if touch.dx > 0 else 'closing' + touch.ox = touch.x + return + + def _change_touch_mode(self, *args): + if not self._touch: + return + touch = self._touch + touch.ud['in_drag_area'] = False + touch.ud['send_touch_down'] = False + self._touch = None + super(Drawer, self).on_touch_down(touch) + return + + def on_touch_up(self, touch): + # skip on tablet mode + if app.ui_mode[0] == 't': + return super(Drawer, self).on_touch_down(touch) + + if touch.ud.get('send_touch_down', None): + Clock.unschedule(self._change_touch_mode) + Clock.schedule_once( + lambda dt: super(Drawer, self).on_touch_down(touch), -1) + if touch.ud.get('in_drag_area', None): + touch.ud['in_drag_area'] = False + Animation.cancel_all(self._overlay_widget) + anim = Animation(x=self._hidden_widget.width + if self.state[0] == 'o' else 0, + d=.1, t='linear') + anim.bind(on_complete = self._complete_drawer_animation) + anim.start(self._overlay_widget) + Clock.schedule_once( + lambda dt: super(Drawer, self).on_touch_up(touch), 0) + + def _complete_drawer_animation(self, *args): + self.state = 'open' if self.state[0] == 'o' else 'closed' + + def add_widget(self, widget, index=1): + if not widget: + return + children = self.children + len_children = len(children) + if len_children == 2: + Logger.debug('Drawer: No more than two widgets allowed') + return + + super(Drawer, self).add_widget(widget) + if len_children == 0: + # first widget add it to the hidden/drawer section + self._hidden_widget = widget + return + # Second Widget + self._overlay_widget = widget + + def remove_widget(self, widget): + super(Drawer, self).remove_widget(self) + if widget == self._hidden_widget: + self._hidden_widget = None + return + if widget == self._overlay_widget: + self._overlay_widget = None + return \ No newline at end of file diff --git a/gui/kivy/gridview.py b/gui/kivy/gridview.py new file mode 100644 index 00000000..567177bc --- /dev/null +++ b/gui/kivy/gridview.py @@ -0,0 +1,203 @@ +from kivy.uix.boxlayout import BoxLayout +from kivy.adapters.dictadapter import DictAdapter +from kivy.adapters.listadapter import ListAdapter +from kivy.properties import ObjectProperty, ListProperty, AliasProperty +from kivy.uix.listview import (ListItemButton, ListItemLabel, CompositeListItem, + ListView) +from kivy.lang import Builder +from kivy.metrics import dp, sp + +Builder.load_string(''' + + header_view: header_view + content_view: content_view + BoxLayout: + orientation: 'vertical' + padding: '0dp', '2dp' + BoxLayout: + id: header_box + orientation: 'vertical' + size_hint: 1, None + height: '30dp' + ListView: + id: header_view + BoxLayout: + id: content_box + orientation: 'vertical' + ListView: + id: content_view + +<-HorizVertGrid> + header_view: header_view + content_view: content_view + ScrollView: + id: scrl + do_scroll_y: False + RelativeLayout: + size_hint_x: None + width: max(scrl.width, dp(sum(root.widths))) + BoxLayout: + orientation: 'vertical' + padding: '0dp', '2dp' + BoxLayout: + id: header_box + orientation: 'vertical' + size_hint: 1, None + height: '30dp' + ListView: + id: header_view + BoxLayout: + id: content_box + orientation: 'vertical' + ListView: + id: content_view + +''') + +class GridView(BoxLayout): + """Workaround solution for grid view by using 2 list view. + Sometimes the height of lines is shown properly.""" + + def _get_hd_adpt(self): + return self.ids.header_view.adapter + + header_adapter = AliasProperty(_get_hd_adpt, None) + ''' + ''' + + def _get_cnt_adpt(self): + return self.ids.content_view.adapter + + content_adapter = AliasProperty(_get_cnt_adpt, None) + ''' + ''' + + headers = ListProperty([]) + ''' + ''' + + widths = ListProperty([]) + ''' + ''' + + data = ListProperty([]) + ''' + ''' + + getter = ObjectProperty(lambda item, i: item[i]) + ''' + ''' + on_context_menu = ObjectProperty(None) + + def __init__(self, **kwargs): + super(GridView, self).__init__(**kwargs) + self._from_widths = False + #self.on_headers(self, self.headers) + + def on_widths(self, instance, value): + self._from_widths = True + self.on_headers(instance, self.headers) + self._from_widths = False + + def on_headers(self, instance, value): + if not self._from_widths: + return + if not (value and self.canvas and self.headers): + return + widths = self.widths + if len(self.widths) != len(value): + return + #if widths is not None: + # widths = ['%sdp' % i for i in widths] + + def generic_args_converter(row_index, + item, + is_header=True, + getter=self.getter): + cls_dicts = [] + _widths = self.widths + getter = self.getter + on_context_menu = self.on_context_menu + + for i, header in enumerate(self.headers): + kwargs = { + 'padding': ('2dp','2dp'), + 'halign': 'center', + 'valign': 'middle', + 'size_hint_y': None, + 'shorten': True, + 'height': '30dp', + 'text_size': (_widths[i], dp(30)), + 'text': getter(item, i), + } + + kwargs['font_size'] = '9sp' + if is_header: + kwargs['deselected_color'] = kwargs['selected_color'] =\ + [0, 1, 1, 1] + else: # this is content + kwargs['deselected_color'] = 1, 1, 1, 1 + if on_context_menu is not None: + kwargs['on_press'] = on_context_menu + + if widths is not None: # set width manually + kwargs['size_hint_x'] = None + kwargs['width'] = widths[i] + + cls_dicts.append({ + 'cls': ListItemButton, + 'kwargs': kwargs, + }) + + return { + 'id': item[-1], + 'size_hint_y': None, + 'height': '30dp', + 'cls_dicts': cls_dicts, + } + + def header_args_converter(row_index, item): + return generic_args_converter(row_index, item) + + def content_args_converter(row_index, item): + return generic_args_converter(row_index, item, is_header=False) + + + self.ids.header_view.adapter = ListAdapter(data=[self.headers], + args_converter=header_args_converter, + selection_mode='single', + allow_empty_selection=False, + cls=CompositeListItem) + + self.ids.content_view.adapter = ListAdapter(data=self.data, + args_converter=content_args_converter, + selection_mode='single', + allow_empty_selection=False, + cls=CompositeListItem) + self.content_adapter.bind_triggers_to_view(self.ids.content_view._trigger_reset_populate) + +class HorizVertGrid(GridView): + pass + + +if __name__ == "__main__": + from kivy.app import App + class MainApp(App): + + def build(self): + data = [] + for i in range(90): + data.append((str(i), str(i))) + self.data = data + return Builder.load_string(''' +BoxLayout: + orientation: 'vertical' + HorizVertGrid: + on_parent: if args[1]: self.content_adapter.data = app.data + headers:['Address', 'Previous output'] + widths: [400, 500] + +