Skip to content

Commit

Permalink
Merge pull request #72 from ZeroPhone/input-capabilities
Browse files Browse the repository at this point in the history
Input "capabilities" support + adding a UniversalInput UI element to work with input-constrained devices
  • Loading branch information
CRImier committed Jan 6, 2018
2 parents ce9975d + fd35e26 commit 6125233
Show file tree
Hide file tree
Showing 14 changed files with 430 additions and 55 deletions.
8 changes: 3 additions & 5 deletions apps/network_apps/wpa_cli/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


from helpers import setup_logger

menu_name = "Wireless"
Expand All @@ -11,7 +9,7 @@
from threading import Thread
from traceback import format_exc

from ui import Menu, Printer, MenuExitException, NumpadCharInput, Refresher, DialogBox, ellipsize
from ui import Menu, Printer, MenuExitException, UniversalInput, Refresher, DialogBox, ellipsize

import wpa_cli

Expand Down Expand Up @@ -59,7 +57,7 @@ def connect_to_network(network_info):
raise MenuExitException
#Offering to enter a password
else:
input = NumpadCharInput(i, o, message="Password:", name="WiFi password enter UI element")
input = UniversalInput(i, o, message="Password:", name="WiFi password enter UI element")
password = input.activate()
if password is None:
return False
Expand Down Expand Up @@ -274,7 +272,7 @@ def remove_network(id):
raise MenuExitException

def set_password(id):
input = NumpadCharInput(i, o, message="Password:", name="WiFi password enter UI element")
input = UniversalInput(i, o, message="Password:", name="WiFi password enter UI element")
password = input.activate()
if password is None:
return False
Expand Down
6 changes: 3 additions & 3 deletions apps/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from subprocess import check_output, CalledProcessError, STDOUT

from helpers import read_or_create_config, local_path_gen
from ui import Menu, Printer, PrettyPrinter, DialogBox, PathPicker, NumpadCharInput, TextReader
from ui import Menu, Printer, PrettyPrinter, DialogBox, PathPicker, UniversalInput, TextReader

menu_name = "Scripts" # App name as seen in main menu while using the system

Expand Down Expand Up @@ -68,14 +68,14 @@ def call_by_path():
path = PathPicker("/", i, o).activate()
if path is None:
return
args = NumpadCharInput(i, o, message="Arguments:", name="Script argument input").activate()
args = UniversalInput(i, o, message="Arguments:", name="Script argument input").activate()
if args is not None:
path = path + " " + args
call_external(path, shell=True)


def call_command():
command = NumpadCharInput(i, o, message="Command:", name="Script command input").activate()
command = UniversalInput(i, o, message="Command:", name="Script command input").activate()
if command is None:
return
call_external(command, shell=True)
Expand Down
2 changes: 0 additions & 2 deletions input/drivers/custom_i2c.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import smbus
from time import sleep

Expand Down
2 changes: 0 additions & 2 deletions input/drivers/hid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


from evdev import InputDevice as HID, list_devices, ecodes
from time import sleep

Expand Down
4 changes: 4 additions & 0 deletions input/drivers/pi_gpio_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def __init__(self, cols=[5, 6, 13], rows=[12, 16, 20, 21], **kwargs):
self.rows = rows
InputSkeleton.__init__(self, **kwargs)

def set_available_keys(self):
# mapping needs to be flattened before we can get available_keys from it
self.available_keys = [item for row in self.mapping for item in row]

def init_hw(self):
import RPi.GPIO as GPIO #Doing that because I couldn't mock it for ReadTheDocs
self.GPIO = GPIO
Expand Down
3 changes: 3 additions & 0 deletions input/drivers/pygame_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def init_hw(self):
self.emulator = emulator.get_emulator()
return True

def set_available_keys(self):
self.available_keys = self.KEY_MAP.values()

def runner(self):
"""
Blocking event loop which just calls supplied callbacks in the keymap.
Expand Down
30 changes: 25 additions & 5 deletions input/drivers/skeleton.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@

import threading
from copy import copy

from helpers import setup_logger

logger = setup_logger(__name__, "warning")

class InputSkeleton():
Expand All @@ -15,6 +14,7 @@ class InputSkeleton():

enabled = True
stop_flag = False
available_keys = None

def __init__(self, mapping=None, threaded=True):
if mapping is not None:
Expand All @@ -23,16 +23,36 @@ def __init__(self, mapping=None, threaded=True):
self.mapping = self.default_mapping
try:
self.init_hw()
except Exception as e:
logger.error("init_hw function not found!")
logger.exception(e)
except AttributeError:
logger.error("{}: init_hw function not found!".format(self.__class__))
self.set_available_keys()
if threaded:
self.start_thread()

def start(self):
"""Sets the ``enabled`` for loop functions to start sending keycodes."""
self.enabled = True

def set_available_keys(self):
"""
A simple ``i.available_keys``-setting code that assumes the driver's mapping is a plain
list of key names. If it's not so, the driver needs to override the
``set_available_keys`` method to properly generate the ``available_keys`` list.
"""
if not hasattr(self, "mapping"):
logger.warning("mapping not available - the HID driver is used?")
logger.warning("available_keys property set to None!")
self.available_keys = None
return
if type(self.mapping) not in (list, tuple):
raise ValueError("Can't use mapping as available_keys - not a list/tuple!")
if not all([isinstance(el, basestring) for el in self.mapping]):
raise ValueError("Can't use mapping as a capability if it's not a list of strings!")
if not all([el.startswith("KEY_") for el in self.mapping]):
nonkey_items = [el for el in self.mapping if not el.startswith("KEY_")]
raise ValueError("Can't use mapping as a capability if its elements don't start with \"KEY_\"! (non-KEY_ items: {})".format(nonkey_items))
self.available_keys = copy(list(self.mapping))

def stop(self):
"""Unsets the ``enabled`` for loop functions to stop sending keycodes."""
self.enabled = False
Expand Down
10 changes: 7 additions & 3 deletions input/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def __init__(self, errno=0, message=""):
self.message = message

class InputListener():
"""A class which listens for input device events and calls corresponding callbacks if set"""
"""
A class which listens for input device events and calls corresponding callbacks if set
"""
stop_flag = None
thread_index = 0
keymap = {}
Expand All @@ -33,9 +35,11 @@ def __init__(self, drivers, keymap=None):
"""Init function for creating KeyListener object. Checks all the arguments and sets keymap if supplied."""
self.drivers = drivers
self.queue = Queue.Queue()
if keymap is None: keymap = {}
for driver, _ in self.drivers:
if keymap is None: keymap = {}
self.available_keys = {}
for driver, driver_name in self.drivers:
driver.send_key = self.receive_key #Overriding the send_key method so that keycodes get sent to InputListener
self.available_keys[driver_name] = driver.available_keys
self.set_keymap(keymap)

def receive_key(self, key):
Expand Down
1 change: 1 addition & 0 deletions ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from checkbox import Checkbox
from dialog import DialogBox
from funcs import ellipsize, format_for_screen, ffs
from input import UniversalInput
from listbox import Listbox
from menu import Menu, MenuExitException
from number_input import IntegerAdjustInput
Expand Down
20 changes: 13 additions & 7 deletions ui/char_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CharArrowKeysInput():
last_displayed_char = 0
first_displayed_char = 0

def __init__(self, i, o, initial_value = "", message="Value:", allowed_chars=["][S", "][c", "][C", "][s", "][n"], name="CharArrowKeysInput"):
def __init__(self, i, o, message="Value:", value="", allowed_chars=["][S", "][c", "][C", "][s", "][n"], name="CharArrowKeysInput", initial_value=""):
"""Initialises the CharArrowKeysInput object.
Args:
Expand All @@ -57,7 +57,7 @@ def __init__(self, i, o, initial_value = "", message="Value:", allowed_chars=["]
Kwargs:
* ``initial_value``: Value to be edited. If not set, will start with an empty string.
* ``value``: Value to be edited. If not set, will start with an empty string.
* ``allowed_chars``: Characters to be used during input. Is a list of strings designating ranges which can be the following:
* '][c' for lowercase ASCII characters
* '][C' for uppercase ASCII characters
Expand All @@ -78,12 +78,15 @@ def __init__(self, i, o, initial_value = "", message="Value:", allowed_chars=["]
self.name = name
self.generate_keymap()
self.allowed_chars = allowed_chars
self.allowed_chars.append("][b")
self.allowed_chars.append("][b") #Adding backspace by default
self.generate_charmap()
if type(initial_value) != str:
#Support for obsolete attribute
if not value and initial_value:
value = initial_value
if type(value) != str:
raise ValueError("CharArrowKeysInput needs a string!")
self.value = list(initial_value)
self.char_indices = [] #Fixes a bug with char_indixes remaining from previous input ( 0_0 )
self.value = list(value)
self.char_indices = [] #Fixes a bug with char_indices remaining from previous input ( 0_0 )
for char in self.value:
self.char_indices.append(self.charmap.index(char))

Expand All @@ -102,14 +105,17 @@ def activate(self):
self.o.cursor()
self.to_foreground()
while self.in_foreground: #All the work is done in input callbacks
sleep(0.1)
self.idle_loop()
self.o.noCursor()
logger.debug(self.name+" exited")
if self.cancel_flag:
return None
else:
return ''.join(self.value) #Making string from the list we have

def idle_loop(self):
sleep(0.1)

def deactivate(self):
""" Deactivates the UI element, exiting it and thus making activate() return."""
self.in_foreground = False
Expand Down
23 changes: 23 additions & 0 deletions ui/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from numpad_input import NumpadCharInput
from char_input import CharArrowKeysInput

def UniversalInput(i, o, *args, **kwargs):
"""
Returns the most appropriate input UI element, based on available keys
of input devices present. For now, always returns UI elements configured
for character input.
"""
# First, checking if any of the drivers with None as available_keys is present
if None in i.available_keys.values():
# HID driver (or other driver with "any key is possible" is likely used
# Let's use the most fully-functional input available at the moment
return NumpadCharInput(i, o, *args, **kwargs)
all_available_keys = sum(i.available_keys.values(), [])
number_keys = ["KEY_{}".format(x) for x in range(10)]
number_keys_available = all([number_key in all_available_keys for number_key in number_keys ])
if number_keys_available:
# All number keys are supported
return NumpadCharInput(i, o, *args, **kwargs)
#fallback - only needs five primary keys
return CharArrowKeysInput(i, o, *args, **kwargs)

0 comments on commit 6125233

Please sign in to comment.