HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
3 stars 1 forks source link

Prompt user via dialog box. #269

Closed EvanKirshenbaum closed 8 months ago

EvanKirshenbaum commented 8 months ago

Currently, when the user needs to do something manually, the code calls:

class System:
    def prompt_and_wait(self, future: Postable[None], *, prompt: Optional[str] = None) -> None:
        def doit() -> None:
            if prompt is not None:
                print(prompt)
            print("Hit <RETURN> to continue.")
            input()
            future.post(None)
        self.terminal_interaction.enqueue(doit)

where terminal_interaction is an AsyncFunctionSerializer. This is currently used by ManualPipettor to prompt the user to add or remove reagent, but we're also going to want to use it to get the user to aim the ESELog laser (#264) and probably for other things.

This works, but all of the interaction happens on the terminal window, and this can be hard to notice when the user is focused on the GUI. What we'd like, when we have a display, is to have it pop up a dialog box and wait for the user to hit Continue.

I was chatting with ChatGPT, and we came up with the following non-blocking dialog box class (Tkinter's dialog boxes all block):

class NonBlockingDialog:
    def __init__(self, root: tkinter.Tk, title: str, message: str, buttons: Sequence[str]):
        self.root = root
        self.answer: Optional[str] = None

        self.window = tkinter.Toplevel(root)
        self.window.title(title)  # this line sets the window's title
        self.window.attributes('-topmost', 1)
        self.label = tkinter.Label(self.window, text=message, wraplength=200)
        self.label.pack()

        for button_text in buttons:
            button = tkinter.Button(self.window, text=button_text, command=lambda text=button_text: self.button_clicked(text))
            button.pack()

    def button_clicked(self, text: str):
        self.answer = text
        self.window.destroy()

This would be created in the GUI thread using monitor.in_display_thread(fn), which would, I presume, result in all of the other interaction (including the destroy()) happening there.

For our purposes, rather than caching an answer there, we'd want either a callback function or a Postable[T] where the buttons are a Sequence[str|tuple[str,T]], with the second item being the thing that is posted or passed to the function, defaulting to the string itself. (Although thinking about it, that may be more complicated than is warranted. Let's just start with the values always being the strings and count on the caller to know what to do with them.)

Migrated from internal repository. Originally created by @EvanKirshenbaum on May 16, 2023 at 2:59 PM PDT. Closed on May 17, 2023 at 11:37 AM PDT.
EvanKirshenbaum commented 8 months ago

This issue was referenced by the following commit before migration:

EvanKirshenbaum commented 8 months ago

Okay, it was a little more complicated than that, but not much. The System.prompt_and_wait() now pops up a dialog box if there's a BoardMonitor. Confirmed with manual pipettor.

While doing this, noticed that in combinatorial synthesis code, this necessitates guarding the transfers into the extraction point with a trigger so that the second one isn't prompted until the first one has moved away.

I also took this opportunity to add a rounding config param for the manual pipettor, so that with the new weird drop size it isn't asking for overly precise amounts.

Migrated from internal repository. Originally created by @EvanKirshenbaum on May 17, 2023 at 11:37 AM PDT.