diff --git a/.github/workflows/pullrequest-unittest.yml b/.github/workflows/pullrequest-unittest.yml new file mode 100644 index 0000000..83deab1 --- /dev/null +++ b/.github/workflows/pullrequest-unittest.yml @@ -0,0 +1,17 @@ +name: Automated Testing + +on: + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: [ main ] + +jobs: + unittest: + if: '! github.event.pull_request.draft' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - run: python3 -m unittest discover diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 13c2c10..ac90c39 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -11,7 +11,7 @@ jobs: package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Adafruit Library Cache id: cache-mpy uses: actions/cache@v4 @@ -29,6 +29,11 @@ jobs: rmdir adafruit-circuitpython-bundle-10.x-mpy-${{ env.adafruit-bundle }} - name: Copy Adafruit Libraries run: | + rm -r -f \ + ./lib/adafruit_display_shapes \ + ./lib/adafruit_display_text \ + ./lib/adafruit_hid \ + ./lib/*.py cp -a \ ~/.adafruit-circuitpython-bundle/lib/adafruit_debouncer.mpy \ ~/.adafruit-circuitpython-bundle/lib/adafruit_display_shapes \ diff --git a/.gitignore b/.gitignore index 42bc50c..3946469 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ downloads/ eggs/ .eggs/ lib/**/*.mpy -lib/**/*.py lib64/ parts/ sdist/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4600741 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./lib" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 1920d7c..cadd81a 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,15 @@ Macropad, and then after releasing the button immediately hold down the blinking top-left key on the keypad (KEY1). You should see the text "Mounting Read/Write" quickly appear on the screen, and then the CIRCUITPY drive will mount in read/write mode. + + +## Building + +I've attempted to build mocks of all necessary libraries so things can be tested locally. + +### Running tests + +Tests can be run with the native Python unittest module, as in: +```sh +python3 -m unittest discover +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1263249 --- /dev/null +++ b/__init__.py @@ -0,0 +1,10 @@ +import sys +import commands + +# backwards compatibility for the 2.x series: +sys.modules['keyboard'] = commands +sys.modules['mouse'] = commands +sys.modules['pause'] = commands +sys.modules['sleep'] = commands +sys.modules['midi'] = commands +sys.modules['consumer'] = commands diff --git a/code.py b/code.py index 1442019..3249c52 100755 --- a/code.py +++ b/code.py @@ -1,34 +1,39 @@ import time -import keyfactory from adafruit_macropad import MacroPad from app import App -from display import Display -from pixels import Pixels -from sleep import Sleep +from keys import Keys +from screen import ScreenListener +from pixels import PixelListener +from hid import InputDeviceListener +from commands import Sleep + +## DEPRECATED +# Ensure backwards compatibility for the 2.x series +# So we don't have to change import statements in old macros +import sys +import commands +sys.modules['keyboard'] = commands +sys.modules['mouse'] = commands +sys.modules['pause'] = commands +sys.modules['sleep'] = commands +sys.modules['midi'] = commands +sys.modules['consumer'] = commands -KEY_LAUNCH = -1 -KEY_ENC_BUTTON = 12 -KEY_ENC_LEFT = 13 -KEY_ENC_RIGHT = 14 -MAX_KEYS = 14 -MAX_LEDS = 12 MACRO_FOLDER = '/macros' +# Core objects macropad = MacroPad() -last_position = macropad.encoder +screen = ScreenListener(macropad) +pixels = PixelListener(macropad) +hid = InputDeviceListener(macropad) + +# State variables last_time_seconds = time.monotonic() +last_position = macropad.encoder sleep_remaining = None +keys = None macro_changed = False app_index = 0 -app_current = None - -# The application state that is shared with the key events -state = { - "macropad": macropad, - "screen": Display(macropad), - "pixels": Pixels(macropad), - "sleeping": False, -} # Fractions of a second that have elapsed since this method's last run def elapsed_seconds(): @@ -40,116 +45,69 @@ def elapsed_seconds(): # Set the macro page (app) at the given index def set_app(index): - global app_current, app_index, sleep_remaining + global app_index, keys, screen, sleep_remaining app_index = index - app_current = apps[app_index] - sleep_remaining = app_current.timeout - state["macropad"].keyboard.release_all() - state["screen"].setApp(app_current) - state["pixels"].setApp(app_current) -# Get the macro sequence to execute for a given key -def get_sequence(key): - global app_current - if key == KEY_LAUNCH: - return app_current.launch[2] if app_current.launch else None - try: # No such sequence for this key - return app_current.macros[key][2] if key <= MAX_KEYS else [] - except (IndexError) as err: - print("Couldn't find sequence for key number ", key) - return None + sleep_remaining = apps[app_index].timeout + screen.setTitle(apps[app_index].name) + macropad.keyboard.release_all() + + keys = Keys(apps[app_index]) + keys.addListener(hid) + keys.addListener(screen) + keys.addListener(pixels) # Load available macros -state["screen"].initialize() +screen.initialize() apps = App.load_all(MACRO_FOLDER) if not apps: - state["screen"].setTitle('NO MACRO FILES FOUND') + screen.setError('NO MACRO FILES FOUND') while True: - pass + time.sleep(60.0) +set_app(app_index) try: # Test the USB HID connection - state["macropad"].keyboard.release_all() + macropad.keyboard.release_all() except OSError as err: print(err) - state["screen"].setTitle('NO USB CONNECTION') + screen.setError('NO USB CONNECTION') while True: - pass + time.sleep(60.0) # Prep before the run loop -set_app(0) -while True: # Event loop - state["macropad"].encoder_switch_debounced.update() - position = state["macropad"].encoder - pressed = False - rotated = False - # Do we need to press the "sleep" button? +while True: # Input event loop + macropad.encoder_switch_debounced.update() sleep_remaining -= elapsed_seconds() - if not state["sleeping"] and sleep_remaining <= 0: - Sleep().press(state) - continue - - if position != last_position: # Did we rotate the encoder? - key_number = KEY_ENC_LEFT if position < last_position else KEY_ENC_RIGHT - last_position = position - rotated = True - - # Do we need to change macro pages? - if rotated and state["macropad"].encoder_switch: - macro_changed = True - app_next = app_index - 1 if key_number is KEY_ENC_LEFT else app_index + 1 - set_app(app_next % len(apps)) - continue # Changing macros, not a keypress event - # We are now switching to a new macro page - elif macro_changed and state["macropad"].encoder_switch_debounced.released: - macro_changed = False - key_number = KEY_LAUNCH - rotated = True - # We only clicked the encoder button (not switching to a new page) - elif state["macropad"].encoder_switch_debounced.released: - key_number = KEY_ENC_BUTTON - pressed = state["macropad"].encoder_switch_debounced.released - else: # Was there a keypress event on the keypad? - event = state["macropad"].keys.events.get() - if not event or event.key_number >= len(app_current.macros): - if state["sleeping"]: time.sleep(1.0) # Low power mode - continue # No key events, or no corresponding macro, resume loop - key_number = event.key_number - pressed = event.pressed - - # Wake up if there is a key event while sleeping - sleep_remaining = app_current.timeout - if state["sleeping"] and (pressed or rotated): - Sleep().press(state) - continue - - sequence = get_sequence(key_number) - if sequence and (rotated or pressed): # Key Down Event - if not state["sleeping"] and (0 <= key_number < MAX_LEDS): - state["pixels"].highlight(key_number, 0xFFFFFF) - state["screen"].highlight(key_number) - - if type(sequence) is list: - for item in sequence: - if type(item) is list: # We have a macro to execute - for subitem in item: # Press the key combination - keyfactory.get(subitem).press(state) - for subitem in item: # Immediately release the key combo - keyfactory.get(subitem).release(state) - else: # We have a key combination to press - keyfactory.get(item).press(state) - else: # We just have a single command to execute - keyfactory.get(sequence).press(state) - - if sequence and (rotated or not pressed): # Key Up Event - if type(sequence) is list: - for item in sequence: - if type(item) is not list: # Release any still-pressed key combinations - keyfactory.get(item).release(state) - # Macro key cobinations should already have been released - else: # Release any still-pressed single commands - keyfactory.get(sequence).release(state) - if not state["sleeping"] and (0 <= key_number < MAX_LEDS): # No pixel for encoder button - state["pixels"].reset(key_number) - state["screen"].reset(key_number) + event = macropad.keys.events.get() + + if event or last_position != macropad.encoder or macropad.encoder_switch_debounced.released: + keys.release(Keys.KEY_SLEEP) # Don't go to sleep! + sleep_remaining = apps[app_index].timeout + if sleep_remaining <= 0: # Go to sleep and slow down + keys.press(Keys.KEY_SLEEP) + time.sleep(1.0) + elif event and event.pressed: # Key was pressed + keys.press(event.key_number) + elif event and event.released: # Key was released + keys.release(event.key_number) + elif macropad.encoder_switch and macropad.encoder < last_position: + last_position = macropad.encoder # Push down and turn (left) + set_app((app_index - 1) % len(apps)) + macro_changed = True + elif macropad.encoder_switch and macropad.encoder > last_position: + last_position = macropad.encoder # Push down and turn (right) + set_app((app_index + 1) % len(apps)) + macro_changed = True + elif macropad.encoder < last_position: # Rotary counter-clockwise + last_position = macropad.encoder + keys.press(Keys.KEY_ENC_LEFT) + keys.release(Keys.KEY_ENC_LEFT) + elif macropad.encoder > last_position: # Rotary clockwise + last_position = macropad.encoder + keys.press(Keys.KEY_ENC_RIGHT) + keys.release(Keys.KEY_ENC_RIGHT) + elif macropad.encoder_switch_debounced.released: + if macro_changed: macro_changed = False # Land on the selected macro page + else: keys.press(Keys.KEY_ENC_BUTTON) # Encoder button "pressed" diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..0edd3d4 --- /dev/null +++ b/commands.py @@ -0,0 +1,91 @@ +import time + +class Commands: + def __init__(self, macro): + self.commands = [] + if isinstance(macro, list): + for item in macro: + self.commands += [Commands.build(item)] + else: + self.commands += [Commands.build(macro)] + + def __del__(self): + self.commands.clear() + + def __bool__(self): + return len(self.commands) > 0 + + def __iter__(self): + return iter(self.commands) + + def __getitem__(self, index): + return self.commands[index] + + def __len__(self): + return len(self.commands) + + @staticmethod + def build(item): + if isinstance(item, Toolbar): + return item + elif isinstance(item, Mouse): + return item + elif isinstance(item, Midi): + return item + elif isinstance(item, Sleep): + return item + elif isinstance(item, float): + return Pause(item) + elif isinstance(item, list): + return Sequence(item) + else: + return Keyboard(item) + +class Command: + def __init__(self, keycode): + self.keycode = keycode + +class Sequence(Command): + def __init__(self, keycodes): + self.commands = [] + for keycode in keycodes: + self.commands += [Commands.build(keycode)] + + def __del__(self): + self.commands.clear() + + def __bool__(self): + return len(self.commands) > 0 + + def __iter__(self): + return iter(self.commands) + + def __getitem__(self, index): + return self.commands[index] + + def __len__(self): + return len(self.commands) + +class Keyboard(Command): + def __init__(self, key): + self.keycode = key + +class Toolbar(Command): + def __init__(self, keycode): + self.keycode = keycode + +class Mouse(Command): + def __init__(self, keycode): + self.keycode = keycode + +class Midi(Command): + def __init__(self, note): + self.keycode = note + +class Pause(Command): + def __init__(self, seconds): + self.keycode = seconds + +class Sleep(Command): + def __init__(self): + pass diff --git a/consumer.py b/consumer.py deleted file mode 100755 index c781b19..0000000 --- a/consumer.py +++ /dev/null @@ -1,12 +0,0 @@ -class Toolbar: - def __init__(self, keycode): - self.keycode = keycode - - def press(self, state): - if self.keycode < 0: - state["macropad"].consumer_control.release() - else: - state["macropad"].consumer_control.press(self.keycode) - - def release(self, state): - state["macropad"].consumer_control.release() diff --git a/hid.py b/hid.py new file mode 100644 index 0000000..e901cba --- /dev/null +++ b/hid.py @@ -0,0 +1,93 @@ +from commands import Commands, Command, Toolbar, Keyboard, Midi, Mouse, Pause, Sequence +import time + +class InputDeviceListener: + MIDI_VELOCITY = 127 + + def __init__(self, macropad): + self.macropad = macropad + + def __del__(self): + pass + + def register(self, _): + pass + + def pressed(self, keys, index): + key = keys[index] + self.pressCommands(key.commands) + + def released(self, keys, index): + key = keys[index] + self.releaseCommands(key.commands) + + def pressCommands(self, commands: Commands): + for command in commands: + self.press(command) + + def releaseCommands(self, commands: Commands): + for command in commands: + self.release(command) + + def press(self, command: Command): + if isinstance(command, Toolbar): return self.pressToolbar(command) + elif isinstance(command, Mouse): return self.pressMouse(command) + elif isinstance(command, Midi): return self.pressMidi(command) + elif isinstance(command, Pause): return self.pressPause(command) + elif isinstance(command, Sequence): return self.pressSequence(command) + elif isinstance(command, Keyboard): return self.pressKeyboard(command) + + def release(self, command: Command): + if isinstance(command, Toolbar): return self.releaseToolbar(command) + elif isinstance(command, Mouse): return self.releaseMouse(command) + elif isinstance(command, Midi): return self.releaseMidi(command) + elif isinstance(command, Keyboard): return self.releaseKeyboard(command) + + def pressToolbar(self, command:Toolbar): + if command.keycode < 0: + self.macropad.consumer_control.release() + else: + self.macropad.consumer_control.press(command.keycode) + + def pressMouse(self, command:Mouse): + if command.keycode < 0: + self.macropad.mouse.release(command.keycode) + else: + self.macropad.mouse.press(command.keycode) + + def pressMidi(self, command:Midi): + if command.keycode < 0: + self.macropad.midi.send(self.macropad.NoteOff(command.keycode, 0)) + else: + self.macropad.midi.send(self.macropad.NoteOn(command.keycode, InputDeviceListener.MIDI_VELOCITY)) + + def pressPause(self, command:Pause): + time.sleep(command.keycode) + + def pressSequence(self, sequence:Sequence): + for command in sequence: + self.press(command) + for command in sequence: + self.release(command) + + def pressKeyboard(self, command:Keyboard): + if not isinstance(command.keycode, int): + self.macropad.keyboard_layout.write(command.keycode) + elif command.keycode < 0: + self.macropad.keyboard.release(command.keycode) + else: + self.macropad.keyboard.press(command.keycode) + + def releaseToolbar(self, command:Toolbar): + self.macropad.consumer_control.release() + + def releaseMouse(self, command:Mouse): + self.macropad.mouse.release(command.keycode) + + def releaseMidi(self, command:Midi): + if command.keycode >= 0: + self.macropad.midi.send(self.macropad.NoteOff(command.keycode, 0)) + + def releaseKeyboard(self, command:Keyboard): + if isinstance(command.keycode, int) and command.keycode >= 0: + self.macropad.keyboard.release(command.keycode) diff --git a/keyboard.py b/keyboard.py deleted file mode 100755 index f11db31..0000000 --- a/keyboard.py +++ /dev/null @@ -1,16 +0,0 @@ -class Keyboard: - def __init__(self, key): - self.key = key - - def press(self, state): - macropad = state["macropad"] - if not isinstance(self.key, int): - macropad.keyboard_layout.write(self.key) - elif self.key < 0: - macropad.keyboard.release(self.key) - else: - macropad.keyboard.press(self.key) - - def release(self, state): - if isinstance(self.key, int) and self.key >= 0: - state["macropad"].keyboard.release(self.key) diff --git a/keyfactory.py b/keyfactory.py deleted file mode 100644 index ad3ff9d..0000000 --- a/keyfactory.py +++ /dev/null @@ -1,20 +0,0 @@ -from consumer import Toolbar -from mouse import Mouse -from pause import Pause -from keyboard import Keyboard -from midi import Midi -from sleep import Sleep - -def get(item): - if isinstance(item, Toolbar): - return item - elif isinstance(item, Mouse): - return item - elif isinstance(item, Midi): - return item - elif isinstance(item, Sleep): - return item - elif isinstance(item, float): - return Pause(item) - else: - return Keyboard(item) diff --git a/keys.py b/keys.py new file mode 100644 index 0000000..d1381f5 --- /dev/null +++ b/keys.py @@ -0,0 +1,55 @@ +from commands import Commands, Sleep + +class Key: + def __init__(self, macro, label='', color=0xF0F0F0): + self.commands = Commands(macro) + self.label = label + self.color = color + +class Keys: + KEY_ENC_BUTTON = 12 # Virtual key for encoder press + KEY_ENC_LEFT = 13 # Virtual key for encoder rotation left + KEY_ENC_RIGHT = 14 # Virtual key for encoder rotation right + KEY_SLEEP = 15 # Hidden key for sleeping + + listeners = [] + keys = [] + app = None + + def __init__(self, app): + self.app = app + + self.keys = [None] * 16 + for i in range(len(self.app.macros)): + color, label, macro = self.app.macros[i] + self.keys[i] = Key(macro, label, color) + self.keys[15] = Key(Sleep()) + + def __del__(self): + self.listeners.clear() + self.keys.clear() + self.app = None + + def __bool__(self): + return len(self.keys) > 0 + + def __iter__(self): + return iter(self.keys) + + def __getitem__(self, index): + return self.keys[index] + + def __len__(self): + return len(self.keys) + + def addListener(self, listener): + self.listeners += [listener] + listener.register(self) + + def press(self, key_index): + for listener in self.listeners: + listener.pressed(self.keys, key_index) + + def release(self, key_index): + for listener in self.listeners: + listener.released(self.keys, key_index) diff --git a/lib/README.md b/lib/README.md index 01d1c1e..acf0602 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,12 +1,14 @@ # Adafruit Macropad Libraries -These MicroPython compiled libraries are provided as a convenience when -installing this version of Macropad Hotkeys. See the Macropad +In packaged versions of this app, these MicroPython compiled libraries are provided as a +convenience when installing Macropad Hotkeys. See the Macropad [Adafruit Learning System Guide](https://learn.adafruit.com/macropad-hotkeys/project-code) for additional details. -If you are installing straight from this repository, you can download the -libraries as a bundle from the +If you instead cloned this repository, you will receive the mocked version +of the libraries used for automated testing. To install Macropad Hotkeys +from a repository, remove the `*.py` files from `lib/` and replace them +with the corresponding files from the [CircuitPython library bundle](https://circuitpython.org/libraries). The libraries required by this version of Macropad Hotkeys includes: @@ -21,6 +23,6 @@ The libraries required by this version of Macropad Hotkeys includes: - adafruit_simple_text_display.mpy - neopixel.mpy -These libraries are individually licensed in each of their GitHub repositories -and are provided by Adafruit under the MIT license +The production libraries are individually licensed in each of their GitHub +repositories and are provided by Adafruit under the MIT license (see also [LICENSE](./LICENSE) in this directory) diff --git a/lib/adafruit_display_shapes/rect.py b/lib/adafruit_display_shapes/rect.py new file mode 100644 index 0000000..7a22ded --- /dev/null +++ b/lib/adafruit_display_shapes/rect.py @@ -0,0 +1,3 @@ +class Rect(): + def __init__(self, x, y, width, height, fill=None): + pass \ No newline at end of file diff --git a/lib/adafruit_display_text/label.py b/lib/adafruit_display_text/label.py new file mode 100644 index 0000000..a146b9f --- /dev/null +++ b/lib/adafruit_display_text/label.py @@ -0,0 +1,8 @@ +class Label(): + def __init__(self, font, text=None, color=None, background_color=None, anchored_position=(0, 0), anchor_point=0): + self.text = text + self.color = color + self.background_color = background_color + self.anchored_position = anchored_position + self.anchor_point = anchor_point + \ No newline at end of file diff --git a/lib/adafruit_hid/__init__.py b/lib/adafruit_hid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/adafruit_hid/consumer_control_code.py b/lib/adafruit_hid/consumer_control_code.py new file mode 100644 index 0000000..ff279e8 --- /dev/null +++ b/lib/adafruit_hid/consumer_control_code.py @@ -0,0 +1,5 @@ +class ConsumerControlCode: + PLAY_PAUSE = 0xCD + MUTE = 0xE2 + VOLUME_DECREMENT = 0xEA + VOLUME_INCREMENT = 0xE9 diff --git a/lib/adafruit_hid/keycode.py b/lib/adafruit_hid/keycode.py new file mode 100644 index 0000000..252e8d1 --- /dev/null +++ b/lib/adafruit_hid/keycode.py @@ -0,0 +1,18 @@ +class Keycode: + A = 0x04 + B = 0x05 + C = 0x06 + D = 0x07 + E = 0x08 + F = 0x09 + G = 0x0A + + RIGHT_ARROW = 0x4F + LEFT_ARROW = 0x50 + DOWN_ARROW = 0x51 + UP_ARROW = 0x52 + + LEFT_SHIFT = 0xE1 + SHIFT = LEFT_SHIFT + RIGHT_SHIFT = 0xE5 + \ No newline at end of file diff --git a/lib/adafruit_hid/mouse.py b/lib/adafruit_hid/mouse.py new file mode 100644 index 0000000..cab24db --- /dev/null +++ b/lib/adafruit_hid/mouse.py @@ -0,0 +1,6 @@ +class Mouse: + LEFT_BUTTON = 1 + RIGHT_BUTTON = 2 + MIDDLE_BUTTON = 4 + BACK_BUTTON = 8 + FORWARD_BUTTON = 16 diff --git a/lib/adafruit_macropad.py b/lib/adafruit_macropad.py new file mode 100644 index 0000000..82e1908 --- /dev/null +++ b/lib/adafruit_macropad.py @@ -0,0 +1,30 @@ +class MacroPad: + def __init__(self): + self.keyboard = Keyboard() + self.encoder_switch_debounced = EncoderSwitch() + self.display = Display() + +class Display: + def __init__(self): + self.width = 0 + self.height = 0 + + def release_all(self): + pass + + def refresh(self): + pass + +class Keyboard: + def __init__(self): + pass + + def release_all(self): + pass + +class EncoderSwitch: + def __init__(self): + pass + + def update(self): + pass \ No newline at end of file diff --git a/lib/displayio.py b/lib/displayio.py new file mode 100644 index 0000000..fdb209d --- /dev/null +++ b/lib/displayio.py @@ -0,0 +1,15 @@ +class Group(): + def __init__(self): + self.list = [] + + def __getitem__(self, index): + return self.list[index] + + def __setitem__(self, index, value): + self.list[index] = value + + def __len__(self): + return len(self.list) + + def append(self, value): + self.list = self.list + [value] \ No newline at end of file diff --git a/lib/terminalio.py b/lib/terminalio.py new file mode 100644 index 0000000..4e5197e --- /dev/null +++ b/lib/terminalio.py @@ -0,0 +1 @@ +FONT = None \ No newline at end of file diff --git a/midi.py b/midi.py deleted file mode 100644 index bdb60dc..0000000 --- a/midi.py +++ /dev/null @@ -1,17 +0,0 @@ -VELOCITY = 127 - -class Midi: - def __init__(self, note): - self.note = note - - def press(self, state): - macropad = state["macropad"] - if self.note < 0: - macropad.midi.send(macropad.NoteOff(self.note, 0)) - else: - macropad.midi.send(macropad.NoteOn(self.note, VELOCITY)) - - def release(self, state): - macropad = state["macropad"] - if self.note >= 0: - macropad.midi.send(macropad.NoteOff(self.note, 0)) diff --git a/mouse.py b/mouse.py deleted file mode 100755 index b8bd1a6..0000000 --- a/mouse.py +++ /dev/null @@ -1,12 +0,0 @@ -class Mouse: - def __init__(self, keycode): - self.keycode = keycode - - def press(self, state): - if self.keycode < 0: - state["macropad"].mouse.release(self.keycode) - else: - state["macropad"].mouse.press(self.keycode) - - def release(self, state): - state["macropad"].mouse.release(self.keycode) diff --git a/pause.py b/pause.py deleted file mode 100755 index 61e430e..0000000 --- a/pause.py +++ /dev/null @@ -1,11 +0,0 @@ -import time - -class Pause: - def __init__(self, seconds): - self.seconds = seconds - - def press(self, state): - time.sleep(self.seconds) - - def release(self, state): - pass diff --git a/pixels.py b/pixels.py index c4f44aa..b4104b7 100644 --- a/pixels.py +++ b/pixels.py @@ -1,51 +1,71 @@ -import time +from commands import Sleep -BRIGHTNESS = 0.3 +class PixelListener: + BRIGHTNESS = 0.3 + MAX_LEDS = 12 + sleeping = False -class Pixels: def __init__(self, macropad): self.pixels = macropad.pixels self.pixels.auto_write = False - self.pixels.brightness = BRIGHTNESS + self.pixels.brightness = PixelListener.BRIGHTNESS + self.keycolors = [] + self.sleeping = False - def setApp(self, app): - self.macros = app.macros + def __del__(self): + self.pixels.clear() - for i in range(12): - if i < len(self.macros): - self.pixels[i] = self.macros[i][0] + def initialize(self): + self.keycolors = [] + for i in range(PixelListener.MAX_LEDS): + if i < len(self.keycolors): + self.pixels[i] = self.keycolors[i] else: - self.pixels[i] = 0 + self.pixels[i] = 0x000000 self.pixels.show() + def register(self, keys): + self.keycolors = list(map(lambda k: k.color, keys)) + for i in range(PixelListener.MAX_LEDS): + if i < len(self.keycolors): + self.pixels[i] = self.keycolors[i] + else: + self.pixels[i] = 0x000000 + self.pixels.show() + + def pressed(self, keys, index): + self.highlight(index) + + commands = keys[index].commands + if isinstance(commands[0], Sleep): + self.sleep() + + def released(self, keys, index): + self.reset(index) + + commands = keys[index].commands + if isinstance(commands[0], Sleep): + self.resume() + def sleep(self): + if self.sleeping: return self.pixels.brightness = 0.0 self.pixels.show() + self.sleeping = True def resume(self): - self.pixels.brightness = BRIGHTNESS + if not self.sleeping: return + self.pixels.brightness = PixelListener.BRIGHTNESS self.pixels.show() + self.sleeping = False - def highlight(self, key_index, color): + def highlight(self, key_index, color=0xFFFFFF): + if key_index >= PixelListener.MAX_LEDS: return self.pixels[key_index] = color self.pixels.show() def reset(self, key_index): - self.pixels[key_index] = self.macros[key_index][0] - self.pixels.brightness = BRIGHTNESS + if key_index >= PixelListener.MAX_LEDS: return + self.pixels[key_index] = self.keycolors[key_index] if key_index < len(self.keycolors) else 0x000000 + self.pixels.brightness = PixelListener.BRIGHTNESS self.pixels.show() - - @staticmethod - def hexToTuple(color): - return (color >> 16, (color >> 8) & 0xFF, color & 0xFF) - - @staticmethod - def blend(color_fg, color_bg, alpha_fg): - red_fg, green_fg, blue_fg = Pixels.hexToTuple(color_fg) - red_bg, green_bg, blue_bg = Pixels.hexToTuple(color_bg) - alpha_bg = 1.0 - alpha_fg - color_result_alpha = 1 - (1 - alpha_fg) * (1 - alpha_bg) - color_result_red = red_fg * alpha_fg / color_result_alpha + red_bg * alpha_bg * (1 - alpha_fg) / color_result_alpha - color_result_green = green_fg * alpha_fg / color_result_alpha + green_bg * alpha_bg * (1 - alpha_fg) / color_result_alpha - color_result_blue = blue_fg * alpha_fg / color_result_alpha + blue_bg * alpha_bg * (1 - alpha_fg) / color_result_alpha - return (color_result_red, color_result_green, color_result_blue) diff --git a/display.py b/screen.py similarity index 66% rename from display.py rename to screen.py index 05ebd9b..5df95c4 100644 --- a/display.py +++ b/screen.py @@ -2,22 +2,30 @@ import terminalio from adafruit_display_text import label from adafruit_display_shapes.rect import Rect +from commands import Sleep + +class ScreenListener: + MAX_LABELS = 12 + sleeping = False -class Display: def __init__(self, macropad): self.display = macropad.display self.display.auto_refresh = False + self.sleeping = False + + def __del__(self): + self.display = None def initialize(self): self.group = displayio.Group() - for key_index in range(12): + for key_index in range(ScreenListener.MAX_LABELS): x = key_index % 3 y = key_index // 3 self.group.append( label.Label(terminalio.FONT, text='', color=0xFFFFFF, - background_color=None, + background_color=0x000000, anchored_position=((self.display.width - 1) * x / 2, self.display.height - 1 - (3 - y) * 12), anchor_point=(x / 2, 1.0) @@ -35,37 +43,54 @@ def initialize(self): ) self.display.root_group = self.group + def setTitle(self, text): + self.group[13].text = text + self.display.refresh() + + def register(self, keys): + for i in range(12): + if i < len(keys): + self.group[i].text = keys[i].label + else: + self.group[i].text = '' + self.display.refresh() + + def pressed(self, keys, index): + self.highlight(index) + + commands = keys[index].commands + if isinstance(commands[0], Sleep): + self.sleep() + + def released(self, keys, index): + self.reset(index) + + commands = keys[index].commands + if isinstance(commands[0], Sleep): + self.resume() + def sleep(self): + if self.sleeping: return self.display.brightness = 0 self.display.root_group = displayio.Group() self.display.refresh() + self.sleeping = True def resume(self): + if not self.sleeping: return self.display.brightness = 1 self.display.root_group = self.group self.display.refresh() + self.sleeping = False def highlight(self, key_index): + if key_index >= ScreenListener.MAX_LABELS: return self.group[key_index].color = 0x000000 self.group[key_index].background_color = 0xFFFFFF self.display.refresh() def reset(self, key_index): + if key_index >= ScreenListener.MAX_LABELS: return self.group[key_index].color = 0xFFFFFF self.group[key_index].background_color = 0x000000 self.display.refresh() - - def setApp(self, app): - self.group[13].text = app.name - for i in range(12): - if i < len(app.macros): - self.group[i].text = app.macros[i][1] - else: - self.group[i].text = '' - self.display.refresh() - - def setTitle(self, text): - self.group[13].text = text - for i in range(12): - self.group[i].text = '' - self.display.refresh() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fe9ebff --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup # type: ignore + +setup( + name="macropad-hotkeys", + use_scm_version=True, + setup_requires=["setuptools_scm"], + description="Use your Adafruit Macropad to setup hotkeys and macros for a number of applications", + long_description="CircuitPython application that runs on the Adafruit Macropad for extensible macros and hotkeys", + long_description_content_type="text/markdown", + url="https://github.com/deckerego/Macropad_Hotkeys", + author="DeckerEgo", + author_email="john@deckerego.net", + py_modules=[], + install_requires=[], + license="MIT", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "Topic :: System :: Hardware :: Universal Serial Bus (USB) :: Human Interface Device (HID)", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + ], + keywords="adafruit circuitpython micropython macropad hotkeys macros keyboard hid", +) \ No newline at end of file diff --git a/sleep.py b/sleep.py deleted file mode 100755 index 5309575..0000000 --- a/sleep.py +++ /dev/null @@ -1,17 +0,0 @@ -import time - -class Sleep: - def __init__(self): - pass - - def press(self, state): - if state["sleeping"]: - state["screen"].resume() - state["pixels"].resume() - else: - state["screen"].sleep() - state["pixels"].sleep() - state["sleeping"] = not state["sleeping"] - - def release(self, state): - pass diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..41c4887 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,12 @@ +import sys +import commands + +sys.path.insert(0,'./lib') + +# backwards compatibility for the 2.x series: +sys.modules['keyboard'] = commands +sys.modules['mouse'] = commands +sys.modules['pause'] = commands +sys.modules['sleep'] = commands +sys.modules['midi'] = commands +sys.modules['consumer'] = commands diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..630aa0c --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,36 @@ +from unittest import TestCase, mock +from adafruit_hid.keycode import Keycode +from keyboard import Keyboard # pyright: ignore[reportMissingImports, reportMissingModuleSource] Switch to command module after 3.x +from pause import Pause # pyright: ignore[reportMissingImports, reportMissingModuleSource] Switch to command module after 3.x +from mouse import Mouse # pyright: ignore[reportMissingImports, reportMissingModuleSource] Switch to command module after 3.x +from commands import Commands, Sequence + +class MockKeycode(Keycode): + MOCK_1 = mock.Mock() + MOCK_2 = mock.Mock() + MOCK_3 = mock.Mock() + MOCK_4 = mock.Mock() + +class TestFactory(TestCase): + def test_alpha(self): + command = Commands.build('O') + self.assertIsInstance(command, Keyboard) + + def test_float(self): + command = Commands.build(1.0) + self.assertIsInstance(command, Pause) + + def test_mouse(self): + command = Commands.build(Mouse(0)) + self.assertIsInstance(command, Mouse) + +class TestCommands(TestCase): + def test_iterator(self): + commands = Commands([[MockKeycode.MOCK_1, MockKeycode.MOCK_2], [MockKeycode.MOCK_3, MockKeycode.MOCK_2, MockKeycode.MOCK_1], MockKeycode.MOCK_4]) + command_index = [] + for command in commands: + command_index += [command] + + self.assertIsInstance(command_index[0], Sequence) + self.assertIsInstance(command_index[1], Sequence) + self.assertIsInstance(command_index[2], Keyboard) diff --git a/tests/test_hid.py b/tests/test_hid.py new file mode 100644 index 0000000..f3fb2ff --- /dev/null +++ b/tests/test_hid.py @@ -0,0 +1,172 @@ +from unittest import mock, TestCase +from keys import Keys, Key +from hid import InputDeviceListener +from commands import Toolbar, Mouse +from adafruit_hid.keycode import Keycode +from adafruit_hid.consumer_control_code import ConsumerControlCode +from adafruit_hid.mouse import Mouse as MouseCode + +class MockKeys(Keys): + def __init__(self, listeners, app): + self.keys = [ + Key('A'), + Key(Keycode.A), + Key([Keycode.A, Keycode.B]), + Key([[Keycode.SHIFT, Keycode.A]]), + Key(Toolbar(ConsumerControlCode.VOLUME_DECREMENT)), + Key(Mouse(MouseCode.LEFT_BUTTON)), + ] + +class MockMacroPad: + def __init__(self): + self.keyboard = MockKeyboard() + self.keyboard_layout = MockKeyboardLayout() + self.consumer_control = MockConsumerControl() + self.mouse = MockMouse() + +class MockKeyboard: + def __init__(self): + self.press = mock.Mock() + self.release = mock.Mock() + self.release_all = mock.Mock() + +class MockKeyboardLayout: + def __init__(self): + self.write = mock.Mock() + +class MockConsumerControl: + def __init__(self): + self.press = mock.Mock() + self.release = mock.Mock() + +class MockMouse: + def __init__(self): + self.press = mock.Mock() + self.release = mock.Mock() + +class TestInputDevice(TestCase): + def test_press_alpha(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 0) + + macropad.keyboard_layout.write.assert_called_once() + macropad.keyboard.press.assert_not_called() + macropad.keyboard.release.assert_not_called() + + def test_release_alpha(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 0) + + macropad.keyboard_layout.write.assert_not_called() + macropad.keyboard.release.assert_not_called() + macropad.keyboard.press.assert_not_called() + + def test_press_keycode(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 1) + + macropad.keyboard.press.assert_called_once() + macropad.keyboard.release.assert_not_called() + + def test_release_keycode(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 1) + + macropad.keyboard.release.assert_called_once() + macropad.keyboard.press.assert_not_called() + + def test_press_toolbar(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 4) + + macropad.consumer_control.press.assert_called_once() + macropad.consumer_control.release.assert_not_called() + macropad.keyboard.press.assert_not_called() + macropad.keyboard.release.assert_not_called() + + def test_release_toolbar(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 4) + + macropad.consumer_control.release.assert_called_once() + macropad.consumer_control.press.assert_not_called() + macropad.keyboard.release.assert_not_called() + macropad.keyboard.press.assert_not_called() + + def test_press_mouse(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 5) + + macropad.mouse.press.assert_called_once() + macropad.mouse.release.assert_not_called() + + def test_release_mouse(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 5) + + macropad.mouse.release.assert_called_once() + macropad.mouse.press.assert_not_called() + + def test_press_sequence(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 3) + + macropad.keyboard.press.assert_has_calls([mock.call(0xE1), mock.call(0x04)]) + macropad.keyboard.release.assert_has_calls([mock.call(0xE1), mock.call(0x04)]) + + def test_release_sequence(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 3) + + macropad.keyboard.release.assert_not_called() + macropad.keyboard.press.assert_not_called() + + def test_press_series(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.pressed(keys, 2) + + macropad.keyboard.press.assert_has_calls([mock.call(0x04), mock.call(0x05)]) + macropad.keyboard.release.assert_not_called() + + def test_release_series(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + listener = InputDeviceListener(macropad) + listener.register(keys) + listener.released(keys, 2) + + macropad.keyboard.release.assert_has_calls([mock.call(0x04), mock.call(0x05)]) + macropad.keyboard.press.assert_not_called() diff --git a/tests/test_keys.py b/tests/test_keys.py new file mode 100644 index 0000000..b971c5d --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,62 @@ +from unittest import TestCase, mock +from keys import Keys +from adafruit_hid.keycode import Keycode +from commands import Commands + +class MockKeycode(Keycode): + MOCK_1 = mock.Mock() + +class MockApp: + def __init__(self): + self.name = '' + self.order = 0 + self.launch = None + self.timeout = 300 + self.macros = [ + (0x0F0F0F, 'MOCK_1', MockKeycode.MOCK_1), + ] + +class MockListener: + def __init__(self): + self.pressed = mock.Mock() + self.released = mock.Mock() + def register(self, _): + pass + +class TestKeys(TestCase): + def test_init(self): + app = MockApp() + keys = Keys(app) + keys.addListener(MockListener()) + + self.assertEqual(len(keys.keys), 16) + self.assertEqual(keys.keys[0].color, 0x0F0F0F) + self.assertIsInstance(keys.keys[0].commands, Commands) + + def test_press(self): + listenerOne = MockListener() + listenerTwo = MockListener() + app = MockApp() + keys = Keys(app) + keys.addListener(listenerOne) + keys.addListener(listenerTwo) + keys.press(0) + + listenerOne.pressed.assert_called_once() + listenerOne.released.assert_not_called() + listenerTwo.pressed.assert_called_once() + listenerTwo.released.assert_not_called() + + def test_release(self): + listenerOne = MockListener() + listenerTwo = MockListener() + app = MockApp() + keys = Keys(app) + keys.addListener(listenerOne) + keys.addListener(listenerTwo) + keys.release(0) + + listenerOne.released.assert_called_once() + listenerOne.pressed.assert_not_called() + listenerTwo.released.assert_called_once() + listenerTwo.pressed.assert_not_called() diff --git a/tests/test_pixels.py b/tests/test_pixels.py new file mode 100644 index 0000000..42885e6 --- /dev/null +++ b/tests/test_pixels.py @@ -0,0 +1,139 @@ +from unittest import mock, TestCase +from keys import Keys, Key +from pixels import PixelListener +from commands import Commands, Sleep + +class MockKeys(Keys): + def __init__(self, listeners, app): + self.keys = [ + Key(mock.Mock(), "", 0x0000FF), + Key(mock.Mock(), "", 0x00FF00), + Key(mock.Mock(), "", 0xFF0000), + ] + +class MockMacroPad: + def __init__(self): + self.pixels = MockPixels() + +class MockPixels: + def __init__(self): + self.auto_write = True + self.brightness = 1.0 + self.leds = [0x000000] * 12 + self.show = mock.Mock() + def __getitem__(self, key): + return self.leds[key] + def __setitem__(self, key, value): + self.leds[key] = value + def clear(self): + self.leds.clear() + +class TestPixels(TestCase): + def test_initalize(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.initialize() + + self.assertEqual(pixels.pixels[1], 0x000000) + + def test_sleep(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.sleep() + + self.assertEqual(pixels.pixels.brightness, 0.0) + + def test_sleep_twice(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.sleep() + pixels.sleep() + + pixels.pixels.show.assert_called_once() + + def test_resume(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.sleep() + pixels.resume() + + self.assertEqual(pixels.pixels.brightness, PixelListener.BRIGHTNESS) + + def test_resume_twice(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.sleep() + + pixels.pixels.show.reset_mock() + pixels.resume() + pixels.resume() + + pixels.pixels.show.assert_called_once() + + def test_highlight(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.highlight(1) + + self.assertEqual(pixels.pixels[0], 0x000000) + self.assertEqual(pixels.pixels[1], 0xFFFFFF) + + def test_reset(self): + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.highlight(1) + pixels.reset(1) + + self.assertEqual(pixels.pixels[0], 0x000000) + self.assertEqual(pixels.pixels[1], 0x000000) + + def test_set_keys(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + pixels = PixelListener(macropad) + pixels.register(keys) + + self.assertEqual(pixels.pixels[0], 0x0000FF) + self.assertEqual(pixels.pixels[1], 0x00FF00) + self.assertEqual(pixels.pixels[2], 0xFF0000) + self.assertEqual(pixels.pixels[3], 0x000000) + + def test_press(self): + keys = MockKeys([], None) + class MockPixelListener(PixelListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + pixels = MockPixelListener(macropad) + pixels.register(keys) + pixels.pressed(keys, 1) + + pixels.highlight.assert_called_once() + pixels.reset.assert_not_called() + + def test_release(self): + keys = MockKeys([], None) + class MockPixelListener(PixelListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + pixels = MockPixelListener(macropad) + pixels.register(keys) + pixels.released(keys, 1) + + pixels.reset.assert_called_once() + pixels.highlight.assert_not_called() + + def test_sleep_command(self): + keys = MockKeys([], None) + keys[0].commands = Commands(Sleep()) + class MockPixelListener(PixelListener): + highlight = mock.Mock() + reset = mock.Mock() + sleep = mock.Mock() + macropad = MockMacroPad() + screen = MockPixelListener(macropad) + screen.register(keys) + screen.pressed(keys, 0) + + screen.sleep.assert_called_once() diff --git a/tests/test_screen.py b/tests/test_screen.py new file mode 100644 index 0000000..9fbf3af --- /dev/null +++ b/tests/test_screen.py @@ -0,0 +1,205 @@ +from unittest import mock, TestCase +from keys import Keys, Key +from screen import ScreenListener +from commands import Commands, Sleep +from adafruit_display_shapes.rect import Rect + +class MockKeys(Keys): + def __init__(self, listeners, app): + self.keys = [ + Key(mock.Mock(), "Test01"), + Key(mock.Mock(), "Test02"), + Key(mock.Mock(), "Test03"), + Key(mock.Mock(), "Test04"), + Key(mock.Mock(), "Test05"), + Key(mock.Mock(), "Test06"), + Key(mock.Mock(), "Test07"), + Key(mock.Mock(), "Test08"), + Key(mock.Mock(), "Test09"), + Key(mock.Mock(), "Test10"), + Key(mock.Mock(), "Test11"), + Key(mock.Mock(), "Test12"), + Key(mock.Mock(), "TestButton"), + Key(mock.Mock(), "TestLeft"), + Key(mock.Mock(), "TestRight"), + ] + +class MockMacroPad: + def __init__(self): + self.display = MockDisplay() + +class MockDisplay: + def __init__(self): + self.width = 128 + self.height = 64 + self.release_all = mock.Mock() + self.refresh = mock.Mock() + +class TestScreen(TestCase): + def test_initalize(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + + elementCount = len(screen.display.root_group) + self.assertEqual(screen.display.root_group[0].color, 0xFFFFFF) + self.assertIsInstance(screen.display.root_group[elementCount - 2], Rect) + self.assertEqual(screen.display.root_group[elementCount - 1].color, 0x000000) + + def test_sleep(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.sleep() + + self.assertEqual(len(screen.display.root_group), 0) + self.assertEqual(screen.display.brightness, 0) + + def test_sleep_twice(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.sleep() + screen.sleep() + + screen.display.refresh.assert_called_once() + + def test_resume(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.sleep() + screen.resume() + + self.assertEqual(len(screen.display.root_group), 14) + self.assertEqual(screen.display.brightness, 1) + + def test_resume_twice(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.sleep() + + screen.display.refresh.reset_mock() + screen.resume() + screen.resume() + + screen.display.refresh.assert_called_once() + + def test_highlight(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.highlight(1) + + self.assertEqual(screen.display.root_group[0].color, 0xFFFFFF) + self.assertEqual(screen.display.root_group[0].background_color, 0x000000) + self.assertEqual(screen.display.root_group[1].color, 0x000000) + self.assertEqual(screen.display.root_group[1].background_color, 0xFFFFFF) + + def test_reset(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.highlight(1) + screen.reset(1) + + self.assertEqual(screen.display.root_group[0].color, 0xFFFFFF) + self.assertEqual(screen.display.root_group[0].background_color, 0x000000) + self.assertEqual(screen.display.root_group[1].color, 0xFFFFFF) + self.assertEqual(screen.display.root_group[1].background_color, 0x000000) + + def test_title(self): + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.group[1].text = "Two" + screen.setTitle("Title") + + self.assertEqual(screen.group[1].text, 'Two') + self.assertEqual(screen.group[13].text, 'Title') + + def test_set_keys(self): + keys = MockKeys([], None) + macropad = MockMacroPad() + screen = ScreenListener(macropad) + screen.initialize() + screen.register(keys) + + self.assertEqual(screen.group[0].text, 'Test01') + self.assertEqual(screen.group[1].text, 'Test02') + self.assertEqual(screen.group[2].text, 'Test03') + self.assertEqual(screen.group[13].text, '') + + def test_press(self): + keys = MockKeys([], None) + class MockScreenListener(ScreenListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + screen = MockScreenListener(macropad) + screen.initialize() + screen.register(keys) + screen.pressed(keys, 1) + + screen.highlight.assert_called_once() + screen.reset.assert_not_called() + + def test_release(self): + keys = MockKeys([], None) + class MockScreenListener(ScreenListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + screen = MockScreenListener(macropad) + screen.initialize() + screen.register(keys) + screen.released(keys, 1) + + screen.reset.assert_called_once() + screen.highlight.assert_not_called() + + def test_press_encoder(self): + keys = MockKeys([], None) + class MockScreenListener(ScreenListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + screen = MockScreenListener(macropad) + screen.initialize() + screen.register(keys) + + screen.display.refresh.reset_mock() + screen.pressed(keys, Keys.KEY_ENC_BUTTON) + + screen.display.refresh.assert_not_called() + + def test_release_encoder(self): + keys = MockKeys([], None) + class MockScreenListener(ScreenListener): + highlight = mock.Mock() + reset = mock.Mock() + macropad = MockMacroPad() + screen = MockScreenListener(macropad) + screen.initialize() + screen.register(keys) + + screen.display.refresh.reset_mock() + screen.released(keys, Keys.KEY_ENC_BUTTON) + + screen.display.refresh.assert_not_called() + + def test_sleep_command(self): + keys = MockKeys([], None) + keys[0].commands = Commands(Sleep()) + class MockScreenListener(ScreenListener): + highlight = mock.Mock() + reset = mock.Mock() + sleep = mock.Mock() + macropad = MockMacroPad() + screen = MockScreenListener(macropad) + screen.initialize() + screen.register(keys) + screen.pressed(keys, 0) + + screen.sleep.assert_called_once()