Created
September 7, 2018 13:22
-
-
Save ilovetogetspamed/242746c5d46eb2fc8539fa3f75137e5b to your computer and use it in GitHub Desktop.
How to work with DataView objects from objects higher up in the widget chain.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from kivy.base import Builder | |
from kivy.uix.screenmanager import Screen | |
from kivy.app import App | |
from kivy.uix.boxlayout import BoxLayout | |
from kivy.logger import Logger | |
from kivy.uix.accordion import Accordion, AccordionItem | |
from kivy.uix.popup import Popup | |
from kivy.uix.label import Label | |
from kivy.uix.button import Button | |
from kivy.uix.textinput import TextInput | |
from kivy.uix.behaviors import FocusBehavior | |
from kivy.uix.recycleboxlayout import RecycleBoxLayout | |
from kivy.uix.recycleview import RecycleView | |
from kivy.uix.recycleview import RecycleDataModel | |
from kivy.uix.recycleview.views import RecycleDataViewBehavior | |
from kivy.uix.recycleview.layout import LayoutSelectionBehavior | |
from kivy.uix.gridlayout import GridLayout | |
from kivy.properties import BooleanProperty | |
from kivy.properties import ObjectProperty | |
from kivy.clock import Clock | |
from kivy.network.urlrequest import UrlRequest | |
from time import sleep | |
import re | |
import socket | |
import ipaddress | |
import json | |
import os | |
from jinja2 import Template | |
from subprocess import check_output, call, CalledProcessError | |
import StringIO | |
from kivy.core.text import LabelBase | |
LabelBase.register(name='FontAwesome', | |
fn_regular='../assets/fonts/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf') | |
LabelBase.register(name='LCDMono', | |
fn_regular='../assets/fonts/LCDMonoWinTT/LCDM2N__.TTF', | |
fn_bold='../assets/fonts/LCDMonoWinTT/LCDM2B__.TTF') | |
# todo DRY this up. | |
UNLOCK_GLYPH = u'\uF09C' | |
LOCK_GLYPH = u'\uF023' | |
kv = """ | |
<WiFi_DataView>: | |
signal_level: '' | |
ssid: '' | |
protected: '' | |
# # Draw a background to indicate selection | |
canvas.before: | |
Color: | |
# rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1) | |
rgba: (.0, 0.9, .1, .3) if self.selected else (0.4, 0.4, 0.4, 1) | |
Rectangle: | |
pos: self.pos | |
size: self.size | |
Line: | |
rectangle: self.x, self.y, self.width, self.height | |
padding: dp(4) | |
rows:1 | |
cols: 3 | |
Label: | |
id: signal_level_label | |
text: root.signal_level | |
Label: | |
id: ssid_label | |
text: root.ssid | |
Label: | |
id: protected_label | |
text: root.protected | |
font_name: "FontAwesome" | |
markup: True | |
<WiFi_RecycleView>: | |
viewclass: 'WiFi_DataView' | |
SelectableRecycleBoxLayout: | |
default_size: None, dp(56) | |
default_size_hint: 1, None | |
size_hint_y: None | |
height: self.minimum_height | |
orientation: 'vertical' | |
multiselect: True | |
touch_multiselect: False | |
<WiFiNetworkAccordionItem>: | |
id: wifi_accordion_item | |
min_space: | |
collapse: False | |
title: 'WiFi Network Configuration' | |
GridLayout: | |
padding: dp(10) | |
spacing: dp(5) | |
rows: 4 | |
canvas: | |
Color: | |
rgba: 0.4, 0.4, 0.4, 1 | |
Rectangle: | |
size: self.size | |
pos: self.pos | |
row_default_height: 40 | |
row_force_default: True | |
GridLayout: | |
cols: 1 | |
Button: | |
background_normal: '' | |
border: [16, 16, 16, 16] | |
text: "Search for WiFi networks" | |
font_size: 22 | |
color: .5,1,.5,1 | |
on_press: root.scan_wifi_networks() | |
GridLayout: | |
rows: 1 | |
cols: 2 | |
Label: | |
text: "SSID" | |
size_hint_x: None | |
width: 100 | |
TextInput: | |
id: wifi_ssid | |
multiline: False | |
hint_text: 'Your ESSID' | |
config_key: {'section': "network", 'option': "wifi_ssid"} | |
on_text_validate: root.write_to_configuration(self) | |
GridLayout: | |
rows: 1 | |
cols: 2 | |
Label: | |
text: "PSK" | |
size_hint_x: None | |
width: 100 | |
TextInput: | |
id: wifi_psk | |
multiline: False | |
password: True | |
hint_text: 'Your Wifi password' | |
config_key: {'section': "network", 'option': "wifi_psk"} | |
on_text_validate: root.write_to_configuration(self) | |
GridLayout: | |
rows: 1 | |
cols: 1 | |
row_default_height: 700 | |
WiFi_RecycleView: | |
id: rv | |
<Test>: | |
Accordion: | |
orientation: 'horizontal' | |
WiFiNetworkAccordionItem: | |
id: wifi_network_configuration | |
""" | |
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, | |
RecycleBoxLayout): | |
""" Adds selection and focus behaviour to the view. """ | |
keyboard_mode = 'managed' # to disable keyboard popup on row selection. | |
class WiFi_DataView(RecycleDataViewBehavior, GridLayout): | |
''' Add selection support to the GridLayout ''' | |
index = None | |
selected = BooleanProperty(False) | |
selectable = BooleanProperty(True) | |
missing = BooleanProperty(False) | |
def __init__(self, **kwargs): | |
super(WiFi_DataView, self).__init__(**kwargs) | |
self.app = App.get_running_app() | |
def refresh_view_attrs(self, rv, index, data): | |
""" Catch and handle the view changes """ | |
self.index = index | |
return super(WiFi_DataView, self).refresh_view_attrs( | |
rv, index, data) | |
def on_touch_down(self, touch): | |
''' Add selection on touch down ''' | |
if super(WiFi_DataView, self).on_touch_down(touch): | |
return True | |
if self.collide_point(*touch.pos) and self.selectable: | |
return self.parent.select_with_touch(self.index, touch) | |
def apply_selection(self, rv, index, is_selected): | |
''' Respond to the selection of items in the view. ''' | |
self.selected = is_selected # setting this turns on the row highlighting (see kv file) | |
if is_selected: | |
# update the SSID TextInput text. | |
self.parent.parent.parent.parent.children[2].children[0].text = self.ids.ssid_label.text | |
# set focus to the PSK TextInput widget. | |
self.parent.parent.parent.parent.children[1].children[0].focus = True | |
class WiFi_SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior, | |
RecycleBoxLayout): | |
""" Adds selection and focus behaviour to the view. """ | |
keyboard_mode = 'managed' # to disable keyboard popup on row selection. | |
class WiFi_RecycleView(RecycleView): | |
""" SSID RecycleView """ | |
def __init__(self, **kwargs): | |
super(WiFi_RecycleView, self).__init__(**kwargs) | |
class WiFiNetworkAccordionItem(AccordionItem): | |
def __init__(self, **kwargs): | |
super(WiFiNetworkAccordionItem, self).__init__(**kwargs) | |
self.app = App.get_running_app() | |
def scan_wifi_networks(self): | |
Logger.info('Configuration Screen: Finding WiFi networks.') | |
# Scan the WiFi network using built-in CLI tool | |
cells = [] | |
wifi_networks = check_output("sudo wifi scan", shell=True) | |
for network in StringIO.StringIO(wifi_networks): | |
cells.append(network.split()) | |
# Load data into GridView | |
self.ids.rv.data = [] # clear list view data | |
# update RecycleView values with new data. | |
if cells: | |
try: | |
self.ids.rv.data = [ | |
{'signal_level': cell[0], | |
'ssid': cell[1], | |
'protected': LOCK_GLYPH if cell[2] == 'protected' else UNLOCK_GLYPH | |
} for cell in cells] | |
except IndexError: | |
print 'Index Error Trapped! wifi scan returned bad data... cleaning up.' | |
self.ids.rv.data = [] # clear list view data | |
for cell in cells: | |
# print len(cell), cell | |
if len(cell) == 3: | |
self.ids.rv.data.append( | |
{ | |
'signal_level': cell[0], | |
'ssid': cell[1], | |
'protected': LOCK_GLYPH if cell[2] == 'protected' else UNLOCK_GLYPH | |
} | |
) | |
continue | |
elif len(cell) == 4: | |
self.ids.rv.data.append( | |
{ | |
'signal_level': cell[0], | |
'ssid': cell[1] + cell[2], | |
'protected': LOCK_GLYPH if cell[3] == 'protected' else UNLOCK_GLYPH | |
} | |
) | |
continue | |
elif len(cell) == 2: | |
self.ids.rv.data.append( | |
{ | |
'signal_level': cell[0], | |
'ssid': 'Hidden SSID', | |
'protected': LOCK_GLYPH if cell[1] == 'protected' else UNLOCK_GLYPH | |
} | |
) | |
else: | |
print 'bad data: ' + cell.__str__() | |
def textinput_on_focus(self, instance, keyboard): | |
if keyboard and keyboard.widget: | |
# keyboard.widget.scale = 0.9 | |
if keyboard.widget.top >= instance.get_top() - instance.height: | |
keyboard.widget.top = instance.get_top()\ | |
+ keyboard.widget.height\ | |
+ instance.height * 1.5 | |
# keyboard.widget.layout = './assets/keyboards/numeric.json' | |
def write_to_configuration(self, instance): | |
""" | |
write_to_configuration | |
:param instance: The control that is sending the write_to_configuration command. | |
:return: True or False depending on the acceptance of the instance.text | |
""" | |
Logger.info('Configuration Screen: write_to_configuration instance.text: {}'.format(instance.text)) | |
if instance.text == '': | |
return False | |
else: | |
# self.validate(instance.config_key, instance.text) | |
try: | |
self.app.config.get(instance.config_key['section'], instance.config_key['option']) | |
except Exception as e: | |
Logger.error('Configuration Screen: WifiNetworkAccordionItem Error: {}'.format(e.message)) | |
return False | |
if self.validate(instance.config_key, instance.text): | |
self.app.config.set(instance.config_key['section'], | |
instance.config_key['option'], instance.text) | |
self.app.config.write() | |
Logger.info('Configuration Screen: WifiNetworkAccordionItem: Wrote {} to [{}] {}'.format( | |
instance.text, instance.config_key['section'], instance.config_key['option'])) | |
# try to start WiFi | |
if instance.config_key['section'] == 'network' and instance.config_key['option'] == 'wifi_psk': | |
# Update wpa_supplicant/wpa_supplicant.conf | |
cmd = "wpa_passphrase {} {}".format(self.ids.wifi_ssid.text, self.ids.wifi_psk.text) | |
try: | |
cmd_result = check_output(cmd, shell=True) | |
new_ssid = str(cmd_result[cmd_result.find('ssid'):cmd_result.find('\n\t#psk')]).split('=')[1] | |
new_ssid = new_ssid.replace('"', '') | |
new_psk = cmd_result[cmd_result.rfind('psk'):cmd_result.rfind('\n}\n')].split('=')[1] | |
# print(new_ssid, new_psk) # debug | |
except CalledProcessError as e: | |
Logger.error('Configuration Screen: WifiNetworkAccordionItem Error: 821 {}'.format(e)) # todo popup? | |
return False | |
wpaTemplate = """ | |
network={ | |
ssid="WIFI-SSID" | |
proto=RSN | |
scan_ssid=1 | |
psk="WIFI-PSK" | |
key_mgmt=WPA-PSK | |
pairwise=CCMP | |
auth_alg=OPEN | |
priority=1 | |
} | |
""" | |
# see "man wpa_supplicant.conf" for valid settings. | |
wifiText = wpaTemplate\ | |
.replace("WIFI-SSID", new_ssid)\ | |
.replace("WIFI-PSK", new_psk) | |
# print(wifiText) # debug | |
try: | |
# to make is simple we just append the new wifi settings. | |
with open("my_wpa_supplicant.conf", "w") as wifiFile: | |
wifiFile.write(wifiText) | |
except IOError: | |
Logger.error('Configuration Screen: WifiNetworkAccordionItem: Unable to write WiFi config') | |
# todo What's the best way to update the OS? I'm thinking about Ansible. | |
# try: | |
# # update the configuration | |
# call("sudo wpa_cli reconfigure", shell=True) | |
# # todo schedule popup with success or failure | |
# except OSError as msg: | |
# print(msg) | |
# Logger.error('Configuration Screen: WifiNetworkAccordionItem: Unable to restart WiFi') | |
return True | |
else: | |
Logger.warning('Configuration Screen: WifiNetworkAccordionItem: Invalid value {} for [{}] {}'.format( | |
instance.text, instance.config_key['section'], instance.config_key['option'])) | |
instance.focus = True | |
popup = Popup(title='Invalid Data (734)', title_color=[1, 0, 0, 1], # color in the Kivy format (r, g, b, a). | |
seperator_color=[255, 0, 0, 1], seperator_height=15, | |
content=Label(text='Please try again.', | |
halign='center', valign='middle'), | |
size_hint=(None, None), size=(300, 300)) | |
popup.open() | |
# dismiss the popup after 1.25 seconds. | |
Clock.schedule_once(lambda *x: popup.dismiss(), 1.25) | |
# remove invalid data from field: | |
while instance._undo: | |
instance.do_undo() | |
instance.focus = True | |
return False | |
def validate(self, config_key, value): | |
Logger.info( | |
"Configuration Screen: WifiNetworkAccordionItem: validate got: config_key: {} value: {}".format( | |
config_key, value)) | |
try: | |
''' validators must return True of False ''' | |
validator = config_key['section'] + '_' + config_key['option'] | |
return validators[validator](value) | |
except Exception as e: | |
Logger.warn( | |
'Configuration Screen: WifiNetworkAccordionItem: Validator Exception: {}'.format(e.message)) | |
return False | |
class Test(BoxLayout): | |
def __init__(self, *args, **kwargs): | |
super(Test, self).__init__(*args, **kwargs) | |
class TestApp(App): | |
def build(self): | |
return Test() | |
if __name__ == '__main__': | |
Builder.load_string(kv) | |
TestApp().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment