-
Notifications
You must be signed in to change notification settings - Fork 22
Revamp UI interactions #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jcollins1983
wants to merge
58
commits into
PyFixate:main
Choose a base branch
from
jcollins1983:revamp-ui
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 32 commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
1673d57
make reformat_text private
jcollins1983 ba6066a
beginnings of (hopefully) improving the UI logic
jcollins1983 55af830
switch to the more descriptive UserInputError from the Fixate excepti…
jcollins1983 844682c
switch to new user_serial from _ui.py
jcollins1983 43c0b69
add user_yes_no and _user_abort_retry
jcollins1983 8cdc998
add _user_choices_
jcollins1983 aac14da
add space after choices string
jcollins1983 bfa1c55
make get_user_input private
jcollins1983 02b228f
gui for user choices done
jcollins1983 b56c71a
add some colour to the user_info_important call
jcollins1983 f4bfb2b
make a bit easier to follow
jcollins1983 0bef7a3
add user_info and user_info_important
jcollins1983 0ae712a
add user_ok
jcollins1983 a9833b8
add user_action
jcollins1983 7ed5d93
add some missing type hints
jcollins1983 7da8de8
add _user_image so indicate that an image would have been displayed
jcollins1983 97affa8
make information standout more
jcollins1983 3c790bc
fix typo
jcollins1983 aca4d91
add user image and gif functionality
jcollins1983 1e0247f
add doc strings
jcollins1983 b4d2b5e
include message RE use of GIFs in ccommand line
jcollins1983 2adc228
add post sequence display functions
jcollins1983 62f7355
move logic out of cmd and qt UIs into ui.py
jcollins1983 c08f417
adjust tests to account for movement of logic from ui to ui controlle…
jcollins1983 5aef5e7
fix a whoopsie
jcollins1983 5eca4af
fix docstring
jcollins1983 12301bd
probably don't need anything other than int or string for serial numbers
jcollins1983 382f2b5
add tests for new _ui.py
jcollins1983 4e7428c
fix tests
jcollins1983 46f3f47
add a bit more flexibility for the user_info_important colours in the…
jcollins1983 5c92d03
add test to ensure Issue #213 is not repeated
jcollins1983 cc7fe83
Merge branch 'PyFixate:main' into revamp-ui
jcollins1983 42ffa0d
fix typo in comment
jcollins1983 89631c3
Merge branch 'main' into revamp-ui
jcollins1983 a5db621
Merge branch 'main' into revamp-ui
jcollins1983 6ef77b5
remove the 'Result' part of the q.put
jcollins1983 8bfefc7
fix the things that were getting in the way of the UI tests
jcollins1983 5c0a2db
make mypy happy
jcollins1983 e4b9c22
remove unused import
jcollins1983 5115025
switch to consistent use of f-strings
jcollins1983 45f2e84
remove unused import
jcollins1983 3f65a18
addressing review comments
jcollins1983 70bfe63
add multiplier to account for width of !
jcollins1983 5cc7272
add manual tests for UI
jcollins1983 783ec7f
add file used in my setup
jcollins1983 2096a71
update release notes
jcollins1983 a72ad7a
one day I will remember all of these things together... bump version
jcollins1983 a6bb641
Merge branch 'main' into revamp-ui
jcollins1983 5b088e6
address review comments round 2 and bump black version
jcollins1983 b8ada04
sync black version to pre-commit...
jcollins1983 e131436
remove black from tox
jcollins1983 a1f0c65
forgot the envlist entry for removing black
jcollins1983 92be7d5
update mypy version to latest
jcollins1983 eca5ab7
remove tox -e black from workflows
jcollins1983 f0fec56
fix typos
jcollins1983 48a623d
Merge branch 'main' into revamp-ui
jcollins1983 428b5b8
Merge branch 'main' into revamp-ui
jcollins1983 5ad6ce2
black on new files
jcollins1983 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,341 @@ | ||
| """ | ||
| This module provides the user interface for fixate. It is agnostic of the | ||
| actual implementation of the UI and provides a standard set of functions used | ||
| to obtain or display information from/to the user. | ||
| """ | ||
|
|
||
| from typing import Callable, Any | ||
| from queue import Queue, Empty | ||
| from enum import StrEnum | ||
| import time | ||
| from pubsub import pub | ||
|
|
||
| # going to honour the post sequence info display from `ui.py` | ||
| from fixate.config import RESOURCES | ||
| from fixate.core.exceptions import UserInputError | ||
| from collections import OrderedDict | ||
|
|
||
|
|
||
| class Validator: | ||
| """ | ||
| Defines a validator object that can be used to validate user input. | ||
| """ | ||
|
|
||
| def __init__(self, func: Callable[[Any], bool], errror_msg: str = "Invalid input"): | ||
| """ | ||
| Args: | ||
| func (function): The function to validate the input | ||
| error_msg (str): The message to display if the input is invalid | ||
| """ | ||
| self.func = func | ||
| self.error_msg = errror_msg | ||
|
|
||
| def __call__(self, resp: Any) -> bool: | ||
| """ | ||
| Args: | ||
| resp (Any): The response to validate | ||
|
|
||
| Returns: | ||
| bool: True if the response is valid, False otherwise | ||
| """ | ||
| return self.func(resp) | ||
|
|
||
| def __str__(self) -> str: | ||
| return self.error_msg | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| class UiColour(StrEnum): | ||
|
jcollins1983 marked this conversation as resolved.
|
||
| RED = "red" | ||
| GREEN = "green" | ||
| BLUE = "blue" | ||
| YELLOW = "yellow" | ||
| WHITE = "white" | ||
| BLACK = "black" | ||
| CYAN = "cyan" | ||
| MAGENTA = "magenta" | ||
| GREY = "grey" | ||
|
|
||
|
|
||
| def _user_request_input(msg: str): | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| q = Queue() | ||
| pub.sendMessage("UI_block_start") | ||
| pub.sendMessage("UI_req_input", msg=msg, q=q) | ||
| resp = q.get() | ||
| pub.sendMessage("UI_block_end") | ||
| return resp | ||
|
|
||
|
|
||
| def user_input(msg: str) -> str: | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| """ | ||
| A blocking function that asks the UI to ask the user for raw input. | ||
|
|
||
| Args: | ||
| msg (str): A message that will be shown to the user | ||
|
|
||
| Returns: | ||
| resp (str): The user response from the UI | ||
| """ | ||
| return _user_request_input(msg) | ||
|
|
||
|
|
||
| def user_input_float(msg: str, attempts: int = 5) -> float: | ||
| """ | ||
| A blocking function that asks the UI to ask the user for input and converts the response to a float. | ||
|
|
||
| Args: | ||
| msg (str): A message that will be shown to the user | ||
| attempts (int): Number of attempts the user has to get the input right | ||
|
|
||
| Returns: | ||
| resp (float): The converted user response from the UI | ||
|
|
||
| Raises: | ||
| UserInputError: If the user fails to enter a number after the specified number of attempts | ||
| """ | ||
| resp = _user_request_input(msg) | ||
|
daniel-montanari marked this conversation as resolved.
Outdated
|
||
| for _ in range(attempts): | ||
| try: | ||
| return float(resp) | ||
| except ValueError: | ||
| pub.sendMessage( | ||
| "UI_display_important", msg="Invalid input, please enter a number" | ||
| ) | ||
| resp = _user_request_input(msg) | ||
| raise UserInputError("User failed to enter a number") | ||
|
|
||
|
|
||
| def _ten_digit_int_serial(serial: str) -> bool: | ||
| return len(serial) == 10 and serial.isdigit() | ||
|
|
||
|
|
||
| _ten_digit_int_serial_v = Validator( | ||
| _ten_digit_int_serial, "Please enter a 10 digit serial number" | ||
| ) | ||
|
|
||
|
|
||
| def user_serial( | ||
| msg: str, | ||
| validator: Validator = _ten_digit_int_serial_v, | ||
| return_type: int | str = int, | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| attempts: int = 5, | ||
| ) -> Any: | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| """ | ||
| A blocking function that asks the UI to ask the user for a serial number. | ||
|
|
||
| Args: | ||
| msg (str): A message that will be shown to the user | ||
| validator (Validator): An optional function to validate the serial number, | ||
| defaults to checking for a 10 digit integer. This function shall return | ||
| True if the serial number is valid, False otherwise. | ||
| return_type (int | str): The type to return the serial number as, defaults to int | ||
|
|
||
| Returns: | ||
| resp (str): The user response from the UI | ||
| """ | ||
| resp = _user_request_input(msg) | ||
|
daniel-montanari marked this conversation as resolved.
Outdated
|
||
| for _ in range(attempts): | ||
| if validator(resp): | ||
| return return_type(resp) | ||
| pub.sendMessage("UI_display_important", msg=f"Invalid input: {validator}") | ||
| resp = _user_request_input(msg) | ||
| raise UserInputError("User failed to enter the correct format serial number") | ||
|
|
||
|
|
||
| def _user_req_choices(msg: str, choices: tuple): | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| # TODO - do we really need this check since this is a private function and any callers should be calling correctly | ||
| if len(choices) < 2: | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| raise ValueError(f"Requires at least two choices to work, {choices} provided") | ||
| q = Queue() | ||
| pub.sendMessage("UI_block_start") | ||
| pub.sendMessage("UI_req_choices", msg=msg, q=q, choices=choices) | ||
| resp = q.get() | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| pub.sendMessage("UI_block_end") | ||
| return resp | ||
|
|
||
|
|
||
| def _choice_from_response(choices: tuple, resp: str) -> str | bool: | ||
|
jcollins1983 marked this conversation as resolved.
Outdated
|
||
| for choice in choices: | ||
| if resp.startswith(choice[0]): | ||
|
daniel-montanari marked this conversation as resolved.
Outdated
|
||
| return choice | ||
| return False | ||
|
|
||
|
|
||
| def _user_choices(msg: str, choices: tuple, attempts: int = 5) -> str: | ||
| resp = _user_req_choices(msg, choices).upper() | ||
|
daniel-montanari marked this conversation as resolved.
Outdated
|
||
| for _ in range(attempts): | ||
| choice = _choice_from_response(choices, resp) | ||
| if choice: | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| return choice | ||
| pub.sendMessage( | ||
| "UI_display_important", | ||
| msg="Invalid input, please enter a valid choice; first letter or full word", | ||
| ) | ||
| resp = _user_req_choices(msg, choices).upper() | ||
| raise UserInputError("User failed to enter a valid response") | ||
|
|
||
|
|
||
| def user_yes_no(msg: str, attempts: int = 1) -> str: | ||
| """ | ||
| A blocking function that asks the UI to ask the user for a yes or no response. | ||
|
|
||
| Args: | ||
| msg (str): A message that will be shown to the user | ||
|
|
||
| Returns: | ||
| resp (str): 'YES' or 'NO' | ||
| """ | ||
| CHOICES = ("YES", "NO") | ||
| return _user_choices(msg, CHOICES, attempts) | ||
|
|
||
|
|
||
| def _user_retry_abort_fail(msg: str, attempts: int = 1) -> str: | ||
|
daniel-montanari marked this conversation as resolved.
Outdated
|
||
| CHOICES = ("RETRY", "ABORT", "FAIL") | ||
| return _user_choices(msg, CHOICES, attempts) | ||
|
|
||
|
|
||
| def user_info(msg: str): | ||
| pub.sendMessage("UI_display", msg=msg) | ||
|
|
||
|
|
||
| def user_info_important( | ||
| msg: str, colour: UiColour = UiColour.RED, bg_colour: UiColour = UiColour.WHITE | ||
| ): | ||
| pub.sendMessage("UI_display_important", msg=msg, colour=colour, bg_colour=bg_colour) | ||
|
|
||
|
|
||
| def user_ok(msg: str): | ||
| """ | ||
| A blocking function that asks the UI to display a message and waits for the user to press OK/Enter. | ||
| """ | ||
| pub.sendMessage("UI_block_start") | ||
| pub.sendMessage("UI_req", msg=msg) | ||
| pub.sendMessage("UI_block_end") | ||
|
|
||
|
|
||
| def user_action(msg: str, action_monitor: Callable[[], bool]) -> bool: | ||
| """ | ||
| Prompts the user to complete an action. | ||
| Actively monitors the target infinitely until the event is detected or a user fail event occurs | ||
|
|
||
| Args: | ||
| msg (str): Message to display to the user | ||
| action_monitor (function): A function that will be called until the user action is cancelled. The function | ||
| should return False if it hasn't completed. If the action is finished return True. | ||
|
|
||
| Returns: | ||
| bool: True if the action is finished, False otherwise | ||
| """ | ||
| # UserActionCallback is used to handle the cancellation of the action either by the user or by the action itself | ||
| class UserActionCallback: | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| def __init__(self): | ||
| # The UI implementation must provide queue.Queue object. We | ||
| # monitor that object. If it is non-empty, we get the message | ||
| # in the q and cancel the target call. | ||
| self.user_cancel_queue = None | ||
|
|
||
| # In the case that the target exists the user action instead | ||
| # of the user, we need to tell the UI to do any clean up that | ||
| # might be required. (e.g. return GUI buttons to the default state | ||
| # Does not need to be implemented by the UI. | ||
| # Function takes no args and should return None. | ||
| self.target_finished_callback = lambda: None | ||
|
|
||
| def set_user_cancel_queue(self, cancel_queue): | ||
| self.user_cancel_queue = cancel_queue | ||
|
|
||
| def set_target_finished_callback(self, callback): | ||
| self.target_finished_callback = callback | ||
|
|
||
| callback_obj = UserActionCallback() | ||
| pub.sendMessage("UI_action", msg=msg, callback_obj=callback_obj) | ||
| try: | ||
| while True: | ||
| try: | ||
| callback_obj.user_cancel_queue.get_nowait() | ||
|
daniel-montanari marked this conversation as resolved.
|
||
| return False | ||
| except Empty: | ||
| pass | ||
|
|
||
| if action_monitor(): | ||
| return True | ||
|
|
||
| # Yield control for other threads but don't slow down target | ||
| time.sleep(0) | ||
| finally: | ||
| # No matter what, if we exit, we want to reset the UI | ||
| callback_obj.target_finished_callback() | ||
|
|
||
|
|
||
| def user_image(path: str): | ||
| """ | ||
| Display an image to the user | ||
|
|
||
| Args: | ||
| path (str): The path to the image file. The underlying library does not take a pathlib.Path object. | ||
| """ | ||
| pub.sendMessage("UI_image", path=path) | ||
|
|
||
|
|
||
| def user_image_clear(): | ||
| """ | ||
| Clear the image canvas | ||
| """ | ||
| pub.sendMessage("UI_image_clear") | ||
|
|
||
|
|
||
| def user_gif(path: str): | ||
| """ | ||
| Display a gif to the user | ||
|
|
||
| Args: | ||
| path (str): The path to the gif file. The underlying library does not take a pathlib.Path object. | ||
| """ | ||
| pub.sendMessage("UI_gif", path=path) | ||
|
|
||
|
|
||
| def _user_post_sequence_info(msg: str, status: str): | ||
| if "_post_sequence_info" not in RESOURCES["SEQUENCER"].context_data: | ||
| RESOURCES["SEQUENCER"].context_data["_post_sequence_info"] = OrderedDict() | ||
| RESOURCES["SEQUENCER"].context_data["_post_sequence_info"][msg] = status | ||
|
|
||
|
|
||
| def user_post_sequence_info_pass(msg: str): | ||
| """ | ||
| Adds information to be displayed to the user at the end if the sequence passes | ||
| This information will be displayed in the order that this function is called. | ||
| Multiple calls with the same message will result in the previous being overwritten. | ||
|
|
||
| This is useful for providing a summary of the sequence to the user at the end. | ||
|
|
||
| Args: | ||
| msg (str): The message to display. | ||
| """ | ||
| _user_post_sequence_info(msg, "PASSED") | ||
|
|
||
|
|
||
| def user_post_sequence_info_fail(msg: str): | ||
| """ | ||
| Adds information to be displayed to the user at the end if the sequence fails. | ||
| This information will be displayed in the order that this function is called. | ||
| Multiple calls with the same message will result in the previous being overwritten. | ||
|
|
||
| This is useful for providing a summary of the sequence to the user at the end. | ||
|
|
||
| Args: | ||
| msg (str): The message to display. | ||
| """ | ||
| _user_post_sequence_info(msg, "FAILED") | ||
|
|
||
|
|
||
| def user_post_sequence_info(msg: str): | ||
| """ | ||
| Adds information to be displayed to the user at the end of the sequence. | ||
| This information will be displayed in the order that this function is called. | ||
| Multiple calls with the same message will result in the previous being overwritten. | ||
|
|
||
| This is useful for providing a summary of the sequence to the user at the end. | ||
|
|
||
| Args: | ||
| msg (str): The message to display. | ||
| """ | ||
| _user_post_sequence_info(msg, "ALL") | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.