From 457b238e0215d62586425951ca695c5a95cda44a Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 25 Aug 2020 19:01:15 +0800 Subject: [PATCH 01/83] Refactoring of command code to separate the main code from each of the commands. This also makes it a lot easier to see what a command does, to modify commands, or to add new ones. This version has no new commands. --- LPHK.py | 3 + command_base.py | 102 ++++ commands_control.py | 503 +++++++++++++++++ commands_external.py | 323 +++++++++++ commands_header.py | 187 +++++++ commands_keys.py | 335 ++++++++++++ commands_mouse.py | 697 ++++++++++++++++++++++++ commands_pause.py | 63 +++ scripts.py | 1229 ++++++++++++++---------------------------- 9 files changed, 2610 insertions(+), 832 deletions(-) create mode 100644 command_base.py create mode 100644 commands_control.py create mode 100644 commands_external.py create mode 100644 commands_header.py create mode 100644 commands_keys.py create mode 100644 commands_mouse.py create mode 100644 commands_pause.py diff --git a/LPHK.py b/LPHK.py index efd96b9..693e4b5 100755 --- a/LPHK.py +++ b/LPHK.py @@ -87,6 +87,8 @@ def datetime_str(): import lp_events, scripts, kb, files, sound, window from utils import launchpad_connector +import commands_header, commands_control, commands_keys, commands_mouse, commands_pause, commands_external + lp = launchpad.Launchpad() EXIT_ON_WINDOW_CLOSE = True @@ -131,6 +133,7 @@ def shutdown(): def main(): init() window.init(lp, launchpad, PATH, PROG_PATH, USER_PATH, VERSION, PLATFORM) + print("here") if EXIT_ON_WINDOW_CLOSE: shutdown() diff --git a/command_base.py b/command_base.py new file mode 100644 index 0000000..50ac752 --- /dev/null +++ b/command_base.py @@ -0,0 +1,102 @@ +# ################################################## +# ### CLASS Command_Basic ### +# ################################################## + +# Command_Basic is a class that describes a command +class Command_Basic: + def __init__( + self, + Name: str # The name of the command (what you put in the script) + ): + + self.name = Name + + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no # pass_no 1 is a symbol gathering pass, pass_no 2 is a pass that requires + # symbols. Any processing that does not set up labels should be done on + # pass 2. For example, goto can be checked on pass 2 to ensure the label + # exists + ): + + return ("", "") # error value! + + + def Parse( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no # pass_no 1 is a symbol gathering pass, pass_no 2 is a pass that requires + # symbols. Any processing that does not set up labels should be done on + # pass 2. Fatal errors can be generated on pass 1 or 2 for invalid syntax. + # Since errors will cause the process to abort, it is permissable for + # the same error to be reported on both passes 1 and 2 (since you won't get + # past pass 1). + ): + + ret = self.Validate(idx, line, lines, split_line, symbols, pass_no) + if ret == True: + return True + + if len(ret) != 2: + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), line) + + if ret[0]: + print(ret[0]) + + return ret + + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + return idx+1 # just move to next line + + +# ################################################## +# ### CLASS Command_Header ### +# ################################################## + +# Command_Header is a class specifically defining a header command +class Command_Header(Command_Basic): + + def __init__( + self, + Name: str, # The name of the command (what you put in the script) + Is_async: bool # is this async? + ): + + super().__init__(Name) + self.is_async = Is_async + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if idx != 0: + return ("ERROR on line " + line + ". " + self.name + " must only appear on line 1.", -1) + + return (None, 0) + + diff --git a/commands_control.py b/commands_control.py new file mode 100644 index 0000000..15da953 --- /dev/null +++ b/commands_control.py @@ -0,0 +1,503 @@ +import command_base, lp_events, scripts + + +# ################################################## +# ### CLASS Control_Comment ### +# ################################################## + +# class that defines the comment command (single quote at beginning of line) +# this is special because it has some different handling in the main sode +# to allow it to work without a space following it +class Control_Comment(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("-") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + return True # never return an error + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " comment: " + split_line[1]) + + return idx+1 + + +scripts.add_command(Control_Comment()) + + +# ################################################## +# ### CLASS Control_Label ### +# ################################################## + +# class that defines the LABEL command (a target of GOTO's etc) +class Control_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + # check number of split_line + if len(split_line) != 2: + return ("Wrong number of parameters in " + self.Name, line) + + # check for duplicate label + if split_line[1] in symbols["labels"]: + return ("duplicate LABEL", line) + + # add label to symbol table + symbols["labels"][split_line[1]] = idx + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " Label: " + split_line[1]) + + return idx+1 + + +scripts.add_command(Control_Label()) + + +# ################################################## +# ### CLASS Control_Goto_Label ### +# ################################################## + +# class that defines the GOTO_LABEL command +class Control_Goto_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("GOTO_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + # check number of split_line + if len(split_line) != 2: + return ("Wrong number of parameters in " + self.Name, line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + # check for label + if split_line[1] in symbols["labels"]: + return symbols["labels"][split_line[1]] + else: + print("missing LABEL '" + split_line[1] + "'") + return -1 + + return idx+1 + + +scripts.add_command(Control_Goto_Label()) + + +# ################################################## +# ### CLASS Control_If_Pressed_Goto_Label ### +# ################################################## + +# class that defines the IF_PRESSED_GOTO_LABEL command +class Control_If_Pressed_Goto_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_PRESSED_GOTO_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) != 2: + return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " If key is pressed goto LABEL " + split_line[1]) + if lp_events.pressed[coords[1]][coords[2]]: + return symbols["labels"][split_line[1]] + + return idx+1 + + +scripts.add_command(Control_If_Pressed_Goto_Label()) + + +# ################################################## +# ### CLASS Control_If_Unpressed_Goto_Label ### +# ################################################## + +# class that defines the IF_UNPRESSED_GOTO_LABEL command +class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_UNPRESSED_GOTO_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) != 2: + return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + if not lp_events.pressed[coords[1]][coords[2]]: + return symbols["labels"][split_line[1]] + + return idx+1 + + +scripts.add_command(Control_If_Unpressed_Goto_Label()) + + +# ################################################## +# ### CLASS Control_Repeat_Label ### +# ################################################## + +# class that defines the REPEAT_LABEL command +class Control_Repeat_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("REPEAT_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + else: + symbols["repeats"][idx] = int(split_line[2]) + symbols["original"][idx] = int(split_line[2]) + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ + split_line[2] + " times max") + + if symbols["repeats"][idx] > 0: + print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") + + return idx+1 + + +scripts.add_command(Control_Repeat_Label()) + + +# ################################################## +# ### CLASS Control_If_Pressed_Repeat_Label ### +# ################################################## + +# class that defines the IF_PRESSED_REPEAT_LABEL command +class Control_If_Pressed_Repeat_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_PRESSED_REPEAT_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + else: + symbols["repeats"][idx] = int(split_line[2]) + symbols["original"][idx] = int(split_line[2]) + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + + if lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") + + return idx+1 + + +scripts.add_command(Control_If_Pressed_Repeat_Label()) + + +# ################################################## +# ### CLASS Control_If_Unpressed_Repeat_Label ### +# ################################################## + +# class that defines the IF_UNPRESSED_REPEAT_LABEL command. +class Control_If_Unpressed_Repeat_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_UNPRESSED_REPEAT_LABEL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + symbols["repeats"][idx] = int(split_line[2]) + symbols["original"][idx] = int(split_line[2]) + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.Name, line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + + if not lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") + + return idx+1 + + +scripts.add_command(Control_If_Unpressed_Repeat_Label()) + + +# ################################################## +# ### CLASS Control_Reset_Repeats ### +# ################################################## + +# class that defines the RESET_REPEATS command +class Control_Reset_Repeats(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RESET_REPEATS") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if len(split_line) > 1: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_ctrl] " + coords[0] + " Reset all repeats") + + for i in symbols["repeats"]: + symbols["repeats"][i] = symbols["original"][i] + + return idx+1 + + +scripts.add_command(Control_Reset_Repeats()) + + diff --git a/commands_external.py b/commands_external.py new file mode 100644 index 0000000..c12d250 --- /dev/null +++ b/commands_external.py @@ -0,0 +1,323 @@ +import command_base, webbrowser, sound, subprocess, os, scripts + + +# ################################################## +# ### CLASS External_Web ### +# ################################################## + +# class that defines the WEB command +class External_Web(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("WEB") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + link = split_line[1] + if "http" not in link: + link = "http://" + link + + print("[cmds_extn] " + coords[0] + " Open website " + link + " in default browser") + + webbrowser.open(link) + + return idx+1 + + +scripts.add_command(External_Web()) + + +# ################################################## +# ### CLASS External_Web_New ### +# ################################################## + +# class that defines the WEB_NEW command +class External_Web_New(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("WEB_NEW") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + link = split_line[1] + if "http" not in link: + link = "http://" + link + + print("[cmds_extn] " + coords[0] + " Open website " + link + " in default browser, try to make a new window") + + webbrowser.open_new(link) + + return idx+1 + + +scripts.add_command(External_Web_New()) + + +# ################################################## +# ### CLASS External_Open ### +# ################################################## + +# class that defines the OPEN command +class External_Open(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("OPEN") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + path_name = " ".join(split_line[1:]) + + if (not os.path.isfile(path_name)) and (not os.path.isdir(path_name)): + return (split_line[0] + " folder or file location '" + path_name + \ + "' does not exist.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + path_name = " ".join(split_line[1:]) + + print("[cmds_extn] " + coords[0] + " Open file or folder " + path_name) + + files.open_file_folder(path_name) + + return idx+1 + + +scripts.add_command(External_Open()) + + +# ################################################## +# ### CLASS External_Sound ### +# ################################################## + +# class that defines the SOUND command (plays a sound file) +class External_Sound(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("SOUND") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 3: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + if len(split_line) > 2: + print("[cmds_extn] " + coords[0] + " Play sound file " + split_line[1] + \ + " at volume " + str(split_line[2])) + sound.play(split_line[1], float(split_line[2])) + else: + print("[cmds_extn] " + coords[0] + " Play sound file " + split_line[1]) + sound.play(split_line[1]) + + return idx+1 + + +scripts.add_command(External_Sound()) + + +# ################################################## +# ### CLASS External_Sound_STOP ### +# ################################################## + +# class that defines the SOUND_STOP command (stops sound) +class External_Sound_Stop(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("SOUND_STOP") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + if len(split_line) > 1: + delay = split_line[1] + print("[scripts] " + coords + + " Stopping sounds with " + delay + " milliseconds fadeout time") + sound.fadeout(int(delay)) + else: + print("[scripts] " + coords + " Stopping sounds") + sound.stop() + + return idx+1 + + +scripts.add_command(External_Sound()) + + +# ################################################## +# ### CLASS External_Code ### +# ################################################## + +# class that defines the CODE command (runs something) +class External_Code(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("CODE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + args = " ".join(split_line[1:]) + print("[cmds_extn] " + coords[0] + " Running code: " + args) + + try: + subprocess.run(args) + except Exception as e: + print("[cmds_extn] " + coords[0] + " Error with running code: " + str(e)) + + return idx+1 + + +scripts.add_command(External_Code()) + + diff --git a/commands_header.py b/commands_header.py new file mode 100644 index 0000000..e5c66bb --- /dev/null +++ b/commands_header.py @@ -0,0 +1,187 @@ +import command_base, kb, lp_events, scripts + + +# ################################################## +# ### CLASS Header_Async ### +# ################################################## + +class Header_Async(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@ASYNC", True) + + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if idx > 0: + return ("@ASYNC must appear on the first line.", lines[0]) + + if len(split_line) > 1: + return ("@ASYNC takes no arguments.", lines[0]) + + return True + + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + return idx+1 + + +scripts.add_command(Header_Async()) + + +# ################################################## +# ### CLASS Header_Simple ### +# ################################################## + +class Header_Simple(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@SIMPLE", False) + + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if idx > 0: + return ("@ASYNC must appear on the first line.", lines[0]) + + if len(split_line) < 2: + return ("@SIMPLE requires a key to bind.", line) + + if len(split_line) > 2: + return ("@SIMPLE only take one argument", line) + + if kb.sp(split_line[1]) == None: + return ("No key named '" + split_line[1] + "'.", line) + + for lin in lines[1:]: + if lin != "" and lin[0] != "-": + return ("When @SIMPLE is used, scripts can only contain comments.", lin) + + return True + + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_head] " + coords + " Simple keybind: " + split_line[1]) + + #PRESS + key = kb.sp(split_line[1]) + releasefunc = lambda: kb.release(key) + kb.press(key) + + #WAIT_UNPRESSED + while lp_events.pressed[x][y]: + sleep(DELAY_EXIT_CHECK) + if check_kill(x, y, is_async, releasefunc): + return idx + 1 + + #RELEASE + kb.release(key) + + return idx+1 + + +scripts.add_command(Header_Simple()) + + +# ################################################## +# ### CLASS Header_Load_Layout ### +# ################################################## + +class Header_Load_Layout(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@LOAD_LAYOUT", False) + + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("@LOAD_LAYOPUT requires a filename as a parameter.", line) + + return True + + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + layout_name = " ".join(split_line[1:]) + + print("[cmds_head] " + coords + " Load layout " + layout_name) + + layout_path = os.path.join(files.LAYOUT_PATH, layout_name) + if not os.path.isfile(layout_path): + print("[cmds_head] " + coords + " ERROR: Layout file does not exist.") + return -1 + + try: + layout = files.load_layout(layout_path, popups=False, save_converted=False) + except files.json.decoder.JSONDecodeError: + print("[cmds_head] " + coords + " ERROR: Layout is malformated.") + return -1 + + if files.layout_changed_since_load: + files.save_lp_to_layout(files.curr_layout) + + files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout) + + return idx+1 + + +scripts.add_command(Header_Load_Layout()) + + diff --git a/commands_keys.py b/commands_keys.py new file mode 100644 index 0000000..fca6860 --- /dev/null +++ b/commands_keys.py @@ -0,0 +1,335 @@ +import command_base, kb, lp_events, scripts + + +# ################################################## +# ### CLASS Keys_Wait_Pressed ### +# ################################################## + +# class that defines the WAIT_PRESSED command (wait while a button is pressed?) +class Keys_Wait_Pressed(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("WAIT_PRESSED") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 1: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async, + pass_no + ): + + print("[cmds_keys] " + coords + " Wait for script key to be unpressed") + + while lp_events.pressed[coords[1]][coords[2]]: + sleep(DELAY_EXIT_CHECK) + if check_kill(x, y, is_async): + return idx + 1 + + return idx+1 + + +scripts.add_command(Keys_Wait_Pressed()) + + +# ################################################## +# ### CLASS Keys_Tap ### +# ################################################## + +# class that defines the TAP command (tap button a button) +class Keys_Tap(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TAP") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 4: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + if kb.sp(split_line[1]) == None: + return ("No key named '" + split_line[1] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + key = kb.sp(split_line[1]) + + releasefunc = lambda: kb.release(key) + + if len(split_line) <= 2: + print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1]) + kb.tap(key) + elif len(split_line) <= 3: + print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + " times") + taps = int(split_line[2]) + + for tap in range(taps): + if check_kill(coords[1], coords[2], is_async, releasefunc): + return idx + 1 + kb.tap(key) + else: + print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + \ + " times for " + str(split_line[3]) + " seconds each") + + taps = int(split_line[2]) + delay = float(split_line[3]) + + for tap in range(taps): + if check_kill(coords[1], coords[2], is_async, releasefunc): + return -1 + + kb.press(key) + if not safe_sleep(delay, coords[1], coords[2], is_async, releasefunc): + return -1 + + return idx+1 + + +scripts.add_command(Keys_Tap()) + + +# ################################################## +# ### CLASS Keys_Press ### +# ################################################## + +# class that defines the PRESS command (press a button) +class Keys_Press(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("PRESS") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + if kb.sp(split_line[1]) == None: + return ("No key named '" + split_line[1] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_keys] " + coords[0] + " Press key " + split_line[1]) + + key = kb.sp(split_line[1]) + kb.press(key) + + return idx+1 + + +scripts.add_command(Keys_Press()) + + +# ################################################## +# ### CLASS Keys_Release ### +# ################################################## + +# class that defines the RELEASE command (un-press a button) +class Keys_Release(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RELEASE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + if kb.sp(split_line[1]) == None: + return ("No key named '" + split_line[1] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_keys] " + coords[0] + " Release key " + split_line[1]) + + key = kb.sp(split_line[1]) + kb.release(key) + + return idx+1 + + +scripts.add_command(Keys_Release()) + + +# ################################################## +# ### CLASS Keys_Release_All ### +# ################################################## + +# class that defines the RELEASE_ALL command (un-press all keys) +class Keys_Release_All(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RELEASE_ALL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 1: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_keys] " + coords[0] + " Release all keys") + + kb.release_all() + + return idx+1 + + +scripts.add_command(Keys_Release_All()) + + +# ################################################## +# ### CLASS Keys_String ### +# ################################################## + +# class that defines the STRING command (type a string) +class Keys_String(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("STRING") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + type_string = " ".join(split_line[1:]) + + print("[cmds_keys] " + coords[0] + " Type out string " + type_string) + + kb.write(type_string) + + return idx+1 + + +scripts.add_command(Keys_String()) diff --git a/commands_mouse.py b/commands_mouse.py new file mode 100644 index 0000000..891871a --- /dev/null +++ b/commands_mouse.py @@ -0,0 +1,697 @@ +import command_base, ms, scripts + + +# ################################################## +# ### CLASS Mouse_Move ### +# ################################################## + +# class that defines the M_MOVE command (wait while a button is pressed?) +class Mouse_Move(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_MOVE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + # no longer allow just 2 tokens + + if pass_no == 1: + if len(split_line) < 3: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 3: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + try: + temp = int(split_line[1]) + except: + return ("'M_MOVE' X value '" + split_line[1] + "' not valid.", line) + + try: + temp = int(split_line[2]) + except: + return ("'M_MOVE' Y value '" + split_line[2] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + # removed error for != 3 tokens + + print("[cmds_mous] " + coords[0] + " Relative mouse movement (" + split_line[1] + ", " + \ + str(split_line[2]) + ")") + + ms.move_to_pos(float(split_line[1]), float(split_line[2])) + + return idx+1 + + +scripts.add_command(Mouse_Move()) + + +# ################################################## +# ### CLASS Mouse_Set ### +# ################################################## + +# class that defines the M_SET command (put the mouse somewhere) +class Mouse_Set(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_SET") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + # no longer allow just 2 tokens + + if pass_no == 1: + if len(split_line) < 3: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 3: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + try: + temp = int(split_line[1]) + except: + return ("'M_SET' X value '" + split_line[1] + "' not valid.", line) + + try: + temp = int(split_line[2]) + except: + return ("'M_SET' Y value '" + split_line[2] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + # removed error for != 3 tokens + + print("[cmds_mous] " + coords[0] + " Set mouse position to (" + split_line[1] + ", " + \ + str(split_line[2]) + ")") + + ms.set_pos(float(split_line[1]), float(split_line[2])) + + return idx+1 + + +scripts.add_command(Mouse_Set()) + + +# ################################################## +# ### CLASS Mouse_Scroll ### +# ################################################## + +# class that defines the M_SCROLL command (???) +class Mouse_Scroll(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_SCROLL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 3: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + try: + temp = int(split_line[1]) + except: + return ("'M_SET' X value '" + split_line[1] + "' not valid.", line) + + try: + temp = float(split_line[1]) + except: + return ("Invalid scroll amount '" + split_line[1] + "'.", line) + + if len(split_line) > 2: + try: + temp = float(split_line[2]) + except: + return ("Invalid scroll amount '" + split_line[2] + "'.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + if len(split_line) > 2: + print("[cmds_mous] " + coords + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") + ms.scroll(float(split_line[2]), float(split_line[1])) + else: + print("[cmds_mous] " + coords + " Scroll " + split_line[1]) + ms.scroll(0, float(split_line[1])) + + return idx+1 + + +scripts.add_command(Mouse_Scroll()) + + +# ################################################## +# ### CLASS Mouse_Line ### +# ################################################## + +# class that defines the M_LINE command (draw a line?) +class Mouse_Line(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_LINE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 5: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 7: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + try: + temp = int(split_line[1]) + except: + return ("'M_LINE' X1 value '" + split_line[1] + "' not valid.", line) + + try: + temp = int(split_line[2]) + except: + return ("'M_LINE' Y1 value '" + split_line[2] + "' not valid.", line) + + try: + temp = int(split_line[3]) + except: + return ("'M_LINE' X2 value '" + split_line[3] + "' not valid.", line) + + try: + temp = int(split_line[4]) + except: + return ("'M_LINE' Y2 value '" + split_line[4] + "' not valid.", line) + + if len(split_line) >= 6: + try: + temp = float(split_line[5]) + except: + return ("'M_LINE' wait value '" + split_line[5] + "' not valid.", line) + + if len(split_line) >= 7: + try: + temp = int(split_line[6]) + if temp == 0: + return ("'M_LINE' skip value cannot be zero.", line) + except: + return ("'M_LINE' skip value '" + split_line[6] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + x1 = int(split_line[1]) + y1 = int(split_line[2]) + x2 = int(split_line[3]) + y2 = int(split_line[4]) + + delay = None + if len(split_line) > 5: + delay = float(split_line[5]) / 1000.0 + + skip = 1 + if len(split_line) > 6: + skip = int(split_line[6]) + + if (delay == None) or (delay <= 0): + print("[cmds_mous] " + coords[0] + " Mouse line from (" + \ + split_line[1] + ", " + split_line[2] + ") to (" + \ + split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step") + else: + print("[cmds_mous] " + coords + " Mouse line from (" + \ + split_line[1] + ", " + split_line[2] + ") to (" + \ + split_line[3] + ", " + split_line[4] + ") by " + \ + str(skip) + " pixels per step and wait " + split_line[5] + " milliseconds between each step") + + points = ms.line_coords(x1, y1, x2, y2) + + for x_M, y_M in points[::skip]: + if check_kill(x, y, is_async): + return -1 + + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, x, y, is_async): + return -1 + + return idx+1 + + +scripts.add_command(Mouse_Line()) + + +# ################################################## +# ### CLASS Mouse_Line_Move ### +# ################################################## + +# class that defines the M_LINE_MOVE command +class Mouse_Line_Move(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_LINE_MOVE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 3: + return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) + + try: + temp = int(split_line[1]) + except: + return ("'" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + + try: + temp = int(split_line[2]) + except: + return ("'" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + + if len(split_line) >= 4: + try: + temp = float(split_line[3]) + except: + return ("'" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + + if len(split_line) >= 5: + try: + temp = int(split_line[4]) + if temp == 0: + return ("'" + split_line[0] + "' skip value cannot be zero.", line) + except: + return ("'" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + x1 = int(split_line[1]) + y1 = int(split_line[2]) + + delay = None + if len(split_line) > 3: + delay = float(split_line[3]) / 1000.0 + + skip = 1 + if len(split_line) > 4: + skip = int(split_line[4]) + + if (delay == None) or (delay <= 0): + print("[cmds_mous] " + coords + " Mouse line move relative (" + \ + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ + " pixels per step") + else: + print("[cmds_mous] " + coords + " Mouse line move relative (" + \ + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ + " pixels per step and wait " + split_line[3] + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + x_N, y_N = x_C + x1, y_C + y1 + points = ms.line_coords(x_C, y_C, x_N, y_N) + + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): + return -1 + + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + +scripts.add_command(Mouse_Line_Move()) + + +# ################################################## +# ### CLASS Mouse_Line_Set ### +# ################################################## + +# class that defines the M_LINE_SET command +class Mouse_Line_Set(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_LINE_SET") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) < 3: + return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) + + try: + temp = int(split_line[1]) + except: + return ("'" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + + try: + temp = int(split_line[2]) + except: + return ("'" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + + if len(split_line) >= 4: + try: + temp = float(split_line[3]) + except: + return ("'" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + + if len(split_line) >= 5: + try: + temp = int(split_line[4]) + if temp == 0: + return ("'" + split_line[0] + "' skip value cannot be zero.", line) + except: + return ("'" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + x1 = int(split_line[1]) + y1 = int(split_line[2]) + + delay = None + if len(split_line) > 3: + delay = float(split_line[3]) / 1000.0 + + skip = 1 + if len(split_line) > 4: + skip = int(split_line[4]) + + if (delay == None) or (delay <= 0): + print("[cmds_mous] " + coords[0] + " Mouse line set (" + \ + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ + " pixels per step") + else: + print("[cmds_mous] " + coords[0] + " Mouse line set (" + \ + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ + " pixels per step and wait " + split_line[3] + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + points = ms.line_coords(x_C, y_C, x1, y1) + + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): + return -1 + ms.set_pos(x_M, y_M) + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + +scripts.add_command(Mouse_Line_Set()) + + +# ################################################## +# ### CLASS Mouse_Recall_Line ### +# ################################################## + +# class that defines the M_RECALL_LINE command +class Mouse_Recall_Line(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_RECALL_LINE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 1: + try: + temp = float(split_line[1]) + except: + return ("'" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) + + if len(split_line) > 2: + try: + temp = int(split_line[2]) + if temp == 0: + return ("'" + split_line[0] + "' skip value cannot be zero.", line) + except: + return ("'" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + x1, y1 = symbols['m_pos'] + + delay = None + if len(split_line) > 1: + delay = float(split_line[1]) / 1000.0 + + skip = 1 + if len(split_line) > 2: + skip = int(split_line[2]) + + if (delay == None) or (delay <= 0): + print("[cmds_mous] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + " in a line by " + str(skip) + " pixels per step") + else: + print("[cmds_mous] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + " in a line by " + str(skip) + " pixels per step and wait " + \ + split_line[1] + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + points = ms.line_coords(x_C, y_C, x1, y1) + + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): + return -1 + + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + +scripts.add_command(Mouse_Recall_Line()) + + +# ################################################## +# ### CLASS Mouse_Store ### +# ################################################## + +# class that defines the M_STORE command +class Mouse_Store(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_STORE") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 1: + return ("'" + split_line[0] + "' takes no arguments.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_mous] " + coords[0] + " Store mouse position") + + symbols["m_pos"] = ms.get_pos() + + return idx+1 + + +scripts.add_command(Mouse_Store()) + + +# ################################################## +# ### CLASS Mouse_Recall ### +# ################################################## + +# class that defines the M_RECALL command +class Mouse_Recall(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_RECALL") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + if len(split_line) > 1: + return ("'" + split_line[0] + "' takes no arguments.", line) + + return True + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + if symbols['m_pos'] == tuple(): + print("[cmds_mous] " + coords[0] + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + else: + print("[cmds_mous] " + coords[0] + " Recall mouse position " + str(symbols['m_pos'])) + ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) + + return idx+1 + + +scripts.add_command(Mouse_Recall()) + + diff --git a/commands_pause.py b/commands_pause.py new file mode 100644 index 0000000..58e57d3 --- /dev/null +++ b/commands_pause.py @@ -0,0 +1,63 @@ +import command_base, scripts + + +# ################################################## +# ### CLASS Pause_Delay ### +# ################################################## + +# class that defines the Delay command (a target of GOTO's etc) +class Pause_Delay(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DELAY") + + def Validate( + self, + idx: int, + line, + lines, + split_line, + symbols, + pass_no + ): + + if pass_no == 1: + # check number of split_line + if len(split_line) < 2: + return ("Too few arguments for command '" + split_line[0] + "'.", line) + + if len(split_line) > 2: + return ("Too many arguments for command '" + split_line[0] + "'.", line) + + try: + temp = float(split_line[1]) + except: + return ("Delay time '" + split_line[1] + "' not valid.", line) + + return True + + + def Run( + self, + idx: int, + split_line, + symbols, + coords, + is_async + ): + + print("[cmds_paus] " + coords[0] + " Delay for " + split_line[1] + " seconds") + + delay = float(split_line[1]) + + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + +scripts.add_command(Pause_Delay()) + + diff --git a/scripts.py b/scripts.py index a402428..d7a44cc 100644 --- a/scripts.py +++ b/scripts.py @@ -1,832 +1,397 @@ -import threading, webbrowser, os, subprocess -from time import sleep -from functools import partial -import lp_events, lp_colors, kb, sound, ms - -COLOR_PRIMED = 5 #red -COLOR_FUNC_KEYS_PRIMED = 9 #amber -EXIT_UPDATE_DELAY = 0.1 -DELAY_EXIT_CHECK = 0.025 - -import files - -VALID_COMMANDS = ["@ASYNC", "@SIMPLE", "@LOAD_LAYOUT", "STRING", "DELAY", "TAP", "PRESS", "RELEASE", "WEB", "WEB_NEW", "CODE", "SOUND", "SOUND_STOP", "WAIT_UNPRESSED", "M_MOVE", "M_SET", "M_SCROLL", "M_LINE", "M_LINE_MOVE", "M_LINE_SET", "LABEL", "IF_PRESSED_GOTO_LABEL", "IF_UNPRESSED_GOTO_LABEL", "GOTO_LABEL", "REPEAT_LABEL", "IF_PRESSED_REPEAT_LABEL", "IF_UNPRESSED_REPEAT_LABEL", "M_STORE", "M_RECALL", "M_RECALL_LINE", "OPEN", "RELEASE_ALL", "RESET_REPEATS"] -ASYNC_HEADERS = ["@ASYNC", "@SIMPLE"] - -threads = [[None for y in range(9)] for x in range(9)] -running = False -to_run = [] -text = [["" for y in range(9)] for x in range(9)] - -def check_kill(x, y, is_async, killfunc=None): - coords = "(" + str(x) + ", " + str(y) + ")" - - if threads[x][y].kill.is_set(): - print("[scripts] " + coords + " Recieved exit flag, script exiting...") - threads[x][y].kill.clear() - if not is_async: - running = False - if killfunc: - killfunc() - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() - return True - else: - return False - -def safe_sleep(time, x, y, is_async, endfunc=None): - while time > DELAY_EXIT_CHECK: - sleep(DELAY_EXIT_CHECK) - time -= DELAY_EXIT_CHECK - if check_kill(x, y, is_async, endfunc): - return False - if time > 0: - sleep(time) - if endfunc: - endfunc() - return True - -def is_ignorable_line(line): - line = line.strip() - if line != "": - if line[0] == "-": - return True - else: - return False - else: - return True - -def schedule_script(script_in, x, y): - global threads - global to_run - global running - coords = "(" + str(x) + ", " + str(y) + ")" - - if threads[x][y] != None: - if threads[x][y].is_alive(): - print("[scripts] " + coords + " Script already running, killing script....") - threads[x][y].kill.set() - return - - if (x, y) in [l[1:] for l in to_run]: - print("[scripts] " + coords + " Script already scheduled, unscheduling...") - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - - if script_in.split("\n")[0].split(" ")[0] in ASYNC_HEADERS: - print("[scripts] " + coords + " Starting asynchronous script in background...") - threads[x][y] = threading.Thread(target=run_script, args=(script_in,x,y)) - threads[x][y].kill = threading.Event() - threads[x][y].start() - elif not running: - print("[scripts] " + coords + " No script running, starting script in background...") - threads[x][y] = threading.Thread(target=run_script_and_run_next, args=(script_in,x,y)) - threads[x][y].kill = threading.Event() - threads[x][y].start() - else: - print("[scripts] " + coords + " A script is already running, scheduling...") - to_run.append((script_in, x, y)) - lp_colors.updateXY(x, y) - -def run_next(): - global to_run - if len(to_run) > 0: - tup = to_run.pop(0) - new_script = tup[0] - x = tup[1] - y = tup[2] - - schedule_script(new_script, x, y) - -def run_script_and_run_next(script_in, x_in, y_in): - global running - global to_run - coords = "(" + str(x_in) + ", " + str(y_in) + ")" - - run_script(script_in, x_in, y_in) - run_next() - -def run_script(script_str, x, y): - global running - global exit - - lp_colors.updateXY(x, y) - coords = "(" + str(x) + ", " + str(y) + ")" - - print("[scripts] " + coords + " Now running script...") - - script_lines = script_str.split("\n") - - script_lines = [i.strip() for i in script_lines] - - #remove comments - if len(script_lines) > 0: - while(is_ignorable_line(script_lines[0])): - line = script_lines.pop(0) - if line != "": - print("[scripts] " + coords + " Comment: " + line[1:]) - if len(script_lines) <= 0: - break - - if len(script_lines) > 0: - is_async = False - if script_lines[0].split(" ")[0] in ASYNC_HEADERS: - is_async = True - else: - running = True - - if script_lines[0].split(" ")[0] == "@ASYNC": - temp = script_lines.pop(0) - - #parse labels - labels = dict() - for idx,line in enumerate(script_lines): - split_line = line.split(" ") - if split_line[0] == "LABEL": - labels[split_line[1]] = idx - - #prepare repeat counter {idx:repeats_left} - repeats = dict() - repeats_original = dict() - - m_pos = () - - def main_logic(idx): - nonlocal m_pos - - if check_kill(x, y, is_async): - return idx + 1 - - line = script_lines[idx] - if line == "": - return idx + 1 - if line[0] == "-": - print("[scripts] " + coords + " Comment: " + line[1:]) - else: - split_line = line.split(" ") - if split_line[0] == "STRING": - type_string = " ".join(split_line[1:]) - print("[scripts] " + coords + " Type out string " + type_string) - kb.write(type_string) - elif split_line[0] == "DELAY": - print("[scripts] " + coords + " Delay for " + split_line[1] + " seconds") - delay = float(split_line[1]) - if not safe_sleep(delay, x, y, is_async): - return -1 - elif split_line[0] == "TAP": - key = kb.sp(split_line[1]) - releasefunc = lambda: kb.release(key) - if len(split_line) <= 2: - print("[scripts] " + coords + " Tap key " + split_line[1]) - kb.tap(key) - elif len(split_line) <= 3: - print("[scripts] " + coords + " Tap key " + split_line[1] + " " + split_line[2] + " times") - taps = int(split_line[2]) - for tap in range(taps): - if check_kill(x, y, is_async, releasefunc): - return idx + 1 - kb.tap(key) - else: - print("[scripts] " + coords + " Tap key " + split_line[1] + " " + split_line[2] + " times for " + str(split_line[3]) + " seconds each") - taps = int(split_line[2]) - delay = float(split_line[3]) - for tap in range(taps): - if check_kill(x, y, is_async, releasefunc): - return -1 - kb.press(key) - if not safe_sleep(delay, x, y, is_async, releasefunc): - return -1 - elif split_line[0] == "PRESS": - print("[scripts] " + coords + " Press key " + split_line[1]) - key = kb.sp(split_line[1]) - kb.press(key) - elif split_line[0] == "RELEASE": - print("[scripts] " + coords + " Release key " + split_line[1]) - key = kb.sp(split_line[1]) - kb.release(key) - elif split_line[0] == "WEB": - link = split_line[1] - if "http" not in link: - link = "http://" + link - print("[scripts] " + coords + " Open website " + link + " in default browser") - webbrowser.open(link) - elif split_line[0] == "WEB_NEW": - link = split_line[1] - if "http" not in link: - link = "http://" + link - print("[scripts] " + coords + " Open website " + link + " in default browser, try to make a new window") - webbrowser.open_new(link) - elif split_line[0] == "CODE": - args = " ".join(split_line[1:]) - print("[scripts] " + coords + " Running code: " + args) - try: - subprocess.run(args) - except Exception as e: - print("[scripts] " + coords + " Error with running code: " + str(e)) - elif split_line[0] == "SOUND": - if len(split_line) > 2: - print("[scripts] " + coords + " Play sound file " + split_line[1] + " at volume " + str(split_line[2])) - sound.play(split_line[1], float(split_line[2])) - else: - print("[scripts] " + coords + " Play sound file " + split_line[1]) - sound.play(split_line[1]) - elif split_line[0] == "SOUND_STOP": - if len(split_line) > 1: - delay = split_line[1] - print("[scripts] " + coords + - " Stopping sounds with " + delay + " milliseconds fadeout time") - sound.fadeout(int(delay)) - else: - print("[scripts] " + coords + " Stopping sounds") - sound.stop() - elif split_line[0] == "WAIT_UNPRESSED": - print("[scripts] " + coords + " Wait for script key to be unpressed") - while lp_events.pressed[x][y]: - sleep(DELAY_EXIT_CHECK) - if check_kill(x, y, is_async): - return idx + 1 - elif split_line[0] == "M_STORE": - print("[scripts] " + coords + " Store mouse position") - m_pos = ms.get_pos() - elif split_line[0] == "M_RECALL": - if m_pos == tuple(): - print("[scripts] " + coords + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") - else: - print("[scripts] " + coords + " Recall mouse position " + str(m_pos)) - ms.set_pos(m_pos[0], m_pos[1]) - elif split_line[0] == "M_RECALL_LINE": - x1, y1 = m_pos - - delay = None - if len(split_line) > 1: - delay = float(split_line[1]) / 1000.0 - - skip = 1 - if len(split_line) > 2: - skip = int(split_line[2]) - - if (delay == None) or (delay <= 0): - print("[scripts] " + coords + " Recall mouse position " + str(m_pos) + " in a line by " + str(skip) + " pixels per step") - else: - print("[scripts] " + coords + " Recall mouse position " + str(m_pos) + " in a line by " + str(skip) + " pixels per step and wait " + split_line[1] + " milliseconds between each step") - - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, x1, y1) - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): - return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): - return -1 - elif split_line[0] == "M_MOVE": - if len(split_line) >= 3: - print("[scripts] " + coords + " Relative mouse movement (" + split_line[1] + ", " + str(split_line[2]) + ")") - ms.move_to_pos(float(split_line[1]), float(split_line[2])) - else: - print("[scripts] " + coords + " Both X and Y are required for mouse movement, skipping...") - elif split_line[0] == "M_SET": - if len(split_line) >= 3: - print("[scripts] " + coords + " Set mouse position to (" + split_line[1] + ", " + str(split_line[2]) + ")") - ms.set_pos(float(split_line[1]), float(split_line[2])) - else: - print("[scripts] " + coords + " Both X and Y are required for mouse positioning, skipping...") - elif split_line[0] == "M_SCROLL": - if len(split_line) > 2: - print("[scripts] " + coords + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") - ms.scroll(float(split_line[2]), float(split_line[1])) - else: - print("[scripts] " + coords + " Scroll " + split_line[1]) - ms.scroll(0, float(split_line[1])) - elif split_line[0] == "M_LINE": - x1 = int(split_line[1]) - y1 = int(split_line[2]) - x2 = int(split_line[3]) - y2 = int(split_line[4]) - - delay = None - if len(split_line) > 5: - delay = float(split_line[5]) / 1000.0 - - skip = 1 - if len(split_line) > 6: - skip = int(split_line[6]) - - if (delay == None) or (delay <= 0): - print("[scripts] " + coords + " Mouse line from (" + split_line[1] + ", " + split_line[2] + ") to (" + split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step") - else: - print("[scripts] " + coords + " Mouse line from (" + split_line[1] + ", " + split_line[2] + ") to (" + split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step and wait " + split_line[5] + " milliseconds between each step") - - points = ms.line_coords(x1, y1, x2, y2) - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): - return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): - return -1 - elif split_line[0] == "M_LINE_MOVE": - x1 = int(split_line[1]) - y1 = int(split_line[2]) - - delay = None - if len(split_line) > 3: - delay = float(split_line[3]) / 1000.0 - - skip = 1 - if len(split_line) > 4: - skip = int(split_line[4]) - - if (delay == None) or (delay <= 0): - print("[scripts] " + coords + " Mouse line move relative (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step") - else: - print("[scripts] " + coords + " Mouse line move relative (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step and wait " + split_line[3] + " milliseconds between each step") - - x_C, y_C = ms.get_pos() - x_N, y_N = x_C + x1, y_C + y1 - points = ms.line_coords(x_C, y_C, x_N, y_N) - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): - return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): - return -1 - elif split_line[0] == "M_LINE_SET": - x1 = int(split_line[1]) - y1 = int(split_line[2]) - - delay = None - if len(split_line) > 3: - delay = float(split_line[3]) / 1000.0 - - skip = 1 - if len(split_line) > 4: - skip = int(split_line[4]) - - if (delay == None) or (delay <= 0): - print("[scripts] " + coords + " Mouse line set (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step") - else: - print("[scripts] " + coords + " Mouse line set (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step and wait " + split_line[3] + " milliseconds between each step") - - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, x1, y1) - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): - return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): - return -1 - elif split_line[0] == "LABEL": - print("[scripts] " + coords + " Label: " + split_line[1]) - return idx + 1 - elif split_line[0] == "IF_PRESSED_GOTO_LABEL": - print("[scripts] " + coords + " If key is pressed goto LABEL " + split_line[1]) - if lp_events.pressed[x][y]: - return labels[split_line[1]] - elif split_line[0] == "IF_UNPRESSED_GOTO_LABEL": - print("[scripts] " + coords + " If key is not pressed goto LABEL " + split_line[1]) - if not lp_events.pressed[x][y]: - return labels[split_line[1]] - elif split_line[0] == "GOTO_LABEL": - print("[scripts] " + coords + " Goto LABEL " + split_line[1]) - return labels[split_line[1]] - elif split_line[0] == "REPEAT_LABEL": - print("[scripts] " + coords + " Repeat LABEL " + split_line[1] + " " + split_line[2] + " times max") - if idx in repeats: - if repeats[idx] > 0: - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - else: - print("[scripts] " + coords + " No repeats left, not repeating.") - else: - repeats[idx] = int(split_line[2]) - repeats_original[idx] = int(split_line[2]) - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - elif split_line[0] == "IF_PRESSED_REPEAT_LABEL": - print("[scripts] " + coords + " If key is pressed repeat LABEL " + split_line[1] + " " + split_line[2] + " times max") - if lp_events.pressed[x][y]: - if idx in repeats: - if repeats[idx] > 0: - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - else: - print("[scripts] " + coords + " No repeats left, not repeating.") - else: - repeats[idx] = int(split_line[2]) - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - elif split_line[0] == "IF_UNPRESSED_REPEAT_LABEL": - print("[scripts] " + coords + " If key is not pressed repeat LABEL " + split_line[1] + " " + split_line[2] + " times max") - if not lp_events.pressed[x][y]: - if idx in repeats: - if repeats[idx] > 0: - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - else: - print("[scripts] " + coords + " No repeats left, not repeating.") - else: - repeats[idx] = int(split_line[2]) - print("[scripts] " + coords + " " + str(repeats[idx]) + " repeats left.") - repeats[idx] -= 1 - return labels[split_line[1]] - elif split_line[0] == "@SIMPLE": - print("[scripts] " + coords + " Simple keybind: " + split_line[1]) - #PRESS - key = kb.sp(split_line[1]) - releasefunc = lambda: kb.release(key) - kb.press(key) - #WAIT_UNPRESSED - while lp_events.pressed[x][y]: - sleep(DELAY_EXIT_CHECK) - if check_kill(x, y, is_async, releasefunc): - return idx + 1 - #RELEASE - kb.release(key) - elif split_line[0] == "@LOAD_LAYOUT": - layout_name = " ".join(split_line[1:]) - print("[scripts] " + coords + " Load layout " + layout_name) - layout_path = os.path.join(files.LAYOUT_PATH, layout_name) - if not os.path.isfile(layout_path): - print("[scripts] " + coords + " ERROR: Layout file does not exist.") - return -1 - try: - layout = files.load_layout(layout_path, popups=False, save_converted=False) - except files.json.decoder.JSONDecodeError: - print("[scripts] " + coords + " ERROR: Layout is malformated.") - return -1 - if files.layout_changed_since_load: - files.save_lp_to_layout(files.curr_layout) - files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout) - elif split_line[0] == "OPEN": - path_name = " ".join(split_line[1:]) - print("[scripts] " + coords + " Open file or folder " + path_name) - files.open_file_folder(path_name) - elif split_line[0] == "RELEASE_ALL": - print("[scripts] " + coords + " Release all keys") - kb.release_all() - elif split_line[0] == "RESET_REPEATS": - print("[scripts] " + coords + " Reset all repeats") - for i in repeats: - repeats[i] = repeats_original[i] - else: - print("[scripts] " + coords + " Invalid command: " + split_line[0] + ", skipping...") - return idx + 1 - run = True - idx = 0 - while run: - idx = main_logic(idx) - if (idx < 0) or (idx >= len(script_lines)): - run = False - - if not is_async: - running = False - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() - - print("[scripts] (" + str(x) + ", " + str(y) + ") Script done running.") - - -def bind(x, y, script_down, color): - global to_run - if (x, y) in [l[1:] for l in to_run]: - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - - schedule_script_bindable = lambda a, b: schedule_script(script_down, x, y) - - lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) - text[x][y] = script_down - files.layout_changed_since_load = True - -def unbind(x, y): - global to_run - lp_events.unbind(x, y) - text[x][y] = "" - if (x, y) in [l[1:] for l in to_run]: - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - if threads[x][y] != None: - threads[x][y].kill.set() - files.layout_changed_since_load = True - -def swap(x1, y1, x2, y2): - global text - color_1 = lp_colors.curr_colors[x1][y1] - color_2 = lp_colors.curr_colors[x2][y2] - - script_1 = text[x1][y1] - script_2 = text[x2][y2] - - unbind(x1, y1) - if script_2 != "": - bind(x1, y1, script_2, color_2) - lp_colors.updateXY(x1, y1) - - unbind(x2, y2) - if script_1 != "": - bind(x2, y2, script_1, color_1) - lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True - -def copy(x1, y1, x2, y2): - global text - color_1 = lp_colors.curr_colors[x1][y1] - - script_1 = text[x1][y1] - - unbind(x2, y2) - if script_1 != "": - bind(x2, y2, script_1, color_1) - lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True - -def move(x1, y1, x2, y2): - global text - color_1 = lp_colors.curr_colors[x1][y1] - - script_1 = text[x1][y1] - - unbind(x1, y1) - unbind(x2, y2) - if script_1 != "": - bind(x2, y2, script_1, color_1) - lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True - -def is_bound(x, y): - if text[x][y] == "": - return False - else: - return True - -def unbind_all(): - global threads - global text - global to_run - lp_events.unbind_all() - text = [["" for y in range(9)] for x in range(9)] - to_run = [] - for x in range(9): - for y in range(9): - if threads[x][y] is not None: - if threads[x][y].isAlive(): - threads[x][y].kill.set() - files.curr_layout = None - files.layout_changed_since_load = False - -def validate_script(script_str): - if script_str == "": - return True - script_lines = script_str.split('\n') - - script_lines = [i.strip() for i in script_lines] - - if len(script_lines) > 0: - while(is_ignorable_line(script_lines[0])): - line = script_lines.pop(0) - if len(script_lines) <= 0: - return True - - first_line = script_lines[0] - first_line_split = first_line.split(" ") - - if first_line_split[0] == "@ASYNC": - if len(first_line_split) > 1: - return ("@ASYNC takes no arguments.", script_lines[0]) - temp = script_lines.pop(0) - - if first_line_split[0] == "@SIMPLE": - if len(first_line_split) < 2: - return ("@SIMPLE requires a key to bind.", first_line) - if len(first_line_split) > 2: - return ("@SIMPLE only take one argument", first_line) - if kb.sp(first_line_split[1]) == None: - return ("No key named '" + first_line_split[1] + "'.", first_line) - for line in script_lines[1:]: - if line != "" and line[0] != "-": - return ("When @SIMPLE is used, scripts can only contain comments.", line) - - if first_line_split[0] == "@LOAD_LAYOUT": - for line in script_lines[1:]: - if line != "" and line[0] != "-": - return ("When @LOAD_LAYOUT is used, scripts can only contain comments.", line) - if len(first_line_split) < 2: - return ("No layout filename provided.", first_line) - layout_path = os.path.join(files.LAYOUT_PATH, " ".join(first_line_split[1:])) - if not os.path.isfile(layout_path): - return ("'" + layout_path + "' does not exist!", first_line) - - try: - layout = files.load_layout(layout_path, popups=False, save_converted=False, printing=False) - except: - return ("Layout '" + layout_path + "' is malformatted.", first_line) - - #parse labels - labels = [] - for line in script_lines: - split_line = line.split(" ") - if split_line[0] == "LABEL": - if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) - if split_line[1] in labels: - return ("Label '" + split_line[1] + "' defined multiple times.", line) - else: - labels.append(split_line[1]) - - for idx, line in enumerate(script_lines): - if line != "": - if line[0] != "-": - split_line = line.split(' ') - if split_line[0][0] == "@": - if idx != 0: - return ("Headers must only be used on the first line of a script.", line) - if split_line[0] not in VALID_COMMANDS: - return ("Command '" + split_line[0] + "' not valid.", line) - if split_line[0] in ["STRING", "DELAY", "TAP", "PRESS", "RELEASE", "WEB", "WEB_NEW", "CODE", "SOUND", "M_MOVE", "M_SET", "M_SCROLL", "OPEN"]: - if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) - if split_line[0] in ["WAIT_UNPRESSED", "RELEASE_ALL", "RESET_REPEATS"]: - if len(split_line) > 1: - return ("Too many arguments for command '" + split_line[0] + "'.", line) - if split_line[0] in ["DELAY", "WEB", "WEB_NEW", "PRESS", "RELEASE", "SOUND_STOP"]: - if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) - if split_line[0] in ["SOUND", "M_MOVE", "M_SCROLL", "M_SET"]: - if len(split_line) > 3: - return ("Too many arguments for command '" + split_line[0] + "'.", line) - if split_line[0] in ["TAP"]: - if len(split_line) > 4: - return ("Too many arguments for command '" + split_line[0] + "'.", line) - if len(split_line) > 3: - try: - temp = float(split_line[3]) - except: - return (split_line[0] + "Tap wait time '" + split_line[3] + "' not valid.", line) - if len(split_line) > 2: - try: - temp = int(split_line[2]) - except: - return (split_line[0] + " repetitions '" + split_line[2] + "' not valid.", line) - if split_line[0] in ["M_LINE"]: - if len(split_line) > 7: - return ("Too many arguments for command '" + split_line[0] + "'.", line) - if split_line[0] in ["TAP", "PRESS", "RELEASE"]: - if kb.sp(split_line[1]) == None: - return ("No key named '" + split_line[1] + "'.", line) - if split_line[0] == "DELAY": - try: - temp = float(split_line[1]) - except: - return ("Delay time '" + split_line[1] + "' not valid.", line) - if split_line[0] == "WAIT_UNPRESSED": - if len(split_line) > 1: - return ("'WAIT_UNPRESSED' takes no arguments.", line) - if split_line[0] == "SOUND": - final_name = sound.full_name(split_line[1]) - if not os.path.isfile(final_name): - return ("Sound file '" + final_name + "' not found.", line) - if not sound.is_valid(split_line[1]): - return ("Sound file '" + final_name + "' not valid.", line) - if len(split_line) > 2: - try: - vol = float(float(split_line[2]) / 100.0) - if (vol < 0.0) or (vol > 1.0): - return ("'SOUND' volume must be between 0 and 100.", line) - except: - return ("'SOUND' volume " + split_line[2] + " not valid.", line) - if split_line[0] in ["M_STORE", "M_RECALL"]: - if len(split_line) > 1: - return ("'" + split_line[0] + "' takes no arguments.", line) - if split_line[0] == "M_RECALL_LINE": - if len(split_line) > 1: - try: - temp = float(split_line[1]) - except: - return ("'" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) - if len(split_line) > 2: - try: - temp = int(split_line[2]) - if temp == 0: - return ("'" + split_line[0] + "' skip value cannot be zero.", line) - except: - return ("'" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) - if split_line[0] == "M_MOVE": - if len(split_line) < 3: - return ("'M_MOVE' requires both an X and a Y movement value.", line) - try: - temp = int(split_line[1]) - except: - return ("'M_MOVE' X value '" + split_line[1] + "' not valid.", line) - try: - temp = int(split_line[2]) - except: - return ("'M_MOVE' Y value '" + split_line[2] + "' not valid.", line) - if split_line[0] == "M_SET": - if len(split_line) < 3: - return ("'M_SET' requires both an X and a Y value.", line) - try: - temp = int(split_line[1]) - except: - return ("'M_SET' X value '" + split_line[1] + "' not valid.", line) - try: - temp = int(split_line[2]) - except: - return ("'M_SET' Y value '" + split_line[2] + "' not valid.", line) - if split_line[0] == "M_SCROLL": - try: - temp = float(split_line[1]) - except: - return ("Invalid scroll amount '" + split_line[1] + "'.", line) - - if len(split_line) > 2: - try: - temp = float(split_line[2]) - except: - return ("Invalid scroll amount '" + split_line[2] + "'.", line) - if split_line[0] == "M_LINE": - if len(split_line) < 5: - return ("'M_LINE' requires at least X1, Y1, X2, and Y2 arguments.", line) - try: - temp = int(split_line[1]) - except: - return ("'M_LINE' X1 value '" + split_line[1] + "' not valid.", line) - try: - temp = int(split_line[2]) - except: - return ("'M_LINE' Y1 value '" + split_line[2] + "' not valid.", line) - try: - temp = int(split_line[3]) - except: - return ("'M_LINE' X2 value '" + split_line[3] + "' not valid.", line) - try: - temp = int(split_line[4]) - except: - return ("'M_LINE' Y2 value '" + split_line[4] + "' not valid.", line) - if len(split_line) >= 6: - try: - temp = float(split_line[5]) - except: - return ("'M_LINE' wait value '" + split_line[5] + "' not valid.", line) - if len(split_line) >= 7: - try: - temp = int(split_line[6]) - if temp == 0: - return ("'M_LINE' skip value cannot be zero.", line) - except: - return ("'M_LINE' skip value '" + split_line[6] + "' not valid.", line) - if split_line[0] in ["M_LINE_MOVE", "M_LINE_SET"]: - if len(split_line) < 3: - return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) - try: - temp = int(split_line[1]) - except: - return ("'" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) - try: - temp = int(split_line[2]) - except: - return ("'" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) - if len(split_line) >= 4: - try: - temp = float(split_line[3]) - except: - return ("'" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) - if len(split_line) >= 5: - try: - temp = int(split_line[4]) - if temp == 0: - return ("'" + split_line[0] + "' skip value cannot be zero.", line) - except: - return ("'" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) - if split_line[0] in ["GOTO_LABEL", "IF_PRESSED_GOTO_LABEL", "IF_UNPRESSED_GOTO_LABEL"]: - if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) - if split_line[1] not in labels: - return ("Label '" + split_line[1] + "' not defined in this script.", line) - if split_line[0] in ["REPEAT_LABEL", "IF_PRESSED_REPEAT_LABEL", "IF_UNPRESSED_REPEAT_LABEL"]: - if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - if split_line[1] not in labels: - return ("Label '" + split_line[1] + "' not defined in this script.", line) - try: - temp = int(split_line[2]) - if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) - except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - if split_line[0] == "OPEN": - path_name = " ".join(split_line[1:]) - if (not os.path.isfile(path_name)) and (not os.path.isdir(path_name)): - return (split_line[0] + " folder or file location '" + path_name + "' does not exist.", line) - return True +import threading, webbrowser, os, subprocess +from time import sleep +from functools import partial +import lp_events, lp_colors, kb, sound, ms, files, command_base + + +# VALID_COMMAND is a dictionary of all commands available. +# it is initially empty because now we load it dynamically + +VALID_COMMANDS = dict() + + +# HEADERS is likewise empty until added (all headers, not just async ones) + +HEADERS = dict() + + +# Add a new command. This removes any existing command of the same name from the VALID_COMMANDS +# and returns it as the result + +def add_command( + a_command: command_base.Command_Basic # the command to add + ): + + if a_command.name in HEADERS: # if this was previously a header, now it isn't + HEADERS.pop(a_command.name) + + if a_command.name in VALID_COMMANDS: # if it already exists + p = VALID_COMMANDS[a_command.name] # get it + else: # otherwise + p = None # the return value will be None + + VALID_COMMANDS[a_command.name] = a_command # add the new command + + if a_command is command_base.Command_Header: # is this a header? + HEADERS[a_command.name] = a_command.is_async # add it + + return p # return any replaced command + + +# Create a new symbol table. This contains information required for the script to run +# it includes the locations of labels, loop counters, etc. If we implement variables +# this is where we would place them + +def new_symbol_table(): + # returns a new (blank) symbol table + # symbol table is dictionary of objects + symbols = { + "repeats": dict(), + "original": dict(), + "labels": dict(), + "m_pos": dict() } + + return symbols + + +# Do what is required to parse the script. Parsing does not output any information unless it is an error + +def parse_script(script_lines, symbols): + err = True + errors = 0 # no errors found + + for pass_no in (1,2): # pass 1, collect info & syntax check, + # pass 2 symbol check & assocoated processing + for idx,line in enumerate(script_lines): # gen line number and text + if is_ignorable_line(line): + continue # don't process ignorable lines + split_line = line.split(" ") # split line on spaces + if split_line[0] in VALID_COMMANDS: # if first element is a command + res = VALID_COMMANDS[split_line[0]].Parse(idx, line, script_lines, split_line, symbols, pass_no); + if res != True: + if err == True: + err = res # note the error + errors += 1 # and 1 more error + else: + msg = "Invalid command '" + split_line[0] + "' on line " + str(idx) + "." + if err == True: + err = (msg, line) # note the error + print (msg) + errors += 1 # and 1 more error + + print('Pass ' + str(pass_no) + ' complete. ' + str(errors) + ' errors detected.') + if err != True: + break # errors prevent next pass + + return err # success or failure + + +COLOR_PRIMED = 5 #red +COLOR_FUNC_KEYS_PRIMED = 9 #amber +EXIT_UPDATE_DELAY = 0.1 +DELAY_EXIT_CHECK = 0.025 + + +threads = [[None for y in range(9)] for x in range(9)] +running = False +to_run = [] +text = [["" for y in range(9)] for x in range(9)] + + +def check_kill(x, y, is_async, killfunc=None): + global threads + + coords = "(" + str(x) + ", " + str(y) + ")" + + if threads[x][y].kill.is_set(): + print("[scripts] " + coords + " Recieved exit flag, script exiting...") + threads[x][y].kill.clear() + if not is_async: + running = False + if killfunc: + killfunc() + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() + return True + else: + return False + + +# a sleep method that works with the multiple threads + +def safe_sleep(time, x, y, is_async, endfunc=None): + while time > DELAY_EXIT_CHECK: + sleep(DELAY_EXIT_CHECK) + time -= DELAY_EXIT_CHECK + if check_kill(x, y, is_async, endfunc): + return False + if time > 0: + sleep(time) + if endfunc: + endfunc() + return True + + +# some lines can be ignored. These include blank lines and comments. It's faster to identify them +# before trying to process them than treat them as an exception afterwards. + +def is_ignorable_line(line): + line = line.strip() # remove leading and trailing spaces + if line != "": + if line[0] == "-": + return True # non-blank lines starting with a hyphen are comments (and can be ignored) + else: + return False # other non-blank lines are significant + else: + return True # blank lines can be igmored + + +def schedule_script(script_in, x, y): + global threads + global to_run + global running + + coords = "(" + str(x) + ", " + str(y) + ")" + + if threads[x][y] != None: + if threads[x][y].is_alive(): + print("[scripts] " + coords + " Script already running, killing script....") + threads[x][y].kill.set() + return + + if (x, y) in [l[1:] for l in to_run]: + print("[scripts] " + coords + " Script already scheduled, unscheduling...") + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] + for index in indexes[::-1]: + temp = to_run.pop(index) + return + + token = script_in.split("\n")[0].split(" ")[0] + if token in HEADERS and HEADERS[token].is_async: + print("[scripts] " + coords + " Starting asynchronous script in background...") + threads[x][y] = threading.Thread(target=run_script, args=(script_in,x,y)) + threads[x][y].kill = threading.Event() + threads[x][y].start() + elif not running: + print("[scripts] " + coords + " No script running, starting script in background...") + threads[x][y] = threading.Thread(target=run_script_and_run_next, args=(script_in,x,y)) + threads[x][y].kill = threading.Event() + threads[x][y].start() + else: + print("[scripts] " + coords + " A script is already running, scheduling...") + to_run.append((script_in, x, y)) + lp_colors.updateXY(x, y) + + +def run_next(): + global to_run + + if len(to_run) > 0: + tup = to_run.pop(0) + new_script = tup[0] + x = tup[1] + y = tup[2] + + schedule_script(new_script, x, y) + + +def run_script_and_run_next(script_in, x_in, y_in): + global running + global to_run + + coords = "(" + str(x_in) + ", " + str(y_in) + ")" + + run_script(script_in, x_in, y_in) + run_next() + + +# run a script + +def run_script(script_str, x, y): + global running + global exit + + lp_colors.updateXY(x, y) + coords = "(" + str(x) + ", " + str(y) + ")" + + print("[scripts] " + coords + " Now running script...") + + script_lines = script_str.split("\n") + script_lines = [i.strip() for i in script_lines] + + if len(script_lines) > 0: + is_async = False + token = script_lines[0].split(" ")[0] + if token in VALID_COMMANDS: + command = VALID_COMMANDS[token] + is_async = token in HEADERS and HEADERS[token].is_async + else: + running = True + + symbols = new_symbol_table() + + # parse labels (do all parsing required for commands) + parse_script(script_lines, symbols) + + + def main_logic(idx): + nonlocal symbols + + if check_kill(x, y, is_async): + return idx + 1 + + line = script_lines[idx] + if line == "": + return idx + 1 + + if line[0] == "-": + split_line = ["-", line[1:]] # comments are special -- not tokenised + else: + split_line = line.split(" ") + + if split_line[0] in VALID_COMMANDS: # if first element is a command + command = VALID_COMMANDS[split_line[0]] # get the command + return command.Run(idx, split_line, symbols, (coords, x, y), is_async) + + else: + print("[scripts] " + coords + " Invalid command: " + split_line[0] + ", skipping...") + + return idx + 1 + + run = True + idx = 0 + while run: + idx = main_logic(idx) + if (idx < 0) or (idx >= len(script_lines)): + run = False + + if not is_async: + running = False + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() + + print("[scripts] (" + str(x) + ", " + str(y) + ") Script done running.") + + +def bind(x, y, script_down, color): + global to_run + + if (x, y) in [l[1:] for l in to_run]: + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] + for index in indexes[::-1]: + temp = to_run.pop(index) + return + + schedule_script_bindable = lambda a, b: schedule_script(script_down, x, y) + + lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) + text[x][y] = script_down + files.layout_changed_since_load = True + + +def unbind(x, y): + global to_run + + lp_events.unbind(x, y) + text[x][y] = "" + if (x, y) in [l[1:] for l in to_run]: + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] + for index in indexes[::-1]: + temp = to_run.pop(index) + return + if threads[x][y] != None: + threads[x][y].kill.set() + files.layout_changed_since_load = True + + +def swap(x1, y1, x2, y2): + global text + + color_1 = lp_colors.curr_colors[x1][y1] + color_2 = lp_colors.curr_colors[x2][y2] + + script_1 = text[x1][y1] + script_2 = text[x2][y2] + + unbind(x1, y1) + if script_2 != "": + bind(x1, y1, script_2, color_2) + lp_colors.updateXY(x1, y1) + + unbind(x2, y2) + if script_1 != "": + bind(x2, y2, script_1, color_1) + lp_colors.updateXY(x2, y2) + files.layout_changed_since_load = True + + +def copy(x1, y1, x2, y2): + global text + + color_1 = lp_colors.curr_colors[x1][y1] + + script_1 = text[x1][y1] + + unbind(x2, y2) + if script_1 != "": + bind(x2, y2, script_1, color_1) + lp_colors.updateXY(x2, y2) + files.layout_changed_since_load = True + + +def move(x1, y1, x2, y2): + global text + + color_1 = lp_colors.curr_colors[x1][y1] + + script_1 = text[x1][y1] + + unbind(x1, y1) + unbind(x2, y2) + if script_1 != "": + bind(x2, y2, script_1, color_1) + lp_colors.updateXY(x2, y2) + files.layout_changed_since_load = True + + +# determine if a key is bound + +def is_bound(x, y): + if text[x][y] == "": + return False + else: + return True + + +# Unbind all keys. + +def unbind_all(): + global threads + global text + global to_run + + lp_events.unbind_all() + text = [["" for y in range(9)] for x in range(9)] + to_run = [] + for x in range(9): + for y in range(9): + if threads[x][y] is not None: + if threads[x][y].isAlive(): + threads[x][y].kill.set() + files.curr_layout = None + files.layout_changed_since_load = False + + +# validating a script consists of doing the checks that we do prior to running, but +# we won't run it afterwards. + +def validate_script(script_str): + if script_str == "": + return True + + script_lines = script_str.split('\n') + script_lines = [i.strip() for i in script_lines] + + symbols = new_symbol_table() + + return parse_script(script_lines, symbols) + + From f95184d5b0779ab5d3446eba45c87a97870943dd Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 27 Aug 2020 20:01:09 +0800 Subject: [PATCH 02/83] Updated version of refactored code with addition of documentation and a couple of new commands --- LPHK.py | 2 +- README.md | 9 +- commands_control.py | 613 ++++++++++++++++++++++++++++++------------- commands_external.py | 183 ++++++------- commands_header.py | 104 ++++---- commands_keys.py | 186 ++++++------- commands_mouse.py | 299 ++++++++++----------- commands_pause.py | 30 ++- 8 files changed, 848 insertions(+), 578 deletions(-) diff --git a/LPHK.py b/LPHK.py index 693e4b5..d319e48 100755 --- a/LPHK.py +++ b/LPHK.py @@ -87,6 +87,7 @@ def datetime_str(): import lp_events, scripts, kb, files, sound, window from utils import launchpad_connector +# just import the control modules to automatically integrate them import commands_header, commands_control, commands_keys, commands_mouse, commands_pause, commands_external lp = launchpad.Launchpad() @@ -133,7 +134,6 @@ def shutdown(): def main(): init() window.init(lp, launchpad, PATH, PROG_PATH, USER_PATH, VERSION, PLATFORM) - print("here") if EXIT_ON_WINDOW_CLOSE: shutdown() diff --git a/README.md b/README.md index 3be09b9..4c6bb5f 100644 --- a/README.md +++ b/README.md @@ -202,21 +202,28 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * If the button the script is bound to is pressed, goto label (argument 1). * `IF_PRESSED_REPEAT_LABEL` * If the button the script is bound to is pressed, goto label (argument 1) a maximum of (argument 2) times. +* `IF_PRESSED_REPEAT` + * Works the same as the IF_PRESSED_REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. * `IF_UNPRESSED_GOTO_LABEL` * If the button the script is bound to is not pressed, goto label (argument 1). * `IF_UNPRESSED_REPEAT_LABEL` * If the button the script is bound to is not pressed, goto label (argument 1) a maximum of (argument 2) times. +* `IF_UNPRESSED_REPEAT` + * Works the same as the IF_UNPRESSED_REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. * `LABEL` * Sets a label named (argument 1) for use with the `*GOTO_LABEL` commands. * `OPEN` * Opens the file or folder (argument 1). * `REPEAT_LABEL` * Goto label (argument 1) a maximum of (argument 2) times. +* `REPEAT` + * Works the same as the REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. * `RESET_REPEATS` * Reset the counter on all repeats. (no arguments) * `SOUND` * Play a sound named (argument 1) inside the `user_sounds/` folder. - * Supports `.wav`, `.flac`, and `.ogg` only. + * Supports `.wa * Works the same as the IF_UNPRESSED_REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. +v`, `.flac`, and `.ogg` only. * If (argument 2) supplied, set volume to (argument 2). * Range is 0 to 100 * `SOUND_STOP` diff --git a/commands_control.py b/commands_control.py index 15da953..8de7183 100644 --- a/commands_control.py +++ b/commands_control.py @@ -1,47 +1,48 @@ import command_base, lp_events, scripts +lib = "cmds_ctrl" # name of this library (for logging) # ################################################## # ### CLASS Control_Comment ### # ################################################## # class that defines the comment command (single quote at beginning of line) -# this is special because it has some different handling in the main sode +# this is special because it has some different handling in the main code # to allow it to work without a space following it class Control_Comment(command_base.Command_Basic): def __init__( self, ): - super().__init__("-") + super().__init__("-") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - return True # never return an error + return True # return True if there is no error (a comment can't ever be an error) def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " comment: " + split_line[1]) + print("[" + lib + "] " + coords[0] + " comment: " + split_line[1:]) # coords[0] is the text "(x, y)" - return idx+1 + return idx+1 # Return the number of the next line to execute, -1 to exit -scripts.add_command(Control_Comment()) +scripts.add_command(Control_Comment()) # register the command # ################################################## @@ -54,47 +55,47 @@ def __init__( self, ): - super().__init__("LABEL") + super().__init__("LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions # check number of split_line if len(split_line) != 2: - return ("Wrong number of parameters in " + self.Name, line) + return ("Wrong number of parameters in " + self.name, line) # check for duplicate label - if split_line[1] in symbols["labels"]: + if split_line[1] in symbols["labels"]: # Does the label already exist (that's bad)? return ("duplicate LABEL", line) - # add label to symbol table - symbols["labels"][split_line[1]] = idx + # add label to symbol table # Add the new label to the labels in the symbol table + symbols["labels"][split_line[1]] = idx # key is label, data is line number return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " Label: " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Label: " + split_line[1]) - return idx+1 + return idx+1 # Nothing to do when executing a label -scripts.add_command(Control_Label()) +scripts.add_command(Control_Label()) # register the command # ################################################## @@ -107,49 +108,51 @@ def __init__( self, ): - super().__init__("GOTO_LABEL") + super().__init__("GOTO_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions # check number of split_line if len(split_line) != 2: - return ("Wrong number of parameters in " + self.Name, line) + return ("Wrong number of parameters in " + self.ame, line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): + print("[" + lib + "] " + coords[0] + " Goto LABEL " + split_line[1]) + # check for label - if split_line[1] in symbols["labels"]: - return symbols["labels"][split_line[1]] - else: - print("missing LABEL '" + split_line[1] + "'") + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 + else: + return symbols["labels"][split_line[1]] # normally we return the line number the label is on - return idx+1 + return idx+1 # We'll never get here -scripts.add_command(Control_Goto_Label()) +scripts.add_command(Control_Goto_Label()) # register the command # ################################################## @@ -162,45 +165,53 @@ def __init__( self, ): - super().__init__("IF_PRESSED_GOTO_LABEL") + super().__init__("IF_PRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 2: return ("'" + split_line[0] + "' takes exactly 1 argument.", line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " If key is pressed goto LABEL " + split_line[1]) - if lp_events.pressed[coords[1]][coords[2]]: - return symbols["labels"][split_line[1]] + print("[" + lib + "] " + coords[0] + " If key is pressed goto LABEL " + split_line[1]) + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if lp_events.pressed[coords[1]][coords[2]]: # coords[1] is x, and coords[2] is y + if split_line[1] in symbols["labels"]: # The label should always exist + return symbols["labels"][split_line[1]] # and we return the line number the label is on + else: + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 return idx+1 -scripts.add_command(Control_If_Pressed_Goto_Label()) +scripts.add_command(Control_If_Pressed_Goto_Label()) # register the command # ################################################## @@ -213,45 +224,50 @@ def __init__( self, ): - super().__init__("IF_UNPRESSED_GOTO_LABEL") + super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 2: return ("'" + split_line[0] + "' takes exactly 1 argument.", line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) - if not lp_events.pressed[coords[1]][coords[2]]: - return symbols["labels"][split_line[1]] + print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed + return symbols["labels"][split_line[1]] # jump to the label return idx+1 -scripts.add_command(Control_If_Unpressed_Goto_Label()) +scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command # ################################################## @@ -264,19 +280,19 @@ def __init__( self, ): - super().__init__("REPEAT_LABEL") + super().__init__("REPEAT_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) @@ -287,38 +303,114 @@ def Validate( else: symbols["repeats"][idx] = int(split_line[2]) symbols["original"][idx] = int(split_line[2]) + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.name, line) + + return True + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ + split_line[2] + " times max") + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if symbols["repeats"][idx] > 0: + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + + return idx+1 + + +scripts.add_command(Control_Repeat_Label()) # register the command + + +# ################################################## +# ### CLASS Control_Repeat ### +# ################################################## + +# class that defines the REPEAT command. This operates more like a +# traditional repeat/until by causing the code to repeat n times (rather than +# n+1, and it resets the counter at the end +class Control_Repeat(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("REPEAT") # the name of the command as you have to enter it in the code + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + else: + symbols["repeats"][idx] = int(split_line[2])-1 + symbols["original"][idx] = int(split_line[2])-1 except: return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) + + if symbols["labels"][split_line[1]] > idx: + return ("Target for " + self.name + " must preceed the command.", line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ + print("[" + lib + "] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ split_line[2] + " times max") if symbols["repeats"][idx] > 0: - print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") - + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + symbols["repeats"][idx] = symbols["original"][idx] # makes this behave like a normal loop return idx+1 -scripts.add_command(Control_Repeat_Label()) +scripts.add_command(Control_Repeat()) # register the command # ################################################## @@ -331,19 +423,19 @@ def __init__( self, ): - super().__init__("IF_PRESSED_REPEAT_LABEL") + super().__init__("IF_PRESSED_REPEAT_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) @@ -357,35 +449,117 @@ def Validate( except: return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - if lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] - else: - print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") return idx+1 -scripts.add_command(Control_If_Pressed_Repeat_Label()) +scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command + + +# ################################################## +# ### CLASS Control_If_Pressed_Repeat ### +# ################################################## + +# class that defines the IF_PRESSED command. This operates more like a +# traditional repeat/until by causing the code to repeat n times (rather than +# n+1, and it resets the counter at the end +class Control_If_Pressed_Repeat(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_PRESSED_REPEAT") # the name of the command as you have to enter it in the code + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + else: + symbols["repeats"][idx] = int(split_line[2])-1 + symbols["original"][idx] = int(split_line[2])-1 + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.name, line) + + if symbols["labels"][split_line[1]] > idx: + return ("Target for " + self.name + " must preceed the command.", line) + + + return True + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + symbols["repeats"][idx] = symbols["original"][idx] # for a normal repeat statement + + return idx+1 + + +scripts.add_command(Control_If_Pressed_Repeat()) # register the command # ################################################## @@ -398,19 +572,19 @@ def __init__( self, ): - super().__init__("IF_UNPRESSED_REPEAT_LABEL") + super().__init__("IF_UNPRESSED_REPEAT_LABEL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) @@ -423,35 +597,116 @@ def Validate( except: return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - if pass_no == 2: + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.Name, line) + return ("Target not found for " + self.name, line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - if not lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[cmds_ctrl] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] - else: - print("[cmds_ctrl] " + coords[0] + " No repeats left, not repeating.") + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if not lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + + return idx+1 + + +scripts.add_command(Control_If_Unpressed_Repeat_Label()) # register the command + + +# ################################################## +# ### CLASS Control_If_Unpressed_Repeat ### +# ################################################## + +# class that defines the IF_UNPRESSED_REPEAT command. This operates more like a +# traditional repeat/until by causing the code to repeat n times (rather than +# n+1, and it resets the counter at the end +class Control_If_Unpressed_Repeat(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_UNPRESSED_REPEAT") # the name of the command as you have to enter it in the code + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + if len(split_line) != 3: + return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + + try: + temp = int(split_line[2]) + if temp < 1: + return (split_line[0] + " requires a minimum of 1 repeat.", line) + symbols["repeats"][idx] = int(split_line[2])-1 + symbols["original"][idx] = int(split_line[2])-1 + except: + return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.name, line) + + if symbols["labels"][split_line[1]] > idx: + return ("Target for " + self.name + " must preceed the command.", line) + + + return True + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if not lp_events.pressed[coords[1]][coords[2]]: + if symbols["repeats"][idx] > 0: + print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + symbols["repeats"][idx] -= 1 + return symbols["labels"][split_line[1]] + else: + print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + symbols["repeats"][idx] = symbols["original"][idx] # to behave more normal return idx+1 -scripts.add_command(Control_If_Unpressed_Repeat_Label()) +scripts.add_command(Control_If_Unpressed_Repeat()) # register the command # ################################################## @@ -464,16 +719,16 @@ def __init__( self, ): - super().__init__("RESET_REPEATS") + super().__init__("RESET_REPEATS") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if len(split_line) > 1: @@ -483,14 +738,14 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_ctrl] " + coords[0] + " Reset all repeats") + print("[" + lib + "] " + coords[0] + " Reset all repeats") for i in symbols["repeats"]: symbols["repeats"][i] = symbols["original"][i] @@ -498,6 +753,6 @@ def Run( return idx+1 -scripts.add_command(Control_Reset_Repeats()) +scripts.add_command(Control_Reset_Repeats()) # register the command diff --git a/commands_external.py b/commands_external.py index c12d250..855cd2e 100644 --- a/commands_external.py +++ b/commands_external.py @@ -1,5 +1,6 @@ import command_base, webbrowser, sound, subprocess, os, scripts +lib = "cmds_extn" # name of this library (for logging) # ################################################## # ### CLASS External_Web ### @@ -11,19 +12,19 @@ def __init__( self, ): - super().__init__("WEB") + super().__init__("WEB") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -34,25 +35,25 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): link = split_line[1] if "http" not in link: link = "http://" + link - print("[cmds_extn] " + coords[0] + " Open website " + link + " in default browser") + print("[" + lib + "] " + coords[0] + " Open website " + link + " in default browser") webbrowser.open(link) return idx+1 -scripts.add_command(External_Web()) +scripts.add_command(External_Web()) # register the command # ################################################## @@ -65,19 +66,19 @@ def __init__( self, ): - super().__init__("WEB_NEW") + super().__init__("WEB_NEW") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -88,25 +89,25 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): link = split_line[1] if "http" not in link: link = "http://" + link - print("[cmds_extn] " + coords[0] + " Open website " + link + " in default browser, try to make a new window") + print("[" + lib + "] " + coords[0] + " Open website " + link + " in default browser, try to make a new window") webbrowser.open_new(link) return idx+1 -scripts.add_command(External_Web_New()) +scripts.add_command(External_Web_New()) # register the command # ################################################## @@ -119,19 +120,19 @@ def __init__( self, ): - super().__init__("OPEN") + super().__init__("OPEN") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -145,23 +146,23 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): path_name = " ".join(split_line[1:]) - print("[cmds_extn] " + coords[0] + " Open file or folder " + path_name) + print("[" + lib + "] " + coords[0] + " Open file or folder " + path_name) files.open_file_folder(path_name) return idx+1 -scripts.add_command(External_Open()) +scripts.add_command(External_Open()) # register the command # ################################################## @@ -174,19 +175,19 @@ def __init__( self, ): - super().__init__("SOUND") + super().__init__("SOUND") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -197,25 +198,25 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): if len(split_line) > 2: - print("[cmds_extn] " + coords[0] + " Play sound file " + split_line[1] + \ + print("[" + lib + "] " + coords[0] + " Play sound file " + split_line[1] + \ " at volume " + str(split_line[2])) sound.play(split_line[1], float(split_line[2])) else: - print("[cmds_extn] " + coords[0] + " Play sound file " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Play sound file " + split_line[1]) sound.play(split_line[1]) return idx+1 -scripts.add_command(External_Sound()) +scripts.add_command(External_Sound()) # register the command # ################################################## @@ -228,19 +229,19 @@ def __init__( self, ): - super().__init__("SOUND_STOP") + super().__init__("SOUND_STOP") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 2: return ("Too many arguments for command '" + split_line[0] + "'.", line) @@ -248,11 +249,11 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): if len(split_line) > 1: @@ -267,7 +268,7 @@ def Run( return idx+1 -scripts.add_command(External_Sound()) +scripts.add_command(External_Sound()) # register the command # ################################################## @@ -280,19 +281,19 @@ def __init__( self, ): - super().__init__("CODE") + super().__init__("CODE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -300,24 +301,24 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): args = " ".join(split_line[1:]) - print("[cmds_extn] " + coords[0] + " Running code: " + args) + print("[" + lib + "] " + coords[0] + " Running code: " + args) try: subprocess.run(args) except Exception as e: - print("[cmds_extn] " + coords[0] + " Error with running code: " + str(e)) + print("[" + lib + "] " + coords[0] + " Error with running code: " + str(e)) return idx+1 -scripts.add_command(External_Code()) +scripts.add_command(External_Code()) # register the command diff --git a/commands_header.py b/commands_header.py index e5c66bb..1f2081f 100644 --- a/commands_header.py +++ b/commands_header.py @@ -10,42 +10,42 @@ def __init__( self, ): - super().__init__("@ASYNC", True) - + super().__init__("@ASYNC", # the name of the header as you have to enter it in the code + True) # You also define if the header causes the script to be asynchronous def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: - if idx > 0: - return ("@ASYNC must appear on the first line.", lines[0]) + if idx > 0: # headers normally have to check the line number + return (self.name + " must appear on the first line.", lines[0]) if len(split_line) > 1: - return ("@ASYNC takes no arguments.", lines[0]) + return (self.name + " takes no arguments.", lines[0]) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): return idx+1 -scripts.add_command(Header_Async()) +scripts.add_command(Header_Async()) # register the header # ################################################## @@ -57,46 +57,46 @@ def __init__( self, ): - super().__init__("@SIMPLE", False) - + super().__init__("@SIMPLE", # the name of the header as you have to enter it in the code + False) # You also define if the header causes the script to be asynchronous def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: - if idx > 0: - return ("@ASYNC must appear on the first line.", lines[0]) + if idx > 0: # headers normally have to check the line number + return (self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return ("@SIMPLE requires a key to bind.", line) + return (self.name + " requires a key to bind.", line) if len(split_line) > 2: - return ("@SIMPLE only take one argument", line) + return (self.name + " only take one argument", line) if kb.sp(split_line[1]) == None: return ("No key named '" + split_line[1] + "'.", line) for lin in lines[1:]: if lin != "" and lin[0] != "-": - return ("When @SIMPLE is used, scripts can only contain comments.", lin) + return ("When " + self.name + " is used, scripts can only contain comments.", lin) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): print("[cmds_head] " + coords + " Simple keybind: " + split_line[1]) @@ -118,7 +118,7 @@ def Run( return idx+1 -scripts.add_command(Header_Simple()) +scripts.add_command(Header_Simple()) # register the header # ################################################## @@ -130,33 +130,37 @@ def __init__( self, ): - super().__init__("@LOAD_LAYOUT", False) + super().__init__("@LOAD_LAYOUT", # the name of the header as you have to enter it in the code + False) # You also define if the header causes the script to be asynchronous def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: + if idx > 0: # headers normally have to check the line number + return (self.name + " must appear on the first line.", lines[0]) + if len(split_line) < 2: - return ("@LOAD_LAYOPUT requires a filename as a parameter.", line) + return (self.name + " requires a filename as a parameter.", line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): layout_name = " ".join(split_line[1:]) @@ -182,6 +186,6 @@ def Run( return idx+1 -scripts.add_command(Header_Load_Layout()) +scripts.add_command(Header_Load_Layout()) # register the header diff --git a/commands_keys.py b/commands_keys.py index fca6860..bbde62f 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -1,5 +1,6 @@ import command_base, kb, lp_events, scripts +lib = "cmds_ctrl" # name of this library (for logging) # ################################################## # ### CLASS Keys_Wait_Pressed ### @@ -11,19 +12,19 @@ def __init__( self, ): - super().__init__("WAIT_PRESSED") + super().__init__("WAIT_PRESSED") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: return ("Too many arguments for command '" + split_line[0] + "'.", line) @@ -31,15 +32,14 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async, - pass_no + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_keys] " + coords + " Wait for script key to be unpressed") + print("[" + lib + "] " + coords + " Wait for script key to be unpressed") while lp_events.pressed[coords[1]][coords[2]]: sleep(DELAY_EXIT_CHECK) @@ -49,7 +49,7 @@ def Run( return idx+1 -scripts.add_command(Keys_Wait_Pressed()) +scripts.add_command(Keys_Wait_Pressed()) # register the command # ################################################## @@ -62,19 +62,19 @@ def __init__( self, ): - super().__init__("TAP") + super().__init__("TAP") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -88,11 +88,11 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): key = kb.sp(split_line[1]) @@ -100,10 +100,10 @@ def Run( releasefunc = lambda: kb.release(key) if len(split_line) <= 2: - print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1]) kb.tap(key) elif len(split_line) <= 3: - print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + " times") + print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + " times") taps = int(split_line[2]) for tap in range(taps): @@ -111,7 +111,7 @@ def Run( return idx + 1 kb.tap(key) else: - print("[cmds_keys] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + \ + print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + \ " times for " + str(split_line[3]) + " seconds each") taps = int(split_line[2]) @@ -128,7 +128,7 @@ def Run( return idx+1 -scripts.add_command(Keys_Tap()) +scripts.add_command(Keys_Tap()) # register the command # ################################################## @@ -141,19 +141,19 @@ def __init__( self, ): - super().__init__("PRESS") + super().__init__("PRESS") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -167,14 +167,14 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_keys] " + coords[0] + " Press key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Press key " + split_line[1]) key = kb.sp(split_line[1]) kb.press(key) @@ -182,7 +182,7 @@ def Run( return idx+1 -scripts.add_command(Keys_Press()) +scripts.add_command(Keys_Press()) # register the command # ################################################## @@ -195,19 +195,19 @@ def __init__( self, ): - super().__init__("RELEASE") + super().__init__("RELEASE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -221,14 +221,14 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_keys] " + coords[0] + " Release key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Release key " + split_line[1]) key = kb.sp(split_line[1]) kb.release(key) @@ -236,7 +236,7 @@ def Run( return idx+1 -scripts.add_command(Keys_Release()) +scripts.add_command(Keys_Release()) # register the command # ################################################## @@ -249,19 +249,19 @@ def __init__( self, ): - super().__init__("RELEASE_ALL") + super().__init__("RELEASE_ALL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: return ("Too many arguments for command '" + split_line[0] + "'.", line) @@ -269,21 +269,21 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_keys] " + coords[0] + " Release all keys") + print("[" + lib + "] " + coords[0] + " Release all keys") kb.release_all() return idx+1 -scripts.add_command(Keys_Release_All()) +scripts.add_command(Keys_Release_All()) # register the command # ################################################## @@ -296,19 +296,19 @@ def __init__( self, ): - super().__init__("STRING") + super().__init__("STRING") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -316,20 +316,20 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): type_string = " ".join(split_line[1:]) - print("[cmds_keys] " + coords[0] + " Type out string " + type_string) + print("[" + lib + "] " + coords[0] + " Type out string " + type_string) kb.write(type_string) return idx+1 -scripts.add_command(Keys_String()) +scripts.add_command(Keys_String()) # register the command diff --git a/commands_mouse.py b/commands_mouse.py index 891871a..de276b2 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -1,5 +1,6 @@ import command_base, ms, scripts +lib = "cmds_mous" # name of this library (for logging) # ################################################## # ### CLASS Mouse_Move ### @@ -11,21 +12,21 @@ def __init__( self, ): - super().__init__("M_MOVE") + super().__init__("M_MOVE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): # no longer allow just 2 tokens - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -35,27 +36,27 @@ def Validate( try: temp = int(split_line[1]) except: - return ("'M_MOVE' X value '" + split_line[1] + "' not valid.", line) + return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'M_MOVE' Y value '" + split_line[2] + "' not valid.", line) + return ("'" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): # removed error for != 3 tokens - print("[cmds_mous] " + coords[0] + " Relative mouse movement (" + split_line[1] + ", " + \ + print("[" + lib + "] " + coords[0] + " Relative mouse movement (" + split_line[1] + ", " + \ str(split_line[2]) + ")") ms.move_to_pos(float(split_line[1]), float(split_line[2])) @@ -63,7 +64,7 @@ def Run( return idx+1 -scripts.add_command(Mouse_Move()) +scripts.add_command(Mouse_Move()) # register the command # ################################################## @@ -76,21 +77,21 @@ def __init__( self, ): - super().__init__("M_SET") + super().__init__("M_SET") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): # no longer allow just 2 tokens - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -100,27 +101,27 @@ def Validate( try: temp = int(split_line[1]) except: - return ("'M_SET' X value '" + split_line[1] + "' not valid.", line) + return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'M_SET' Y value '" + split_line[2] + "' not valid.", line) + return ("'" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): # removed error for != 3 tokens - print("[cmds_mous] " + coords[0] + " Set mouse position to (" + split_line[1] + ", " + \ + print("[" + lib + "] " + coords[0] + " Set mouse position to (" + split_line[1] + ", " + \ str(split_line[2]) + ")") ms.set_pos(float(split_line[1]), float(split_line[2])) @@ -128,7 +129,7 @@ def Run( return idx+1 -scripts.add_command(Mouse_Set()) +scripts.add_command(Mouse_Set()) # register the command # ################################################## @@ -141,19 +142,19 @@ def __init__( self, ): - super().__init__("M_SCROLL") + super().__init__("M_SCROLL") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -163,7 +164,7 @@ def Validate( try: temp = int(split_line[1]) except: - return ("'M_SET' X value '" + split_line[1] + "' not valid.", line) + return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = float(split_line[1]) @@ -180,24 +181,24 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): if len(split_line) > 2: - print("[cmds_mous] " + coords + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") + print("[" + lib + "] " + coords + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") ms.scroll(float(split_line[2]), float(split_line[1])) else: - print("[cmds_mous] " + coords + " Scroll " + split_line[1]) + print("[" + lib + "] " + coords + " Scroll " + split_line[1]) ms.scroll(0, float(split_line[1])) return idx+1 -scripts.add_command(Mouse_Scroll()) +scripts.add_command(Mouse_Scroll()) # register the command # ################################################## @@ -210,19 +211,19 @@ def __init__( self, ): - super().__init__("M_LINE") + super().__init__("M_LINE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 5: return ("Too few arguments for command '" + split_line[0] + "'.", line) @@ -232,46 +233,46 @@ def Validate( try: temp = int(split_line[1]) except: - return ("'M_LINE' X1 value '" + split_line[1] + "' not valid.", line) + return ("'" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'M_LINE' Y1 value '" + split_line[2] + "' not valid.", line) + return ("'" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) try: temp = int(split_line[3]) except: - return ("'M_LINE' X2 value '" + split_line[3] + "' not valid.", line) + return ("'" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) try: temp = int(split_line[4]) except: - return ("'M_LINE' Y2 value '" + split_line[4] + "' not valid.", line) + return ("'" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) if len(split_line) >= 6: try: temp = float(split_line[5]) except: - return ("'M_LINE' wait value '" + split_line[5] + "' not valid.", line) + return ("'" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) if len(split_line) >= 7: try: temp = int(split_line[6]) if temp == 0: - return ("'M_LINE' skip value cannot be zero.", line) + return ("'" + self.name + "' skip value cannot be zero.", line) except: - return ("'M_LINE' skip value '" + split_line[6] + "' not valid.", line) + return ("'" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) return True def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): x1 = int(split_line[1]) @@ -288,11 +289,11 @@ def Run( skip = int(split_line[6]) if (delay == None) or (delay <= 0): - print("[cmds_mous] " + coords[0] + " Mouse line from (" + \ + print("[" + lib + "] " + coords[0] + " Mouse line from (" + \ split_line[1] + ", " + split_line[2] + ") to (" + \ split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step") else: - print("[cmds_mous] " + coords + " Mouse line from (" + \ + print("[" + lib + "] " + coords + " Mouse line from (" + \ split_line[1] + ", " + split_line[2] + ") to (" + \ split_line[3] + ", " + split_line[4] + ") by " + \ str(skip) + " pixels per step and wait " + split_line[5] + " milliseconds between each step") @@ -312,11 +313,11 @@ def Run( return idx+1 -scripts.add_command(Mouse_Line()) +scripts.add_command(Mouse_Line()) # register the command # ################################################## -# ### CLASS Mouse_Line_Move ### +# ### CLASS Mouse_Line_Move ### # ################################################## # class that defines the M_LINE_MOVE command @@ -325,19 +326,19 @@ def __init__( self, ): - super().__init__("M_LINE_MOVE") + super().__init__("M_LINE_MOVE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) @@ -369,11 +370,11 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): x1 = int(split_line[1]) @@ -388,11 +389,11 @@ def Run( skip = int(split_line[4]) if (delay == None) or (delay <= 0): - print("[cmds_mous] " + coords + " Mouse line move relative (" + \ + print("[" + lib + "] " + coords + " Mouse line move relative (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step") else: - print("[cmds_mous] " + coords + " Mouse line move relative (" + \ + print("[" + lib + "] " + coords + " Mouse line move relative (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step and wait " + split_line[3] + " milliseconds between each step") @@ -413,7 +414,7 @@ def Run( return idx+1 -scripts.add_command(Mouse_Line_Move()) +scripts.add_command(Mouse_Line_Move()) # register the command # ################################################## @@ -426,19 +427,19 @@ def __init__( self, ): - super().__init__("M_LINE_SET") + super().__init__("M_LINE_SET") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) @@ -470,11 +471,11 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): x1 = int(split_line[1]) @@ -489,11 +490,11 @@ def Run( skip = int(split_line[4]) if (delay == None) or (delay <= 0): - print("[cmds_mous] " + coords[0] + " Mouse line set (" + \ + print("[" + lib + "] " + coords[0] + " Mouse line set (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step") else: - print("[cmds_mous] " + coords[0] + " Mouse line set (" + \ + print("[" + lib + "] " + coords[0] + " Mouse line set (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step and wait " + split_line[3] + " milliseconds between each step") @@ -511,7 +512,7 @@ def Run( return idx+1 -scripts.add_command(Mouse_Line_Set()) +scripts.add_command(Mouse_Line_Set()) # register the command # ################################################## @@ -524,19 +525,19 @@ def __init__( self, ): - super().__init__("M_RECALL_LINE") + super().__init__("M_RECALL_LINE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: try: temp = float(split_line[1]) @@ -555,11 +556,11 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): x1, y1 = symbols['m_pos'] @@ -573,10 +574,10 @@ def Run( skip = int(split_line[2]) if (delay == None) or (delay <= 0): - print("[cmds_mous] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + print("[" + lib + "] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ " in a line by " + str(skip) + " pixels per step") else: - print("[cmds_mous] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + print("[" + lib + "] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ " in a line by " + str(skip) + " pixels per step and wait " + \ split_line[1] + " milliseconds between each step") @@ -596,7 +597,7 @@ def Run( return idx+1 -scripts.add_command(Mouse_Recall_Line()) +scripts.add_command(Mouse_Recall_Line()) # register the command # ################################################## @@ -609,19 +610,19 @@ def __init__( self, ): - super().__init__("M_STORE") + super().__init__("M_STORE") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: return ("'" + split_line[0] + "' takes no arguments.", line) @@ -629,21 +630,21 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_mous] " + coords[0] + " Store mouse position") + print("[" + lib + "] " + coords[0] + " Store mouse position") - symbols["m_pos"] = ms.get_pos() + symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. return idx+1 -scripts.add_command(Mouse_Store()) +scripts.add_command(Mouse_Store()) # register the command # ################################################## @@ -656,7 +657,7 @@ def __init__( self, ): - super().__init__("M_RECALL") + super().__init__("M_RECALL") # the name of the command as you have to enter it in the code def Validate( self, @@ -668,7 +669,7 @@ def Validate( pass_no ): - if pass_no == 1: + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: return ("'" + split_line[0] + "' takes no arguments.", line) @@ -676,22 +677,22 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): if symbols['m_pos'] == tuple(): - print("[cmds_mous] " + coords[0] + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + print("[" + lib + "] " + coords[0] + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[cmds_mous] " + coords[0] + " Recall mouse position " + str(symbols['m_pos'])) + print("[" + lib + "] " + coords[0] + " Recall mouse position " + str(symbols['m_pos'])) ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) return idx+1 -scripts.add_command(Mouse_Recall()) +scripts.add_command(Mouse_Recall()) # register the command diff --git a/commands_pause.py b/commands_pause.py index 58e57d3..ce23b90 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -1,5 +1,6 @@ import command_base, scripts +lib = "cmds_paus" # name of this library (for logging) # ################################################## # ### CLASS Pause_Delay ### @@ -11,16 +12,16 @@ def __init__( self, ): - super().__init__("DELAY") + super().__init__("DELAY") # the name of the command as you have to enter it in the code def Validate( self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: @@ -41,14 +42,14 @@ def Validate( def Run( self, - idx: int, - split_line, - symbols, - coords, - is_async + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously ): - print("[cmds_paus] " + coords[0] + " Delay for " + split_line[1] + " seconds") + print("[" + lib + "] " + coords[0] + " Delay for " + split_line[1] + " seconds") delay = float(split_line[1]) @@ -58,6 +59,7 @@ def Run( return idx+1 -scripts.add_command(Pause_Delay()) +scripts.add_command(Pause_Delay()) # register the command + From 268f2f3b60c729e8a475be77a0d6b492e8c6a7c0 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 29 Aug 2020 12:00:24 +0800 Subject: [PATCH 03/83] Fix an ugly cut and paste error in the documentation --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c6bb5f..b2fc1dd 100644 --- a/README.md +++ b/README.md @@ -222,8 +222,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * Reset the counter on all repeats. (no arguments) * `SOUND` * Play a sound named (argument 1) inside the `user_sounds/` folder. - * Supports `.wa * Works the same as the IF_UNPRESSED_REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. -v`, `.flac`, and `.ogg` only. + * Supports `.wav`, `.flac`, and `.ogg` only. * If (argument 2) supplied, set volume to (argument 2). * Range is 0 to 100 * `SOUND_STOP` From f8f928c2c02aea989e9e83cab93f7ce6a425a873 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 29 Aug 2020 14:22:45 +0800 Subject: [PATCH 04/83] Minor changes and documentation: 1) break out the import of command modules so you don't have to modify LPHK.py to add a command 2) minor bug fix to error reporting in @ASYNC 3) correction of definition of symbol table in scripts.py --- LPHK.py | 2 +- command_list.py | 9 +++++++++ commands_header.py | 2 +- scripts.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 command_list.py diff --git a/LPHK.py b/LPHK.py index d319e48..ea6691f 100755 --- a/LPHK.py +++ b/LPHK.py @@ -88,7 +88,7 @@ def datetime_str(): from utils import launchpad_connector # just import the control modules to automatically integrate them -import commands_header, commands_control, commands_keys, commands_mouse, commands_pause, commands_external +import command_list lp = launchpad.Launchpad() diff --git a/command_list.py b/command_list.py new file mode 100644 index 0000000..7952976 --- /dev/null +++ b/command_list.py @@ -0,0 +1,9 @@ +# This module exists simply to list the command modules imported into LPHK + +import \ + commands_header, \ + commands_control, \ + commands_keys, \ + commands_mouse, \ + commands_pause, \ + commands_external \ No newline at end of file diff --git a/commands_header.py b/commands_header.py index 1f2081f..7cf7a6f 100644 --- a/commands_header.py +++ b/commands_header.py @@ -28,7 +28,7 @@ def Validate( return (self.name + " must appear on the first line.", lines[0]) if len(split_line) > 1: - return (self.name + " takes no arguments.", lines[0]) + return (self.name + " takes no arguments.", line) return True diff --git a/scripts.py b/scripts.py index d7a44cc..8e627c5 100644 --- a/scripts.py +++ b/scripts.py @@ -49,7 +49,7 @@ def new_symbol_table(): "repeats": dict(), "original": dict(), "labels": dict(), - "m_pos": dict() } + "m_pos": tuple() } return symbols From f3d58017f31992b43dbf10f7c663dd670e17d120 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 1 Sep 2020 21:04:53 +0800 Subject: [PATCH 05/83] Added a new command RPN_EVAL. This allows RPN expressions to be evaluated (like an old time Hewlett Packard calculator). Very little functionality at the moment, but adding variables to the the symbol table will allow it to do real math and use the results. Putting the stack in the symbol table would allow this to be used to pass values between routines (as parameters), even allowing recursive routines!. Minimal checking right now, but a couple of examples show what can be done. --- command_list.py | 3 +- commands_rpncalc.py | 128 +++++++++++++++++++++++++++++++++ user_scripts/examples/rpn.lps | 1 + user_scripts/examples/rpn2.lps | 1 + 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 commands_rpncalc.py create mode 100644 user_scripts/examples/rpn.lps create mode 100644 user_scripts/examples/rpn2.lps diff --git a/command_list.py b/command_list.py index 7952976..9ad0905 100644 --- a/command_list.py +++ b/command_list.py @@ -6,4 +6,5 @@ commands_keys, \ commands_mouse, \ commands_pause, \ - commands_external \ No newline at end of file + commands_external, \ + commands_rpncalc diff --git a/commands_rpncalc.py b/commands_rpncalc.py new file mode 100644 index 0000000..6b7c680 --- /dev/null +++ b/commands_rpncalc.py @@ -0,0 +1,128 @@ +import command_base, lp_events, scripts + +lib = "cmds_rpnc" # name of this library (for logging) + +# ################################################## +# ### CLASS RpnCalc_Rpn_Eval ### +# ################################################## + +# class that defines the RPN_EVAL command. +# This command allows math to be performed on a simulated RPN calculator. +# This is useful because as a stack model it also provides the framework for +# passing parameters to and from other routines if the stack is preserved +# in the symbol table. In this version The output is to the log, but it +# is easily extended. +class RpnCalc_Rpn_Eval(command_base.Command_Basic): + + def add(self, stack): + a = stack.pop() + b = stack.pop() + stack.append(b+a) + + def subtract(self, stack): + a = stack.pop() + b = stack.pop() + stack.append(b-a) + + def multiply(self, stack): + a = stack.pop() + b = stack.pop() + stack.append(b*a) + + def divide(self, stack): + a = stack.pop() + b = stack.pop() + stack.append(b/a) + + def view(self, stack): + print('Top of stack = ', stack[-1]) + + def views(self, stack): + print('Stack = ', stack) + + def pi(self, stack): + stack.append(3.1415926535) + + def sqr(self, stack): + a = stack.pop() + stack.append(a*a) + + def dup(self, stack): + stack.append(stack[-1]) + + def clst(self, stack): + stack.clear() + + def __init__( + self, + ): + + super().__init__("RPN_EVAL") # the name of the command as you have to enter it in the code + + self.operators = dict() + self.operators["+"] = self.add + self.operators["-"] = self.subtract + self.operators["*"] = self.multiply + self.operators["/"] = self.divide + self.operators["VIEW"] = self.view + self.operators["VIEWS"] = self.views + self.operators["PI"] = self.pi + self.operators["SQR"] = self.sqr + self.operators["DUP"] = self.dup + self.operators["CLST"] = self.clst + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + # check number of split_line + if len(split_line) < 2: + return ("Wrong number of parameters (at least 1 required) in " + self.name, line) + + # I'm not going to parse the commands at the moment. + + return True # return True if there is no error + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" + + stack = [] # this is local, but it could be stored globally in the symbol table! + + for i in split_line[1:]: + try: + n = float(i) + ok = True + except ValueError: + ok = False + + if ok: + stack.append(n) + continue + + opr = i.upper() + if opr in self.operators: + self.operators[opr](stack) + else: + print("invalid operator '", i, "'") + break + + return idx+1 # Normal default exit to the next line + + +scripts.add_command(RpnCalc_Rpn_Eval()) # register the command + diff --git a/user_scripts/examples/rpn.lps b/user_scripts/examples/rpn.lps new file mode 100644 index 0000000..cee1fa0 --- /dev/null +++ b/user_scripts/examples/rpn.lps @@ -0,0 +1 @@ +RPN_EVAL 1 1 + view 2 * 0.5 / view diff --git a/user_scripts/examples/rpn2.lps b/user_scripts/examples/rpn2.lps new file mode 100644 index 0000000..820ab76 --- /dev/null +++ b/user_scripts/examples/rpn2.lps @@ -0,0 +1 @@ +RPN_EVAL 3 2 1 view views + * view views pi sqr view / views dup 1 - views From ba4087739d91bca6600d2e05a10cb2b3dca2c4b0 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 4 Sep 2020 20:16:09 +0800 Subject: [PATCH 06/83] Significant modifications to the RPN_EVAL command. - Adds variables (global and local, but both currently local) - Adds error checking - Adds more commands - Documentation! --- README.md | 32 ++++ commands_rpncalc.py | 298 ++++++++++++++++++++++++++++----- user_scripts/examples/rpn2.lps | 2 +- user_scripts/examples/rpn3.lps | 4 + variables.py | 61 +++++++ 5 files changed, 350 insertions(+), 47 deletions(-) create mode 100644 user_scripts/examples/rpn3.lps create mode 100644 variables.py diff --git a/README.md b/README.md index b2fc1dd..ec7a8a5 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,38 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * `M_STORE` * Stores the current mouse position for use with the `M_RECALL*` commands. +#### Variables and calculator [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +* `RPN_EVAL` + * An RPN (stack-based) calculator that implements local and global variables (global variables are not global yet). + * Any number of commands may follow from 1 to infinity? + * Commands are NOT case sensitive + * Any numeric value is pushed onto the stack + * Common functions pop their parameters off the stack and push the result. Note that any function requiring more values that there are on the stack will be returned zero for all additional parameters. + * + - replaces the top two values on the stack with their sum + * - - replaces the top two values on the stack with their difference + * * - replaces the top two values on the stack with their product + * / - replaces the top two values on the stack with their quotient + * Some operations only change the top value on the stack. + * 1/x - replaces the top value on the stack with it's inverse + * sqr - replaces the value on the top of the stack with its square + * Some operations manipulate the stack + * dup - duplicates the value on the top of the stack + * x<>y - swaps the position of the top two items on the stack + * clst - clears the stack + * Some operations handle variables (these are all followed by a variable name). Note that refering to a variable that does not exist will return zero, but not greate that variable. Whilst it is possible to name a variable using a string of numbers representing a number (e.g. '32') these will likely not be accessible from other commands -- AVOID THEM + * >L {x} - Takes the value on the top of the stack and stores it in local variable {x} + * >G {x} - Takes the value on the top of the stack and stores it in the globalk variable {x} + * > {x} - Stores the value in the local variable {x} if it exists, otherwise the global variable {x} if it exists, otherwise creates a new local variable {x} + * Y"] = self.swap_x_y + self.operators[">"] = self.sto + self.operators[">L"] = self.sto_l + self.operators[">G"] = self.sto_g + self.operators["<"] = self.rcl + self.operators["y view_s diff --git a/user_scripts/examples/rpn3.lps b/user_scripts/examples/rpn3.lps new file mode 100644 index 0000000..af40752 --- /dev/null +++ b/user_scripts/examples/rpn3.lps @@ -0,0 +1,4 @@ +RPN_EVAL 1 > x view_s < x view_s view_l view_g +RPN_EVAL 2 >G y view_s < y Y view_s +RPN_EVAL view_g diff --git a/variables.py b/variables.py new file mode 100644 index 0000000..ca674c6 --- /dev/null +++ b/variables.py @@ -0,0 +1,61 @@ +# operations needed to access variables + + +# Note that popping a value from an empty stack returns 0. An alternative is +# to return an error +def pop(syms): + # return the top valie from the stack (and remove it) + try: + return syms['stack'].pop() # take the top value from the stack in the supplied symbol table + except: + return 0 + # raise Exception("Stack empty") + + +def push(syms, val): + # put val on to the top of the stack + syms['stack'].append(val) # Push a value onto the stack in the supplied symbol table + + +# the top of the stack will also return 0 for an empty stack. Alternatively it could +# return an error. +def top(syms): + # peek at the top value of the stack without removing it + try: + return syms['stack'][-1] + except: + return 0 + #raise Exception("Stack empty") + + +def is_defined(name, sym): + # is the variable defined in the symbol library + return sym and name in sym['vars'] + + +# This returns 0 if the variable is not defined. An alternative is to return an error +def get(name, syms_1, syms_2): + # get a variable. look in one symbol table, then the next. + # this allows an order to be defined to get local vars then global + if is_defined(name, syms_1): # First look in the local symbol table (if defined) + return syms_1['vars'][name] + if is_defined(name, syms_2): # then the global one + return syms_2['vars'][name] + return 0 + # raise Exception("Variable not found") + + +def put(name, val, syms): + # store a value in a named variable in a specific variable list + syms['vars'][name] = val + + +# if you try to grab an argument where no more exists, an error will result +def next_cmd(ret, cmds): + # pull the next value from the commands list and return incremented result + try: + v = cmds[ret] # we get the next element + except: + raise Exception("Can't get next element.") + else: + return ret+1, v # and we return an updated pointer and the removed element From 2ca6d0e283d3d0929e0b5680ade9bd373b1ad12f Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 4 Sep 2020 21:48:50 +0800 Subject: [PATCH 07/83] Improvements to RPN_EVAL - Bug fixes - Documentation fixes - More documentation - Conditional statements in RPN_EVAL - Stack and global variables are now global to the scripts I would prefer if the stack and global variables were preserved across executions of a script, and local variables are local to that execution of the script (not just the line). Even better would be variables shared between concurrently running scripts (1 instance per instance of LPHK). that may be next. --- README.md | 40 +++++-- commands_rpncalc.py | 200 ++++++++++++++++++++++++++------- scripts.py | 4 +- user_scripts/examples/rpn3.lps | 21 +++- variables.py | 12 +- 5 files changed, 221 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index ec7a8a5..5d5a1d6 100644 --- a/README.md +++ b/README.md @@ -291,33 +291,51 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * `RPN_EVAL` * An RPN (stack-based) calculator that implements local and global variables (global variables are not global yet). * Any number of commands may follow from 1 to infinity? - * Commands are NOT case sensitive + * Commands and variables are NOT case sensitive * Any numeric value is pushed onto the stack * Common functions pop their parameters off the stack and push the result. Note that any function requiring more values that there are on the stack will be returned zero for all additional parameters. * + - replaces the top two values on the stack with their sum - * - - replaces the top two values on the stack with their difference - * * - replaces the top two values on the stack with their product - * / - replaces the top two values on the stack with their quotient + * - - replaces the top two values on the stack with their difference + * * - replaces the top two values on the stack with their product + * / - replaces the top two values on the stack with their quotient * Some operations only change the top value on the stack. * 1/x - replaces the top value on the stack with it's inverse * sqr - replaces the value on the top of the stack with its square * Some operations manipulate the stack * dup - duplicates the value on the top of the stack + * pop - removes the top item from the stack * x<>y - swaps the position of the top two items on the stack - * clst - clears the stack + * clst - clears the stack * Some operations handle variables (these are all followed by a variable name). Note that refering to a variable that does not exist will return zero, but not greate that variable. Whilst it is possible to name a variable using a string of numbers representing a number (e.g. '32') these will likely not be accessible from other commands -- AVOID THEM * >L {x} - Takes the value on the top of the stack and stores it in local variable {x} - * >G {x} - Takes the value on the top of the stack and stores it in the globalk variable {x} - * > {x} - Stores the value in the local variable {x} if it exists, otherwise the global variable {x} if it exists, otherwise creates a new local variable {x} - * G {x} - Takes the value on the top of the stack and stores it in the globalk variable {x} + * > {x} - Stores the value in the local variable {x} if it exists, otherwise the global variable {x} if it exists, otherwise creates a new local variable {x} + * Y? - Is the top value of the stack greater than the next value on the stack + * X>=Y? - Is the top value of the stack greater than or equal to the next value on the stack + * X x view_s < x view_s view_l view_g +RPN_EVAL view_s view_l view_g +RPN_EVAL 0 x=0? view +RPN_EVAL x!=0? view +RPN_EVAL 1 x=y? view +RPN_EVAL x!=y? view +RPN_EVAL ? x view +RPN_EVAL !? x view +RPN_EVAL 1 > x 1 + >G Z < x view_s view_l view_g +RPN_EVAL >L x ? x view +RPN_EVAL >L x !? x view +RPN_EVAL >L x ?L x view +RPN_EVAL >L x !?L x view +RPN_EVAL >L x ?G x view +RPN_EVAL >L x !?G x view +RPN_EVAL ? z view +RPN_EVAL !? z view +RPN_EVAL ?L z view +RPN_EVAL !?L z view +RPN_EVAL ?G z view +RPN_EVAL !?G z view RPN_EVAL 2 >G y view_s < y Y view_s RPN_EVAL view_g diff --git a/variables.py b/variables.py index ca674c6..09de90a 100644 --- a/variables.py +++ b/variables.py @@ -19,10 +19,10 @@ def push(syms, val): # the top of the stack will also return 0 for an empty stack. Alternatively it could # return an error. -def top(syms): - # peek at the top value of the stack without removing it +def top(syms, i): + # peek at the top value of the stack without removing it (for i=1, y:i=2, z:i=3...) try: - return syms['stack'][-1] + return syms['stack'][-i] except: return 0 #raise Exception("Stack empty") @@ -30,13 +30,15 @@ def top(syms): def is_defined(name, sym): # is the variable defined in the symbol library - return sym and name in sym['vars'] + return sym and name.lower() in sym['vars'] # This returns 0 if the variable is not defined. An alternative is to return an error def get(name, syms_1, syms_2): # get a variable. look in one symbol table, then the next. # this allows an order to be defined to get local vars then global + name = name.lower() + if is_defined(name, syms_1): # First look in the local symbol table (if defined) return syms_1['vars'][name] if is_defined(name, syms_2): # then the global one @@ -47,7 +49,7 @@ def get(name, syms_1, syms_2): def put(name, val, syms): # store a value in a named variable in a specific variable list - syms['vars'][name] = val + syms['vars'][name.lower()] = val # if you try to grab an argument where no more exists, an error will result From f768b7a5799c3e69043bd4c2fe2797c2821bd189 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 6 Sep 2020 02:04:10 +0800 Subject: [PATCH 08/83] Useful implementation of local and global variables for the RPN_EVAL command * the stack and local variables are maintained across executions of a script. * the script is only validated the first time you run it, subsequently it is assumed to be OK * RESET_REPEATS is automatically called at the beginning of script execution to reset counters * Global variables are shared across ALL scripts * NO synchronisation of access to global variables (this will likely be problematic and I will fix it later) * Refactored scripts.py to make a button self-aware. In a future version this could be passed to commands to give a simpler interface to commands. * updated documentation * commented examples for the RPN_EVAL function showing how the stack, local, and global variables work. --- LPHK.py | 4 +- README.md | 9 +- commands_rpncalc.py | 218 +++++++------ lp_colors.py | 5 +- scripts.py | 560 ++++++++++++++++++--------------- user_scripts/examples/rpn.lps | 3 +- user_scripts/examples/rpn2.lps | 1 + user_scripts/examples/rpn3.lps | 1 + user_scripts/examples/rpn4.lps | 4 + user_scripts/examples/rpn5.lps | 14 + variables.py | 18 +- window.py | 14 +- 12 files changed, 473 insertions(+), 378 deletions(-) create mode 100644 user_scripts/examples/rpn4.lps create mode 100644 user_scripts/examples/rpn5.lps diff --git a/LPHK.py b/LPHK.py index ea6691f..214fc64 100755 --- a/LPHK.py +++ b/LPHK.py @@ -115,8 +115,8 @@ def shutdown(): scripts.to_run = [] for x in range(9): for y in range(9): - if scripts.threads[x][y] != None: - scripts.threads[x][y].kill.set() + if scripts.buttons[x][y].thread != None: + scripts.buttons[x][y].thread.kill.set() if window.lp_connected: scripts.unbind_all() lp_events.timer.cancel() diff --git a/README.md b/README.md index 5d5a1d6..d259a09 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * pop - removes the top item from the stack * x<>y - swaps the position of the top two items on the stack * clst - clears the stack + * stack - pushes the length of the stack onto the stack * Some operations handle variables (these are all followed by a variable name). Note that refering to a variable that does not exist will return zero, but not greate that variable. Whilst it is possible to name a variable using a string of numbers representing a number (e.g. '32') these will likely not be accessible from other commands -- AVOID THEM * >L {x} - Takes the value on the top of the stack and stores it in local variable {x} * >G {x} - Takes the value on the top of the stack and stores it in the globalk variable {x} @@ -313,6 +314,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * variables.top(g_syms, 2): + def x_gt_y(self, symbols, cmd, cmds): + if variables.top(symbols, 1) > variables.top(symbols, 2): return 1 else: return len(cmds)+1 - def x_ge_y(self, g_syms, l_syms, cmd, cmds): - if variables.top(g_syms, 1) >= variables.top(g_syms, 2): + def x_ge_y(self, symbols, cmd, cmds): + if variables.top(symbols, 1) >= variables.top(symbols, 2): return 1 else: return len(cmds)+1 - def x_lt_y(self, g_syms, l_syms, cmd, cmds): - if variables.top(g_syms, 1) < variables.top(g_syms, 2): + def x_lt_y(self, symbols, cmd, cmds): + if variables.top(symbols, 1) < variables.top(symbols, 2): return 1 else: return len(cmds)+1 - def x_le_y(self, g_syms, l_syms, cmd, cmds): - if variables.top(g_syms, 1) <= variables.top(g_syms, 2): + def x_le_y(self, symbols, cmd, cmds): + if variables.top(symbols, 1) <= variables.top(symbols, 2): return 1 else: return len(cmds)+1 - def is_def(self, g_syms, l_syms, cmd, cmds): + def is_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if variables.is_defined(v, g_syms) or variables.is_defined(v, l_syms): + if variables.is_defined(v, symbols['g_vars']) or variables.is_defined(v, symbols['l_vars']): return ret else: return len(cmds)+1 - def is_not_def(self, g_syms, l_syms, cmd, cmds): + def is_not_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if not (variables.is_defined(v, g_syms) or variables.is_defined(v, l_syms)): + if not (variables.is_defined(v, symbols['g_vars']) or variables.is_defined(v, symbols['l_vars'])): return ret else: return len(cmds)+1 - def is_local_def(self, g_syms, l_syms, cmd, cmds): + def is_local_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if variables.is_defined(v, l_syms): + if variables.is_defined(v, symbols['l_vars']): return ret else: return len(cmds)+1 - def is_local_not_def(self, g_syms, l_syms, cmd, cmds): + def is_local_not_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if not variables.is_defined(v, l_syms): + if not variables.is_defined(v, symbols['l_vars']): return ret else: return len(cmds)+1 - def is_global_def(self, g_syms, l_syms, cmd, cmds): + def is_global_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if variables.is_defined(v, g_syms): + if variables.is_defined(v, symbols['g_vars']): return ret else: return len(cmds)+1 - def is_global_not_def(self, g_syms, l_syms, cmd, cmds): + def is_global_not_def(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) - if not variables.is_defined(v, g_syms): + if not variables.is_defined(v, symbols['g_vars']): return ret else: return len(cmds)+1 @@ -372,6 +387,8 @@ def __init__( self.operators["DUP"] = self.dup # Duplicate top of stack self.operators["POP"] = self.pop # remove item from top of stack self.operators["CLST"] = self.clst # clear stack + self.operators["CL_L"] = self.cl_l # clear local variables + self.operators["STACK"] = self.stack_len # length of stack self.operators["X<>Y"] = self.swap_x_y # swap x and y self.operators[">"] = self.sto # store self.operators[">L"] = self.sto_l # store local @@ -424,9 +441,6 @@ def Run( print("[" + lib + "] " + coords[0] + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" - l_syms = { # this would be the local symbol table - "vars": dict()} # containing just the local variables - i = 1 # using a loop counter rath erthan an itterator because it's hard to pass iters as params while i < len(split_line): # for each item of the line of tokens cmd = split_line[i] # get the current one @@ -443,7 +457,7 @@ def Run( opr = cmd.upper() # Convert to uppercase for searching if opr in self.operators: # if it's valid try: - i = i + self.operators[opr](symbols, l_syms, opr, split_line[i:]) # run it + i = i + self.operators[opr](symbols, opr, split_line[i:]) # run it except: print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " '" + cmd + "'") break diff --git a/lp_colors.py b/lp_colors.py index 9f7ec9e..2242f50 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -86,10 +86,11 @@ def luminance(r, g, b): def updateXY(x, y): if window.lp_connected: + btn = scripts.buttons[x][y] if (x, y) != (8, 0): is_running = False - if scripts.threads[x][y] != None: - if scripts.threads[x][y].isAlive(): + if btn.thread != None: + if btn.thread.isAlive(): is_running = True is_func_key = ((y == 0) or (x == 8)) diff --git a/scripts.py b/scripts.py index 71543f1..13ed53f 100644 --- a/scripts.py +++ b/scripts.py @@ -15,6 +15,17 @@ HEADERS = dict() +# GLOBALS is likewise empty until global variables get created + +GLOBALS = dict() + + +COLOR_PRIMED = 5 #red +COLOR_FUNC_KEYS_PRIMED = 9 #amber +EXIT_UPDATE_DELAY = 0.1 +DELAY_EXIT_CHECK = 0.025 + + # Add a new command. This removes any existing command of the same name from the VALID_COMMANDS # and returns it as the result @@ -50,350 +61,393 @@ def new_symbol_table(): "original": dict(), "labels": dict(), "m_pos": tuple(), - "vars": dict(), # global (to the script) variables + "g_vars": GLOBALS, + "l_vars": dict(), # global (to the script) variables "stack": [] } # script stack (for RPN_EVAL) return symbols -# Do what is required to parse the script. Parsing does not output any information unless it is an error - -def parse_script(script_lines, symbols): - err = True - errors = 0 # no errors found - - for pass_no in (1,2): # pass 1, collect info & syntax check, - # pass 2 symbol check & assocoated processing - for idx,line in enumerate(script_lines): # gen line number and text - if is_ignorable_line(line): - continue # don't process ignorable lines - split_line = line.split(" ") # split line on spaces - if split_line[0] in VALID_COMMANDS: # if first element is a command - res = VALID_COMMANDS[split_line[0]].Parse(idx, line, script_lines, split_line, symbols, pass_no); - if res != True: +# ################################################## +# ### CLASS Button ### +# ################################################## + +# class that defines a button command. +# A button is a class containing all that's essential for a button. +class Button(): + def __init__( + self, + x, # The button column + y, # The button row + script_str # The Script + ): + + self.x = x + self.y = y + self.script_str = script_str # The script + self.validated = False # Has the script been validated? + self.symbols = None # The symbol table for the button + self.script_lines = None # the lines of the script + self.thread = None # the thread associated with this button + self.running = False # is the script running? + self.is_async = False # async execution flag + self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? + + + # Do what is required to parse the script. Parsing does not output any information unless it is an error + + def parse_script(self): + err = True + errors = 0 # no errors found + + for pass_no in (1,2): # pass 1, collect info & syntax check, + # pass 2 symbol check & assocoated processing + for idx,line in enumerate(self.script_lines): # gen line number and text + if self.is_ignorable_line(line): + continue # don't process ignorable lines + split_line = line.split(" ") # split line on spaces + if split_line[0] in VALID_COMMANDS: # if first element is a command + res = VALID_COMMANDS[split_line[0]].Parse(idx, line, self.script_lines, split_line, self.symbols, pass_no); + if res != True: + if err == True: + err = res # note the error + errors += 1 # and 1 more error + else: + msg = "Invalid command '" + split_line[0] + "' on line " + str(idx) + "." if err == True: - err = res # note the error - errors += 1 # and 1 more error - else: - msg = "Invalid command '" + split_line[0] + "' on line " + str(idx) + "." - if err == True: - err = (msg, line) # note the error - print (msg) - errors += 1 # and 1 more error - - print('Pass ' + str(pass_no) + ' complete. ' + str(errors) + ' errors detected.') - if err != True: - break # errors prevent next pass - - return err # success or failure - - -COLOR_PRIMED = 5 #red -COLOR_FUNC_KEYS_PRIMED = 9 #amber -EXIT_UPDATE_DELAY = 0.1 -DELAY_EXIT_CHECK = 0.025 - - -threads = [[None for y in range(9)] for x in range(9)] -running = False -to_run = [] -text = [["" for y in range(9)] for x in range(9)] + err = (msg, line) # note the error + print (msg) + errors += 1 # and 1 more error + + print('Pass ' + str(pass_no) + ' complete. ' + str(errors) + ' errors detected.') + if err != True: + break # errors prevent next pass + + return err # success or failure + + + def check_kill(self, killfunc=None): + if not self.thread: + print ("expecting a thread in ", self.coords) + return False + + if self.thread.kill.is_set(): + print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") + self.thread.kill.clear() + if not self.is_async: + self.running = False + if killfunc: + killfunc() + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() + return True + else: + return False -def check_kill(x, y, is_async, killfunc=None): - global threads + # a sleep method that works with the multiple threads - coords = "(" + str(x) + ", " + str(y) + ")" - - if threads[x][y].kill.is_set(): - print("[scripts] " + coords + " Recieved exit flag, script exiting...") - threads[x][y].kill.clear() - if not is_async: - running = False - if killfunc: - killfunc() - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() + def safe_sleep(self, time, endfunc=None): + while time > DELAY_EXIT_CHECK: + sleep(DELAY_EXIT_CHECK) + time -= DELAY_EXIT_CHECK + if check_kill(self.x, self.y, self.is_async, endfunc): + return False + if time > 0: + sleep(time) + if endfunc: + endfunc() return True - else: - return False -# a sleep method that works with the multiple threads + # some lines can be ignored. These include blank lines and comments. It's faster to identify them + # before trying to process them than treat them as an exception afterwards. -def safe_sleep(time, x, y, is_async, endfunc=None): - while time > DELAY_EXIT_CHECK: - sleep(DELAY_EXIT_CHECK) - time -= DELAY_EXIT_CHECK - if check_kill(x, y, is_async, endfunc): - return False - if time > 0: - sleep(time) - if endfunc: - endfunc() - return True - - -# some lines can be ignored. These include blank lines and comments. It's faster to identify them -# before trying to process them than treat them as an exception afterwards. + def is_ignorable_line(self, line): + line = line.strip() # remove leading and trailing spaces + if line != "": + if line[0] == "-": + return True # non-blank lines starting with a hyphen are comments (and can be ignored) + else: + return False # other non-blank lines are significant + else: + return True # blank lines can be igmored + + + def schedule_script(self): + global to_run + + if self.thread != None: + if self.thread.is_alive(): + print("[scripts] " + self.coords + " Script already running, killing script....") + self.thread.kill.set() + return + + if (self.x, self.y) in [l[1:] for l in to_run]: + print("[scripts] " + self.coords + " Script already scheduled, unscheduling...") + indexes = [i for i, v in enumerate(to_run) if ((v[1] == self.x) and (v[2] == self.y))] + for index in indexes[::-1]: + temp = to_run.pop(index) + return -def is_ignorable_line(line): - line = line.strip() # remove leading and trailing spaces - if line != "": - if line[0] == "-": - return True # non-blank lines starting with a hyphen are comments (and can be ignored) + if self.is_async: + print("[scripts] " + self.coords + " Starting asynchronous script in background...") + self.thread = threading.Thread(target=run_script, args=()) + self.thread.kill = threading.Event() + self.thread.start() + elif not self.running: + print("[scripts] " + self.coords + " No script running, starting script in background...") + self.thread = threading.Thread(target=self.run_script_and_run_next, args=()) + self.thread.kill = threading.Event() + self.thread.start() else: - return False # other non-blank lines are significant - else: - return True # blank lines can be igmored + print("[scripts] " + self.coords + " A script is already running, scheduling...") + to_run.append((self.x, self.y)) + + lp_colors.updateXY(self.x, self.y) -def schedule_script(script_in, x, y): - global threads - global to_run - global running + def run_next(self): + global to_run + global buttons - coords = "(" + str(x) + ", " + str(y) + ")" + if len(to_run) > 0: + tup = to_run.pop(0) + x = tup[0] + y = tup[1] - if threads[x][y] != None: - if threads[x][y].is_alive(): - print("[scripts] " + coords + " Script already running, killing script....") - threads[x][y].kill.set() - return + btn = buttons[x][y] + btn.schedule_script() - if (x, y) in [l[1:] for l in to_run]: - print("[scripts] " + coords + " Script already scheduled, unscheduling...") - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - - token = script_in.split("\n")[0].split(" ")[0] - if token in HEADERS and HEADERS[token].is_async: - print("[scripts] " + coords + " Starting asynchronous script in background...") - threads[x][y] = threading.Thread(target=run_script, args=(script_in,x,y)) - threads[x][y].kill = threading.Event() - threads[x][y].start() - elif not running: - print("[scripts] " + coords + " No script running, starting script in background...") - threads[x][y] = threading.Thread(target=run_script_and_run_next, args=(script_in,x,y)) - threads[x][y].kill = threading.Event() - threads[x][y].start() - else: - print("[scripts] " + coords + " A script is already running, scheduling...") - to_run.append((script_in, x, y)) - lp_colors.updateXY(x, y) + def run_script_and_run_next(self): + self.run_script() + self.run_next() -def run_next(): - global to_run - if len(to_run) > 0: - tup = to_run.pop(0) - new_script = tup[0] - x = tup[1] - y = tup[2] + # run a script - schedule_script(new_script, x, y) + def run_script(self): + lp_colors.updateXY(self.x, self.y) + + if not self.validate_script(): + return + + print("[scripts] " + self.coords + " Now running script...") + + self.running = not self.is_async + cmd = "RESET_REPEATS" # before we run, we want to rest loop counters + if cmd in VALID_COMMANDS: + command = VALID_COMMANDS[cmd] + command.Run(0, [cmd], self.symbols, (self.coords, self.x, self.y), self.is_async) + + if len(self.script_lines) > 0: + self.running = True -def run_script_and_run_next(script_in, x_in, y_in): - global running - global to_run + def main_logic(idx): + if self.check_kill(): + return idx + 1 + + line = self.script_lines[idx] + if line == "": + return idx + 1 - coords = "(" + str(x_in) + ", " + str(y_in) + ")" + if line[0] == "-": + split_line = ["-", line[1:]] # comments are special -- not tokenised + else: + split_line = line.split(" ") - run_script(script_in, x_in, y_in) - run_next() + if split_line[0] in VALID_COMMANDS: # if first element is a command + command = VALID_COMMANDS[split_line[0]] # get the command + return command.Run(idx, split_line, self.symbols, (self.coords, self.x, self.y), self.is_async) + else: + print("[scripts] " + self.coords + " Invalid command: " + split_line[0] + ", skipping...") + return idx + 1 -# run a script + run = True + idx = 0 + while run: + idx = main_logic(idx) + if (idx < 0) or (idx >= len(self.script_lines)): + run = False + + if not self.is_async: + self.running = False + + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() + + print("[scripts] " + self.coords + " Script done running.") -def run_script(script_str, x, y): - global running - global exit + + # validating a script consists of doing the checks that we do prior to running, but + # we won't run it afterwards. - lp_colors.updateXY(x, y) - coords = "(" + str(x) + ", " + str(y) + ")" - - print("[scripts] " + coords + " Now running script...") + def validate_script(self): + if self.validated or self.script_str == "": # If valid or there is no script... + self.validated = True + return True # ...validation succeeds! - script_lines = script_str.split("\n") - script_lines = [i.strip() for i in script_lines] + self.script_lines = self.script_str.split('\n') # Create the lines + self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks - if len(script_lines) > 0: - is_async = False - token = script_lines[0].split(" ")[0] - if token in VALID_COMMANDS: - command = VALID_COMMANDS[token] - is_async = token in HEADERS and HEADERS[token].is_async - else: - running = True - - symbols = new_symbol_table() + self.symbols = new_symbol_table() # Create a shiny new symbol table + self.is_async = False # default is NOT async - # parse labels (do all parsing required for commands) - parse_script(script_lines, symbols) - + if self.parse_script(): # If parsing is OK + self.validated = True # Script is valid - def main_logic(idx): - nonlocal symbols - - if check_kill(x, y, is_async): - return idx + 1 - - line = script_lines[idx] - if line == "": - return idx + 1 + if len(self.script_lines) > 0: # look for async header and set flag + token = self.script_lines[0].split(" ")[0] + self.is_async = token in HEADERS and HEADERS[token].is_async + else: + self.symbols = None # otherwise destroy symbol table - if line[0] == "-": - split_line = ["-", line[1:]] # comments are special -- not tokenised - else: - split_line = line.split(" ") + return self.validated # and tell us the result + - if split_line[0] in VALID_COMMANDS: # if first element is a command - command = VALID_COMMANDS[split_line[0]] # get the command - return command.Run(idx, split_line, symbols, (coords, x, y), is_async) - else: - print("[scripts] " + coords + " Invalid command: " + split_line[0] + ", skipping...") +buttons = [[Button(x, y, "") for y in range(9)] for x in range(9)] +to_run = [] - return idx + 1 - run = True - idx = 0 - while run: - idx = main_logic(idx) - if (idx < 0) or (idx >= len(script_lines)): - run = False - - if not is_async: - running = False - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (x, y)).start() - - print("[scripts] (" + str(x) + ", " + str(y) + ") Script done running.") - +# bind a button -def bind(x, y, script_down, color): +def bind(x, y, script_str, color): global to_run + global buttons + + btn = Button(x, y, script_str) + buttons[x][y] = btn - if (x, y) in [l[1:] for l in to_run]: - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return + if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + for index in indexes[::-1]: # and for each of them (in reverse order) + temp = to_run.pop(index) # Remove them from the list + return # Why do we return here? - schedule_script_bindable = lambda a, b: schedule_script(script_down, x, y) + schedule_script_bindable = lambda a, b: btn.schedule_script() lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) - text[x][y] = script_down - files.layout_changed_since_load = True + files.layout_changed_since_load = True # Mark the layout as changed + +# unbind a button def unbind(x, y): global to_run + global buttons + + lp_events.unbind(x, y) # Clear any events associated with the button + + btn = Button(x, y, "") # create the new blank button + + if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + for index in indexes[::-1]: # and for each of them (in reverse order) + temp = to_run.pop(index) # Remove them from the list + buttons[x][y] = btn # Clear the button script + return # WHY do we return here? + + if thread[x][y] != None: # If the button is actially executing + thread[x][y].kill.set() # then kill it + + buttons[x][y] = btn # Clear the button script + + files.layout_changed_since_load = True # Mark the layout as changed - lp_events.unbind(x, y) - text[x][y] = "" - if (x, y) in [l[1:] for l in to_run]: - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - if threads[x][y] != None: - threads[x][y].kill.set() - files.layout_changed_since_load = True +# swap details for two buttons def swap(x1, y1, x2, y2): global text - color_1 = lp_colors.curr_colors[x1][y1] - color_2 = lp_colors.curr_colors[x2][y2] + color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 + color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 - script_1 = text[x1][y1] - script_2 = text[x2][y2] + script_1 = buttons[x1, y1].script_str # Script for btn #1 + script_2 = buttons[x2, y2].script_str # Script for btn #2 - unbind(x1, y1) - if script_2 != "": - bind(x1, y1, script_2, color_2) - lp_colors.updateXY(x1, y1) + unbind(x1, y1) # Unbind #1 + if script_2 != "": # If there is a script #2... + bind(x1, y1, script_2, color_2) # ...bind it to #1 + lp_colors.updateXY(x1, y1) # Update the colours for btn #1 - unbind(x2, y2) + unbind(x2, y2) # Do the reverse for #2 if script_1 != "": bind(x2, y2, script_1, color_1) lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True + + files.layout_changed_since_load = True # Flag that the layout has changed + +# Duplicate a button def copy(x1, y1, x2, y2): - global text + global buttons - color_1 = lp_colors.curr_colors[x1][y1] + color_1 = lp_colors.curr_colors[x1][y1] # Get colour of btn to be copied - script_1 = text[x1][y1] + script_1 = buttons[x1, y1].script_str # Get script to be copied - unbind(x2, y2) - if script_1 != "": - bind(x2, y2, script_1, color_1) - lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True + unbind(x2, y2) # Unbind the destination + if script_1 != "": # If we're copying a button with a script... + bind(x2, y2, script_1, color_1) # ...bind the details to the destination + lp_colors.updateXY(x2, y2) # Update the colours + + files.layout_changed_since_load = True # Flag the layout as changed +# move a button + def move(x1, y1, x2, y2): - global text + global buttons - color_1 = lp_colors.curr_colors[x1][y1] + color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour - script_1 = text[x1][y1] + script_1 = buttons[x1, y1].script_str # Get source button script - unbind(x1, y1) + unbind(x1, y1) # Unbind *both* buttons unbind(x2, y2) - if script_1 != "": - bind(x2, y2, script_1, color_1) - lp_colors.updateXY(x2, y2) - files.layout_changed_since_load = True + + if script_1 != "": # If the source had a script... + bind(x2, y2, script_1, color_1) # ...bind it to the destination + lp_colors.updateXY(x2, y2) # Update the destination colours + + files.layout_changed_since_load = True # And flag the layout as changed # determine if a key is bound def is_bound(x, y): - if text[x][y] == "": - return False + global buttons + + if buttons[x][y].script_str == "": # If there is no script... + return False # ...it's not bound else: - return True + return True # Otherwise it is # Unbind all keys. def unbind_all(): - global threads - global text + global buttons global to_run - lp_events.unbind_all() - text = [["" for y in range(9)] for x in range(9)] - to_run = [] - for x in range(9): - for y in range(9): - if threads[x][y] is not None: - if threads[x][y].isAlive(): - threads[x][y].kill.set() - files.curr_layout = None - files.layout_changed_since_load = False + lp_events.unbind_all() # Unbind all events + text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank + to_run = [] # nothing queued to run - -# validating a script consists of doing the checks that we do prior to running, but -# we won't run it afterwards. - -def validate_script(script_str): - if script_str == "": - return True - - script_lines = script_str.split('\n') - script_lines = [i.strip() for i in script_lines] + for x in range(9): # For each column... + for y in range(9): # ...and row + btn = buttons[x][y] + if btn.thread is not None: # If there is a thread... + if btn.thread.isAlive(): # ...and if the thread is alive... + btn.thread.kill.set() # ...kill it + + files.curr_layout = None # There is no current layout + files.layout_changed_since_load = False # So mark it as unchanged - symbols = new_symbol_table() - return parse_script(script_lines, symbols) diff --git a/user_scripts/examples/rpn.lps b/user_scripts/examples/rpn.lps index cee1fa0..0c4115c 100644 --- a/user_scripts/examples/rpn.lps +++ b/user_scripts/examples/rpn.lps @@ -1 +1,2 @@ -RPN_EVAL 1 1 + view 2 * 0.5 / view +- simple math. add 1 and 1, then multiply by 2 and divide by a half. Then show the value +RPN_EVAL clst 1 1 + view 2 * 0.5 / view diff --git a/user_scripts/examples/rpn2.lps b/user_scripts/examples/rpn2.lps index 5592a11..ffed05f 100644 --- a/user_scripts/examples/rpn2.lps +++ b/user_scripts/examples/rpn2.lps @@ -1 +1,2 @@ +- This demonstrates a few other commands RPN_EVAL 3 2 1 view view_s + * view view_s 3.141 sqr view / view_s dup 1 - view_s 1/x x<>y view_s diff --git a/user_scripts/examples/rpn3.lps b/user_scripts/examples/rpn3.lps index cc20748..eb5be64 100644 --- a/user_scripts/examples/rpn3.lps +++ b/user_scripts/examples/rpn3.lps @@ -1,3 +1,4 @@ +- this tests some of the conditional statements RPN_EVAL view_s view_l view_g RPN_EVAL 0 x=0? view RPN_EVAL x!=0? view diff --git a/user_scripts/examples/rpn4.lps b/user_scripts/examples/rpn4.lps new file mode 100644 index 0000000..74a76bb --- /dev/null +++ b/user_scripts/examples/rpn4.lps @@ -0,0 +1,4 @@ +- this demostrates that the stack is retained across executions of the script +- every time it is run, you will see the stack grow +RPN_EVAL dup 1 + +RPN_EVAL view_s diff --git a/user_scripts/examples/rpn5.lps b/user_scripts/examples/rpn5.lps new file mode 100644 index 0000000..0b5c59d --- /dev/null +++ b/user_scripts/examples/rpn5.lps @@ -0,0 +1,14 @@ +- This demonstrates the use of local and global variables +- +- Assign this script to a pair of buttons. Watch how the global variable is shared +- and the local variable isn't +- +- clear the stack +RPN_EVAL clst +- Now get the global variable a and increment it +RPN_EVAL G a +- Now get the local variable b and increment it +RPN_EVAL b +- look at the local and global variables +RPN_EVAL view_l view_g + diff --git a/variables.py b/variables.py index 09de90a..5572731 100644 --- a/variables.py +++ b/variables.py @@ -28,28 +28,28 @@ def top(syms, i): #raise Exception("Stack empty") -def is_defined(name, sym): +def is_defined(name, vbls): # is the variable defined in the symbol library - return sym and name.lower() in sym['vars'] + return vbls and name.lower() in vbls # This returns 0 if the variable is not defined. An alternative is to return an error -def get(name, syms_1, syms_2): +def get(name, l_vbls, g_vbls): # get a variable. look in one symbol table, then the next. # this allows an order to be defined to get local vars then global name = name.lower() - if is_defined(name, syms_1): # First look in the local symbol table (if defined) - return syms_1['vars'][name] - if is_defined(name, syms_2): # then the global one - return syms_2['vars'][name] + if is_defined(name, l_vbls): # First look in the local symbol table (if defined) + return l_vbls[name] + if is_defined(name, g_vbls): # then the global one + return g_vbls[name] return 0 # raise Exception("Variable not found") -def put(name, val, syms): +def put(name, val, vbls): # store a value in a named variable in a specific variable list - syms['vars'][name.lower()] = val + vbls[name.lower()] = val # if you try to grab an argument where no more exists, an error will result diff --git a/window.py b/window.py index 990dea1..a95bd1f 100644 --- a/window.py +++ b/window.py @@ -404,7 +404,8 @@ def validate_func(): text_string = t.get(1.0, tk.END) try: - script_validate = scripts.validate_script(text_string) + btn = scripts.Button(x, y, text_string) + script_validate = btn.validate_script() except: #self.save_script(w, x, y, text_string) # This will fail and throw a popup error self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") @@ -424,7 +425,7 @@ def validate_func(): t.grid(column=0, row=0, rowspan=3, padx=10, pady=10) if text_override == None: - t.insert(tk.INSERT, scripts.text[x][y]) + t.insert(tk.INSERT, scripts.buttons[x][y].script_str) else: t.insert(tk.INSERT, text_override) t.bind("<>", self.custom_paste) @@ -593,7 +594,8 @@ def open_editor_func(): if open_editor: self.script_entry_window(x, y, script_text, color) try: - script_validate = scripts.validate_script(script_text) + btn = scripts.Button(x, y, script_text) + script_validate = btn.validate_script() except: self.popup(window, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = open_editor_func) raise @@ -689,9 +691,9 @@ def run_end(func): def modified_layout_save_prompt(self): if files.layout_changed_since_load == True: layout_empty = True - for x_texts in scripts.text: - for text in x_texts: - if text != "": + for x_btns in scripts.buttons: + for btn in x_btns: + if btn.script_str != "": layout_empty = False break From cf99b9ec7a71162e4f20c1421ab3839ca80149a1 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 6 Sep 2020 17:03:53 +0800 Subject: [PATCH 09/83] RPN_EVAL upgrades as promised * locking implemented for global variables * Better example scripts with comments * implemented last_x * added integer division, modulus, integer, frac, chs, and y^x operators * more internal documentation * handles integer and floating point constants as their own type * fixes a bug handling the constant 0 * KNOWN BUG: Validation flags errors but doesn't stop you from running a script! --- README.md | 10 +- commands_rpncalc.py | 355 ++++++++++++++++++++++++--------- scripts.py | 9 +- user_scripts/examples/rpn2.lps | 14 +- user_scripts/examples/rpn3.lps | 209 ++++++++++++++++--- variables.py | 4 +- 6 files changed, 473 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index d259a09..d46d0e3 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil #### Variables and calculator [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) * `RPN_EVAL` - * An RPN (stack-based) calculator that implements local and global variables (global variables are not global yet). + * An RPN (stack-based) calculator that implements local and global variables. * Any number of commands may follow from 1 to infinity? * Commands and variables are NOT case sensitive * Any numeric value is pushed onto the stack @@ -298,9 +298,15 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * - - replaces the top two values on the stack with their difference * * - replaces the top two values on the stack with their product * / - replaces the top two values on the stack with their quotient + * // - performs integer division + * mod - calculates the modulus (remainder) + * y^x - raises the second value on the stack to the power of the first value on the stack * Some operations only change the top value on the stack. * 1/x - replaces the top value on the stack with it's inverse * sqr - replaces the value on the top of the stack with its square + * int - replaces the value on trhe top of the stack with the integer part + * frac - replaces the value on trhe top of the stack with the fractional part + * chs - changes the sign of the value on the top of the stack (does not affect lastx) * Some operations manipulate the stack * dup - duplicates the value on the top of the stack * pop - removes the top item from the stack @@ -336,7 +342,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * ?G {x} - Does a global variable {x} exist * !?G {x} - Does a global variable {x} not exist * The stack is local to the current script, however it is maintained between executions! - * The global variables global to all scripts. There is currently no synchronisation of access to the global variables. This may cause problems, but that will be adressed in the next version (I hope) + * The global variables are global to all scripts. * Local variables are local to the current script (and are maintained across executions) * The stack and local variables will be lost if the script is edited. diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 4e3df61..74b0f9d 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -24,6 +24,7 @@ def add(self, a = variables.pop(symbols) # add requires 2 params, pop them off the stack... b = variables.pop(symbols) # + symbols['l_vars']['last x'] = a try: c = b+a # RPN functions are defined as b (operator) a @@ -39,6 +40,7 @@ def subtract(self, symbols, cmd, cmds): ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) + symbols['l_vars']['last x'] = a try: c = b-a @@ -54,6 +56,7 @@ def multiply(self, symbols, cmd, cmds): ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) + symbols['l_vars']['last x'] = a try: c = b*a @@ -69,6 +72,7 @@ def divide(self, symbols, cmd, cmds): ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) + symbols['l_vars']['last x'] = a try: c = b/a @@ -80,10 +84,42 @@ def divide(self, symbols, cmd, cmds): return ret + def i_div(self, symbols, cmd, cmds): + ret = 1 + a = variables.pop(symbols) + b = variables.pop(symbols) + symbols['l_vars']['last x'] = a + + try: + c = b//a + except: + raise Exception("Error in division: " + str(b) + " // " + str(a)) # Errors are highly possible here + + variables.push(symbols, c) + + return ret + + + def mod(self, symbols, cmd, cmds): + ret = 1 + a = variables.pop(symbols) + b = variables.pop(symbols) + symbols['l_vars']['last x'] = a + + try: + c = b%a + except: + raise Exception("Error in mod: " + str(b) + " % " + str(a)) # Errors are highly possible here + + variables.push(symbols, c) + + return ret + + def view(self, symbols, cmd, cmds): # view the top of the stack (typically where results are) ret = 1 - print('Top of stack = ', variables.top(symbols, 1)) + print('Top of stack = ', variables.top(symbols, 1)) # we're going to peek at the top of the stack without popping return ret @@ -91,7 +127,7 @@ def view(self, symbols, cmd, cmds): def view_s(self, symbols, cmd, cmds): # View the entire stack. Probably a debugging tool. ret = 1 - print('Stack = ', symbols['stack']) + print('Stack = ', symbols['stack']) # show the entire stack return ret @@ -99,7 +135,7 @@ def view_s(self, symbols, cmd, cmds): def view_l(self, symbols, cmd, cmds): # View the local variables. Probably a debugging tool. ret = 1 - print('Local = ', symbols['l_vars']) + print('Local = ', symbols['l_vars']) # show all local variables return ret @@ -107,14 +143,16 @@ def view_l(self, symbols, cmd, cmds): def view_g(self, symbols, cmd, cmds): # View the global variables. Probably a debugging tool. ret = 1 - print('Global = ', symbols['g_vars']) + with symbols['g_vars'][0]: # lock the globals while we do this + print('Global = ', symbols['g_vars'][1]) return ret def one_on_x(self, symbols, cmd, cmds): ret = 1 - a = variables.pop(g_symbols) + a = variables.pop(symbols) + symbols['l_vars']['last x'] = a try: variables.push(symbols, 1/a) @@ -124,13 +162,54 @@ def one_on_x(self, symbols, cmd, cmds): return ret + def int_x(self, symbols, cmd, cmds): + # get the integer part of x + ret = 1 + a = variables.pop(symbols) + symbols['l_vars']['last x'] = a + + try: + variables.push(symbols, int(a)) + except: + raise Exception("Error in '" + cmd + "' " + str(a)) # Errors are highly unlikely here + + return ret + + + def frac_x(self, symbols, cmd, cmds): + # get the fractionasl part of x + ret = 1 + a = variables.pop(symbols) + symbols['l_vars']['last x'] = a + + try: + variables.push(symbols, a - int(a)) + except: + raise Exception("Error in '" + cmd + "' " + str(a)) # Errors are highly unlikely here + + return ret + + + def chs(self, symbols, cmd, cmds): + ret = 1 + a = variables.pop(symbols) + + try: + variables.push(symbols, -a) + except: + raise Exception("Error in chs: " + str(a)) # Errors are highly improbable here + + return ret + + def sqr(self, symbols, cmd, cmds): # calculates the square ret = 1 a = variables.pop(symbols) + symbols['l_vars']['last x'] = a try: - c = a*a + c = a**2 except: raise Exception("Error in squaring: " + str(a)) @@ -139,6 +218,23 @@ def sqr(self, symbols, cmd, cmds): return ret + def y_to_x(self, symbols, cmd, cmds): + # calculates the square + ret = 1 + a = variables.pop(symbols) + b = variables.pop(symbols) + symbols['l_vars']['last x'] = a + + try: + c = b**a + except: + raise Exception("Error raising: " + str(b) + " to the " + str(a) + "th power") # Errors are highly possible here + + variables.push(symbols, c) + + return ret + + def dup(self, symbols, cmd, cmds): # duplicates the value on the top of the stack ret = 1 @@ -163,6 +259,19 @@ def clst(self, symbols, cmd, cmds): return ret + def last_x(self, symbols, cmd, cmds): + # resurrects the last value of x that was "consumed" by an operation + ret = 1 + try: + a = symbols['l_vars']['last x'] # attempt to get the last-x value + except: + a = 0 # default is zero + + variables.push(symbols, a) # and push it onto the stack + + return ret + + def cl_l(self, symbols, cmd, cmds): # clears the stack ret = 1 @@ -195,15 +304,16 @@ def swap_x_y(self, symbols, cmd, cmds): def sto(self, symbols, cmd, cmds): # stores the value in local var if it exists, otherwise global var. If neither, creates local ret = 1 - ret, v = variables.next_cmd(ret, cmds) - a = variables.top(symbols, 1) - - if variables.is_defined(v, symbols['l_vars']): - variables.put(v, a, symbols['l_vars']) - elif variables.is_defined(v, symbols['g_vars']): - variables.put(v, a, symbols['g_vars']) - else: - variables.put(v, a, symbols['l_vars']) + ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? + a = variables.top(symbols, 1) # will be stored from the top of the stack + + with symbols['g_vars'][0]: # lock the globals while we do this + if variables.is_defined(v, symbols['l_vars']): # Is it local... + variables.put(v, a, symbols['l_vars']) # ...then store it locally + elif variables.is_defined(v, symbols['g_vars'][1]): # Is it global... + variables.put(v, a, symbols['g_vars'][1]) # ...store it globally + else: + variables.put(v, a, symbols['l_vars']) # default is to create new in locals return ret @@ -211,9 +321,10 @@ def sto(self, symbols, cmd, cmds): def sto_g(self, symbols, cmd, cmds): # stores the value on the top of the stack into the global variable named by the next token ret = 1 - ret, v = variables.next_cmd(ret, cmds) - a = variables.top(symbols, 1) - variables.put(v, a, symbols['g_vars']) + ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? + a = variables.top(symbols, 1) # will be stored from the top of the stack + with symbols['g_vars'][0]: # lock the globals + variables.put(v, a, symbols['g_vars'][1]) # and store it there return ret @@ -232,7 +343,8 @@ def rcl(self, symbols, cmd, cmds): # recalls a variable. Try local first, then global ret = 1 ret, v = variables.next_cmd(ret, cmds) - a = variables.get(v, symbols['l_vars'], symbols['g_vars']) + with symbols['g_vars'][0]: # lock the globals while we do this + a = variables.get(v, symbols['l_vars'], symbols['g_vars'][1]) variables.push(symbols, a) return ret @@ -252,12 +364,14 @@ def rcl_g(self, symbols, cmd, cmds): # recalls a global variable (useful if you define an identical local var) ret = 1 ret, v = variables.next_cmd(ret, cmds) - a = variables.get(v, None, symbols['g_vars']) - variables.push(symbols, a) + with symbols['g_vars'][0]: # lock the globals while we do this + a = variables.get(v, None, symbols['g_vars'][1]) # grab the value from the global vars + variables.push(symbols, a) # and push onto the stack return ret def x_eq_zero(self, symbols, cmd, cmds): + # only continues eval if the top of the stack is 0 if variables.top(symbols, 1) == 0: return 1 else: @@ -265,6 +379,7 @@ def x_eq_zero(self, symbols, cmd, cmds): def x_ne_zero(self, symbols, cmd, cmds): + # only continues eval if the top of the stack is not 0 if variables.top(symbols, 1) != 0: return 1 else: @@ -272,6 +387,7 @@ def x_ne_zero(self, symbols, cmd, cmds): def x_eq_y(self, symbols, cmd, cmds): + # only continues eval if the two top values are equal if variables.top(symbols, 1) == variables.top(symbols, 2): return 1 else: @@ -279,6 +395,7 @@ def x_eq_y(self, symbols, cmd, cmds): def x_ne_y(self, symbols, cmd, cmds): + # only continues eval if the two top values are not equal if variables.top(symbols, 1) != variables.top(symbols, 2): return 1 else: @@ -286,6 +403,7 @@ def x_ne_y(self, symbols, cmd, cmds): def x_gt_y(self, symbols, cmd, cmds): + # only continue if the top value > the second value on the stack if variables.top(symbols, 1) > variables.top(symbols, 2): return 1 else: @@ -293,6 +411,7 @@ def x_gt_y(self, symbols, cmd, cmds): def x_ge_y(self, symbols, cmd, cmds): + # only continue if the top value >= the second value on the stack if variables.top(symbols, 1) >= variables.top(symbols, 2): return 1 else: @@ -300,6 +419,7 @@ def x_ge_y(self, symbols, cmd, cmds): def x_lt_y(self, symbols, cmd, cmds): + # only continue if the top value < the second value on the stack if variables.top(symbols, 1) < variables.top(symbols, 2): return 1 else: @@ -307,6 +427,7 @@ def x_lt_y(self, symbols, cmd, cmds): def x_le_y(self, symbols, cmd, cmds): + # only continue if the top value <= the second value on the stack if variables.top(symbols, 1) <= variables.top(symbols, 2): return 1 else: @@ -314,24 +435,29 @@ def x_le_y(self, symbols, cmd, cmds): def is_def(self, symbols, cmd, cmds): + # only continue if the variable is defined (locally or globally is OK) ret = 1 ret, v = variables.next_cmd(ret, cmds) - if variables.is_defined(v, symbols['g_vars']) or variables.is_defined(v, symbols['l_vars']): - return ret - else: - return len(cmds)+1 + with symbols['g_vars'][0]: # lock the globals while we do this + if variables.is_defined(v, symbols['g_vars'][1]) or variables.is_defined(v, symbols['l_vars']): + return ret + else: + return len(cmds)+1 def is_not_def(self, symbols, cmd, cmds): + # only continue if the variable is not defined (either locally or globally) ret = 1 ret, v = variables.next_cmd(ret, cmds) - if not (variables.is_defined(v, symbols['g_vars']) or variables.is_defined(v, symbols['l_vars'])): - return ret - else: - return len(cmds)+1 + with symbols['g_vars'][0]: # lock the globals while we do this + if not (variables.is_defined(v, symbols['g_vars'][1]) or variables.is_defined(v, symbols['l_vars'])): + return ret + else: + return len(cmds)+1 def is_local_def(self, symbols, cmd, cmds): + # only continue if the variable is defined locally ret = 1 ret, v = variables.next_cmd(ret, cmds) if variables.is_defined(v, symbols['l_vars']): @@ -341,6 +467,7 @@ def is_local_def(self, symbols, cmd, cmds): def is_local_not_def(self, symbols, cmd, cmds): + # only continue if the variable is not defined locally ret = 1 ret, v = variables.next_cmd(ret, cmds) if not variables.is_defined(v, symbols['l_vars']): @@ -350,21 +477,25 @@ def is_local_not_def(self, symbols, cmd, cmds): def is_global_def(self, symbols, cmd, cmds): + # only continue if the variable is defined globally ret = 1 ret, v = variables.next_cmd(ret, cmds) - if variables.is_defined(v, symbols['g_vars']): - return ret - else: - return len(cmds)+1 + with symbols['g_vars'][0]: # lock the globals while we do this + if variables.is_defined(v, symbols['g_vars'][1]): + return ret + else: + return len(cmds)+1 def is_global_not_def(self, symbols, cmd, cmds): + # only continue if the variable is not defined globally ret = 1 ret, v = variables.next_cmd(ret, cmds) - if not variables.is_defined(v, symbols['g_vars']): - return ret - else: - return len(cmds)+1 + with symbols['g_vars'][0]: # lock the globals while we do this + if not variables.is_defined(v, symbols['g_vars'][1]): + return ret + else: + return len(cmds)+1 def __init__( @@ -374,42 +505,49 @@ def __init__( super().__init__("RPN_EVAL") # the name of the command as you have to enter it in the code self.operators = dict() - self.operators["+"] = self.add # + - self.operators["-"] = self.subtract # - - self.operators["*"] = self.multiply # * - self.operators["/"] = self.divide # / - self.operators["VIEW"] = self.view # View X - self.operators["VIEW_S"] = self.view_s # View stack - self.operators["VIEW_L"] = self.view_l # View local vars - self.operators["VIEW_G"] = self.view_g # View global vars - self.operators["1/X"] = self.one_on_x # 1/x - self.operators["SQR"] = self.sqr # ** - self.operators["DUP"] = self.dup # Duplicate top of stack - self.operators["POP"] = self.pop # remove item from top of stack - self.operators["CLST"] = self.clst # clear stack - self.operators["CL_L"] = self.cl_l # clear local variables - self.operators["STACK"] = self.stack_len # length of stack - self.operators["X<>Y"] = self.swap_x_y # swap x and y - self.operators[">"] = self.sto # store - self.operators[">L"] = self.sto_l # store local - self.operators[">G"] = self.sto_g # store global - self.operators["<"] = self.rcl # recall - self.operators["Y?"] = self.x_gt_y # is x > y? - self.operators["X>=Y?"] = self.x_ge_y # is x >= y? - self.operators["XY"] = (self.swap_x_y, 0) # swap x and y + self.operators[">"] = (self.sto, 1) # store + self.operators[">L"] = (self.sto_l, 1) # store local + self.operators[">G"] = (self.sto_g, 1) # store global + self.operators["<"] = (self.rcl, 1) # recall + self.operators["Y?"] = (self.x_gt_y, 0) # is x > y? + self.operators["X>=Y?"] = (self.x_ge_y, 0) # is x >= y? + self.operators["X c_len: + return ("Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + else: # if invalid, report it + return ("Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + + return True + def Run( self, idx: int, # The current line number @@ -441,31 +601,36 @@ def Run( print("[" + lib + "] " + coords[0] + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" - i = 1 # using a loop counter rath erthan an itterator because it's hard to pass iters as params + i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params + while i < len(split_line): # for each item of the line of tokens cmd = split_line[i] # get the current one + n = None # what we get if it's not a number try: - n = float(cmd) # numbers get... + n = int(cmd) # is it an integer? except ValueError: - pass - else: + try: + n = float(cmd) # how about a float? + except ValueError: + pass + + if n != None: # if it was one of the above symbols['stack'].append(n) # ...put on the stack i += 1 # move along to the next token - continue - - opr = cmd.upper() # Convert to uppercase for searching - if opr in self.operators: # if it's valid - try: - i = i + self.operators[opr](symbols, opr, split_line[i:]) # run it - except: - print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " '" + cmd + "'") - break - else: # if invalid, report it - print("invalid operator #" + str(i) + " '" + cmd + "'") - break - - return idx+1 # Normal default exit to the next line + else: + opr = cmd.upper() # Convert to uppercase for searching + if opr in self.operators: # if it's valid + try: + i = i + self.operators[opr][0](symbols, opr, split_line[i:]) # run it + except: + print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " '" + cmd + "'") + break + else: # if invalid, report it + print("invalid operator #" + str(i) + " '" + cmd + "'") + break + + return idx+1 # Normal default exit to the next line scripts.add_command(RpnCalc_Rpn_Eval()) # register the command diff --git a/scripts.py b/scripts.py index 13ed53f..6ec982c 100644 --- a/scripts.py +++ b/scripts.py @@ -17,7 +17,8 @@ # GLOBALS is likewise empty until global variables get created -GLOBALS = dict() +GLOBALS = dict() # the globals themselvs +GLOBAL_LOCK = threading.Lock() # a lock got the globals to prevent simultaneous access COLOR_PRIMED = 5 #red @@ -61,9 +62,9 @@ def new_symbol_table(): "original": dict(), "labels": dict(), "m_pos": tuple(), - "g_vars": GLOBALS, - "l_vars": dict(), # global (to the script) variables - "stack": [] } # script stack (for RPN_EVAL) + "g_vars": [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) + "l_vars": dict(), # local (to the script) variables (with no lock) + "stack": [] } # script stack (for RPN_EVAL) return symbols diff --git a/user_scripts/examples/rpn2.lps b/user_scripts/examples/rpn2.lps index ffed05f..a1863fa 100644 --- a/user_scripts/examples/rpn2.lps +++ b/user_scripts/examples/rpn2.lps @@ -1,2 +1,12 @@ -- This demonstrates a few other commands -RPN_EVAL 3 2 1 view view_s + * view view_s 3.141 sqr view / view_s dup 1 - view_s 1/x x<>y view_s +- A little more complex example with some stack manipulation +- and the use of the lastx command to recover the last value of +- x that has been consumed +- +- A simple demonstrration of lastx +RPN_EVAL 1 2 / view_s lastx view_s clst +- +- Using dup and lastx to separate the integer and fractional part of a number +RPN_EVAL 11 4 / dup int lastx frac view_s clst +- +- I haven't checked on my HP calculators. Does it handle -ve values correctly? +RPN_EVAL 11 4 chs / dup int lastx frac view_s clst diff --git a/user_scripts/examples/rpn3.lps b/user_scripts/examples/rpn3.lps index eb5be64..fef850f 100644 --- a/user_scripts/examples/rpn3.lps +++ b/user_scripts/examples/rpn3.lps @@ -1,24 +1,185 @@ -- this tests some of the conditional statements -RPN_EVAL view_s view_l view_g -RPN_EVAL 0 x=0? view -RPN_EVAL x!=0? view -RPN_EVAL 1 x=y? view -RPN_EVAL x!=y? view -RPN_EVAL ? x view -RPN_EVAL !? x view -RPN_EVAL 1 > x 1 + >G Z < x view_s view_l view_g -RPN_EVAL >L x ? x view -RPN_EVAL >L x !? x view -RPN_EVAL >L x ?L x view -RPN_EVAL >L x !?L x view -RPN_EVAL >L x ?G x view -RPN_EVAL >L x !?G x view -RPN_EVAL ? z view -RPN_EVAL !? z view -RPN_EVAL ?L z view -RPN_EVAL !?L z view -RPN_EVAL ?G z view -RPN_EVAL !?G z view -RPN_EVAL 2 >G y view_s < y Y view_s -RPN_EVAL view_g +- this tests a heap of RPN_EVAL if you get a divide by 0 error, the command has failed, x display or silence is sucess +- stack displays only used for initial tests + +- divide by 0 (exception - div by 0 error is expected) +RPN_EVAL 1 0 / + +- check x=y? (also checks zeros being pulled off the stack) +RPN_EVAL clst x=y? 1 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 0 x=y? 2 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 0 0 x=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 1 1 x=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 1 x=y? view_s 1 0 / +RPN_EVAL clst 1 2 x=y? view_s 1 0 / +RPN_EVAL clst 2 1 x=y? view_s 1 0 / + +- check x=y? +RPN_EVAL clst 1 1 x!=y? view_s 1 0 / +RPN_EVAL clst 1 4 x!=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 2 1 x!=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / + +- check clst +RPN_EVAL 1 2 3 4 clst x!=y? view_s 1 0 / + +- check stack +RPN_EVAL clst stack 0 x!=y? view_s 1 0 / +RPN_EVAL clst 1 2 3 4 stack x!=y? view_s 1 0 / + +- check + +RPN_EVAL clst 0 0 + 0 x!=y? view_s 1 0 / +RPN_EVAL clst -1 1 + 0 x!=y? view_s 1 0 / +RPN_EVAL clst 1 1 + 2 x!=y? view_s 1 0 / +RPN_EVAL clst 1.75 -3 + -1.25 x!=y? view_s 1 0 / + +- check - +RPN_EVAL clst 0 0 - 0 x!=y? view_s 1 0 / +RPN_EVAL clst -1 1 - -2 x!=y? view_s 1 0 / +RPN_EVAL clst 3 1 - 2 x!=y? view_s 1 0 / +RPN_EVAL clst 1.75 3 - -1.25 x!=y? view_s 1 0 / + +- check * +RPN_EVAL clst 99 0 * 0 x!=y? view_s 1 0 / +RPN_EVAL clst -1 1 * -1 x!=y? view_s 1 0 / +RPN_EVAL clst 5 0.5 * 2.5 x!=y? view_s 1 0 / + +- check / (not checking for divide by zero) +RPN_EVAL clst 99 0 * 0 x!=y? view_s 1 0 / +RPN_EVAL clst -1 1 * -1 x!=y? view_s 1 0 / +RPN_EVAL clst 5 0.5 * 2.5 x!=y? view_s 1 0 / + +- check // +RPN_EVAL clst 10 1 // 10 x!=y? view_s 1 0 / +RPN_EVAL clst 10.25 3 // 3 x!=y? view_s 1 0 / +RPN_EVAL clst -10 3 // -4 x!=y? view_s 1 0 / +RPN_EVAL clst 10 -3 // -4 x!=y? view_s 1 0 / + +- check mod +RPN_EVAL clst 10 1 mod 0 x!=y? view_s 1 0 / +RPN_EVAL clst 10.25 3 mod 1.25 x!=y? view_s 1 0 / +RPN_EVAL clst -10 3 mod 2 x!=y? view_s 1 0 / +RPN_EVAL clst 10 -3 mod -2 x!=y? view_s 1 0 / + +- check 1/x +RPN_EVAL clst 1 1/x 1 x!=y? view_s 1 0 / +RPN_EVAL clst 10 1/x 0.1 x!=y? view_s 1 0 / +RPN_EVAL clst -5 1/x -0.2 x!=y? view_s 1 0 / + +- check int +RPN_EVAL clst 1 int 1 x!=y? view_s 1 0 / +RPN_EVAL clst -1 int -1 x!=y? view_s 1 0 / +RPN_EVAL clst 1.5 int 1 x!=y? view_s 1 0 / +RPN_EVAL clst -1.5 int -1 x!=y? view_s 1 0 / + +- check frac +RPN_EVAL clst 1 frac 0 x!=y? view_s 1 0 / +RPN_EVAL clst -1 frac 0 x!=y? view_s 1 0 / +RPN_EVAL clst 1.5 frac 0.5 x!=y? view_s 1 0 / +RPN_EVAL clst -1.5 frac -0.5 x!=y? view_s 1 0 / + +- check chs +RPN_EVAL clst 1 chs -1 x!=y? view_s 1 0 / +RPN_EVAL clst -1 chs 1 x!=y? view_s 1 0 / +RPN_EVAL clst 0 chs 0 x!=y? view_s 1 0 / + +- check sqr +RPN_EVAL clst 0 sqr 0 x!=y? view_s 1 0 / +RPN_EVAL clst 1 sqr 1 x!=y? view_s 1 0 / +RPN_EVAL clst -1 sqr 1 x!=y? view_s 1 0 / +RPN_EVAL clst 2.25 sqr 2.25 2.25 * x!=y? view_s 1 0 / + +- check y^x +RPN_EVAL clst 0 0 y^x 1 x!=y? view_s 1 0 / +RPN_EVAL clst 1 1 y^x 1 x!=y? view_s 1 0 / +RPN_EVAL clst 1 -1 y^x 1 x!=y? view_s 1 0 / +RPN_EVAL clst 2 3 y^x 8 x!=y? view_s 1 0 / +RPN_EVAL clst 2 -3 y^x 0.125 x!=y? view_s 1 0 / +RPN_EVAL clst -2 3 y^x -8 x!=y? view_s 1 0 / + +- check dup +RPN_EVAL clst 1 dup x!=y? view_s 1 0 / + +- check pop +RPN_EVAL clst pop x!=y? view_s 1 0 / +RPN_EVAL clst 2 1 1 pop x=y? view_s 1 0 / + +- check lastx +RPN_EVAL clst 1 2 + lastx 2 x!=y? view_s 1 0 / +RPN_EVAL clst 1 3 - lastx 3 x!=y? view_s 1 0 / +RPN_EVAL clst 2 4 * lastx 4 x!=y? view_s 1 0 / +RPN_EVAL clst 3 6 / lastx 6 x!=y? view_s 1 0 / +RPN_EVAL clst 7 sqr lastx 7 x!=y? view_s 1 0 / +RPN_EVAL clst 7 sqr lastx 7 x!=y? view_s 1 0 / + +- X<>Y + +RPN_EVAL clst 1 2 x<>y 2 x=y? view_s 1 0 / +RPN_EVAL clst 1 2 x<>y 1 x!=y? view_s 1 0 / + +- sto and rcl local, and exists +RPN_EVAL cl_l ?L a view_l 1 0 / +RPN_EVAL clst 1 L a clst G b clst lx clst gx clst L gx 8 >G gx clst < gx 1 x!=y? view_s view_l view_g 1 0 / +RPN_EVAL clst 4 > gx clst < gx 4 x!=y? view_s view_l view_g 1 0 / +RPN_EVAL clst y? +RPN_EVAL clst 1 1 x>y? view_s 1 0 / +RPN_EVAL clst 1 3 x>y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 2 1 x>y? view_s 1 0 / + +- x>=y? +RPN_EVAL clst 1 1 x>=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 1 3 x>=y? 3 +RPN_EVAL stack x!=y? view_s 1 0 / +RPN_EVAL clst 2 1 x>=y? view_s 1 0 / + +- x Date: Sun, 6 Sep 2020 17:46:51 +0800 Subject: [PATCH 10/83] Bug preventing scripts from being validated fixed. * So, I called validate (that returns a boolean) instead of parse (that returns the error messages). And my Parse function also didn't do the setup required to operate correctly. A little shuffle shuffle and it's all fixed. * Also, tidied up a script a little (no functional changes). --- scripts.py | 18 +++++++++++------- user_scripts/examples/rpn.lps | 7 ++++++- window.py | 7 ++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scripts.py b/scripts.py index 6ec982c..c2b8058 100644 --- a/scripts.py +++ b/scripts.py @@ -98,6 +98,16 @@ def __init__( # Do what is required to parse the script. Parsing does not output any information unless it is an error def parse_script(self): + if self.validated: # we don't want to repeat validation over and over + return True + + if self.script_lines == None: # A little setup if the script lines are not created + self.script_lines = self.script_str.split('\n') # Create the lines + self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks + + self.symbols = new_symbol_table() # Create a shiny new symbol table + self.is_async = False # default is NOT async + err = True errors = 0 # no errors found @@ -230,7 +240,7 @@ def run_script_and_run_next(self): def run_script(self): lp_colors.updateXY(self.x, self.y) - if not self.validate_script(): + if self.validate_script() != True: return print("[scripts] " + self.coords + " Now running script...") @@ -289,12 +299,6 @@ def validate_script(self): self.validated = True return True # ...validation succeeds! - self.script_lines = self.script_str.split('\n') # Create the lines - self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks - - self.symbols = new_symbol_table() # Create a shiny new symbol table - self.is_async = False # default is NOT async - if self.parse_script(): # If parsing is OK self.validated = True # Script is valid diff --git a/user_scripts/examples/rpn.lps b/user_scripts/examples/rpn.lps index 0c4115c..2f1c52a 100644 --- a/user_scripts/examples/rpn.lps +++ b/user_scripts/examples/rpn.lps @@ -1,2 +1,7 @@ -- simple math. add 1 and 1, then multiply by 2 and divide by a half. Then show the value +- Simple math. + +- Add 1 and 1. +- Then multiply by 2 and divide by a half. +- Finally, show the value RPN_EVAL clst 1 1 + view 2 * 0.5 / view + diff --git a/window.py b/window.py index a95bd1f..666439f 100644 --- a/window.py +++ b/window.py @@ -405,7 +405,7 @@ def validate_func(): text_string = t.get(1.0, tk.END) try: btn = scripts.Button(x, y, text_string) - script_validate = btn.validate_script() + script_validate = btn.parse_script() except: #self.save_script(w, x, y, text_string) # This will fail and throw a popup error self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") @@ -592,10 +592,11 @@ def save_script(self, window, x, y, script_text, open_editor = False, color=None def open_editor_func(): nonlocal x, y if open_editor: - self.script_entry_window(x, y, script_text, color) + self.script_entry_window(x, y, script_text, color) + try: btn = scripts.Button(x, y, script_text) - script_validate = btn.validate_script() + script_validate = btn.parse_script() except: self.popup(window, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = open_editor_func) raise From d44343958e96dd29898cce040da1b3e1b3a18725 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 6 Sep 2020 18:36:49 +0800 Subject: [PATCH 11/83] Added line numbers to reporting of goings-on within LPHK * long scripts can be hard to debug. Now the line number of the script is printed along with any status or error message. --- commands_control.py | 130 +++++++++++++++++++++---------------------- commands_external.py | 38 ++++++------- commands_header.py | 26 ++++----- commands_keys.py | 40 ++++++------- commands_mouse.py | 108 +++++++++++++++++------------------ commands_pause.py | 8 +-- commands_rpncalc.py | 12 ++-- scripts.py | 4 +- 8 files changed, 183 insertions(+), 183 deletions(-) diff --git a/commands_control.py b/commands_control.py index 8de7183..a50e344 100644 --- a/commands_control.py +++ b/commands_control.py @@ -37,7 +37,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " comment: " + split_line[1:]) # coords[0] is the text "(x, y)" + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " comment: " + split_line[1:]) # coords[0] is the text "(x, y)" return idx+1 # Return the number of the next line to execute, -1 to exit @@ -70,7 +70,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions # check number of split_line if len(split_line) != 2: - return ("Wrong number of parameters in " + self.name, line) + return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.name, line) # check for duplicate label if split_line[1] in symbols["labels"]: # Does the label already exist (that's bad)? @@ -90,7 +90,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Label: " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Label: " + split_line[1]) return idx+1 # Nothing to do when executing a label @@ -123,11 +123,11 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions # check number of split_line if len(split_line) != 2: - return ("Wrong number of parameters in " + self.ame, line) + return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.ame, line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -144,7 +144,7 @@ def Run( # check for label if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: return symbols["labels"][split_line[1]] # normally we return the line number the label is on @@ -179,11 +179,11 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes exactly 1 argument.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -196,16 +196,16 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is pressed goto LABEL " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed goto LABEL " + split_line[1]) if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: if lp_events.pressed[coords[1]][coords[2]]: # coords[1] is x, and coords[2] is y if split_line[1] in symbols["labels"]: # The label should always exist return symbols["labels"][split_line[1]] # and we return the line number the label is on else: - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 return idx+1 @@ -238,11 +238,11 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes exactly 1 argument.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -255,10 +255,10 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed goto LABEL " + split_line[1]) if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed @@ -294,21 +294,21 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + " requires a minimum of 1 repeat.", line) else: symbols["repeats"][idx] = int(split_line[2]) symbols["original"][idx] = int(split_line[2]) except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -321,7 +321,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ split_line[2] + " times max") if not split_line[1] in symbols["labels"]: # The label should always exist @@ -329,11 +329,11 @@ def Run( return -1 else: if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") return idx+1 @@ -367,24 +367,24 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) else: symbols["repeats"][idx] = int(split_line[2])-1 symbols["original"][idx] = int(split_line[2])-1 except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) if symbols["labels"][split_line[1]] > idx: - return ("Target for " + self.name + " must preceed the command.", line) + return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) return True @@ -397,15 +397,15 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Repeat LABEL " + split_line[1] + " " + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ split_line[2] + " times max") if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") symbols["repeats"][idx] = symbols["original"][idx] # makes this behave like a normal loop return idx+1 @@ -437,21 +437,21 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + " requires a minimum of 1 repeat.", line) else: symbols["repeats"][idx] = int(split_line[2]) symbols["original"][idx] = int(split_line[2]) except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -464,19 +464,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: if lp_events.pressed[coords[1]][coords[2]]: if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") return idx+1 @@ -510,24 +510,24 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) else: symbols["repeats"][idx] = int(split_line[2])-1 symbols["original"][idx] = int(split_line[2])-1 except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) if symbols["labels"][split_line[1]] > idx: - return ("Target for " + self.name + " must preceed the command.", line) + return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) return True @@ -541,19 +541,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: if lp_events.pressed[coords[1]][coords[2]]: if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") symbols["repeats"][idx] = symbols["original"][idx] # for a normal repeat statement return idx+1 @@ -586,20 +586,20 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) symbols["repeats"][idx] = int(split_line[2]) symbols["original"][idx] = int(split_line[2]) except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -612,19 +612,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + print(" Line:" + str(idx+1) + " Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: if not lp_events.pressed[coords[1]][coords[2]]: if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") return idx+1 @@ -658,23 +658,23 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) != 3: - return ("'" + split_line[0] + "' needs both a label name and how many times to repeat.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) try: temp = int(split_line[2]) if temp < 1: - return (split_line[0] + " requires a minimum of 1 repeat.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) symbols["repeats"][idx] = int(split_line[2])-1 symbols["original"][idx] = int(split_line[2])-1 except: - return (split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) if symbols["labels"][split_line[1]] > idx: - return ("Target for " + self.name + " must preceed the command.", line) + return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) return True @@ -688,7 +688,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") if not split_line[1] in symbols["labels"]: # The label should always exist print("missing LABEL '" + split_line[1] + "'") # otherwise an error @@ -696,11 +696,11 @@ def Run( else: if not lp_events.pressed[coords[1]][coords[2]]: if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " " + str(symbols["repeats"][idx]) + " repeats left.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") symbols["repeats"][idx] -= 1 return symbols["labels"][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " No repeats left, not repeating.") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") symbols["repeats"][idx] = symbols["original"][idx] # to behave more normal return idx+1 @@ -732,7 +732,7 @@ def Validate( ): if len(split_line) > 1: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -745,7 +745,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Reset all repeats") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Reset all repeats") for i in symbols["repeats"]: symbols["repeats"][i] = symbols["original"][i] diff --git a/commands_external.py b/commands_external.py index 855cd2e..c3ff11c 100644 --- a/commands_external.py +++ b/commands_external.py @@ -26,10 +26,10 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("TLine:" + str(idx+1) + " - oo many arguments for command '" + split_line[0] + "'.", line) return True @@ -46,7 +46,7 @@ def Run( if "http" not in link: link = "http://" + link - print("[" + lib + "] " + coords[0] + " Open website " + link + " in default browser") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open website " + link + " in default browser") webbrowser.open(link) @@ -80,10 +80,10 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -100,7 +100,7 @@ def Run( if "http" not in link: link = "http://" + link - print("[" + lib + "] " + coords[0] + " Open website " + link + " in default browser, try to make a new window") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open website " + link + " in default browser, try to make a new window") webbrowser.open_new(link) @@ -134,12 +134,12 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) path_name = " ".join(split_line[1:]) if (not os.path.isfile(path_name)) and (not os.path.isdir(path_name)): - return (split_line[0] + " folder or file location '" + path_name + \ + return ("Line:" + str(idx+1) + " - " + split_line[0] + " folder or file location '" + path_name + \ "' does not exist.", line) return True @@ -155,7 +155,7 @@ def Run( path_name = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Open file or folder " + path_name) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open file or folder " + path_name) files.open_file_folder(path_name) @@ -189,10 +189,10 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 3: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -206,11 +206,11 @@ def Run( ): if len(split_line) > 2: - print("[" + lib + "] " + coords[0] + " Play sound file " + split_line[1] + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ " at volume " + str(split_line[2])) sound.play(split_line[1], float(split_line[2])) else: - print("[" + lib + "] " + coords[0] + " Play sound file " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) sound.play(split_line[1]) return idx+1 @@ -243,7 +243,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -259,10 +259,10 @@ def Run( if len(split_line) > 1: delay = split_line[1] print("[scripts] " + coords + - " Stopping sounds with " + delay + " milliseconds fadeout time") + " Line:" + str(idx+1) + " Stopping sounds with " + delay + " milliseconds fadeout time") sound.fadeout(int(delay)) else: - print("[scripts] " + coords + " Stopping sounds") + print("[scripts] " + coords + " Line:" + str(idx+1) + " Stopping sounds") sound.stop() return idx+1 @@ -295,7 +295,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) return True @@ -309,12 +309,12 @@ def Run( ): args = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Running code: " + args) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Running code: " + args) try: subprocess.run(args) except Exception as e: - print("[" + lib + "] " + coords[0] + " Error with running code: " + str(e)) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Error with running code: " + str(e)) return idx+1 diff --git a/commands_header.py b/commands_header.py index 7cf7a6f..5edf48d 100644 --- a/commands_header.py +++ b/commands_header.py @@ -25,10 +25,10 @@ def Validate( if pass_no == 1: if idx > 0: # headers normally have to check the line number - return (self.name + " must appear on the first line.", lines[0]) + return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) > 1: - return (self.name + " takes no arguments.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " takes no arguments.", line) return True @@ -72,20 +72,20 @@ def Validate( if pass_no == 1: if idx > 0: # headers normally have to check the line number - return (self.name + " must appear on the first line.", lines[0]) + return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return (self.name + " requires a key to bind.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a key to bind.", line) if len(split_line) > 2: - return (self.name + " only take one argument", line) + return ("Line:" + str(idx+1) + " - " + self.name + " only take one argument", line) if kb.sp(split_line[1]) == None: - return ("No key named '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) for lin in lines[1:]: if lin != "" and lin[0] != "-": - return ("When " + self.name + " is used, scripts can only contain comments.", lin) + return ("Line:" + str(idx+1) + " - When " + self.name + " is used, scripts can only contain comments.", lin) return True @@ -99,7 +99,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[cmds_head] " + coords + " Simple keybind: " + split_line[1]) + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " Simple keybind: " + split_line[1]) #PRESS key = kb.sp(split_line[1]) @@ -146,10 +146,10 @@ def Validate( if pass_no == 1: if idx > 0: # headers normally have to check the line number - return (self.name + " must appear on the first line.", lines[0]) + return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return (self.name + " requires a filename as a parameter.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a filename as a parameter.", line) return True @@ -165,17 +165,17 @@ def Run( layout_name = " ".join(split_line[1:]) - print("[cmds_head] " + coords + " Load layout " + layout_name) + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " Load layout " + layout_name) layout_path = os.path.join(files.LAYOUT_PATH, layout_name) if not os.path.isfile(layout_path): - print("[cmds_head] " + coords + " ERROR: Layout file does not exist.") + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " ERROR: Layout file does not exist.") return -1 try: layout = files.load_layout(layout_path, popups=False, save_converted=False) except files.json.decoder.JSONDecodeError: - print("[cmds_head] " + coords + " ERROR: Layout is malformated.") + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " ERROR: Layout is malformated.") return -1 if files.layout_changed_since_load: diff --git a/commands_keys.py b/commands_keys.py index bbde62f..3326d6d 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -26,7 +26,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -39,7 +39,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords + " Wait for script key to be unpressed") + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Wait for script key to be unpressed") while lp_events.pressed[coords[1]][coords[2]]: sleep(DELAY_EXIT_CHECK) @@ -76,13 +76,13 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 4: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) if kb.sp(split_line[1]) == None: - return ("No key named '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) return True @@ -100,10 +100,10 @@ def Run( releasefunc = lambda: kb.release(key) if len(split_line) <= 2: - print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1]) kb.tap(key) elif len(split_line) <= 3: - print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + " times") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + " times") taps = int(split_line[2]) for tap in range(taps): @@ -111,7 +111,7 @@ def Run( return idx + 1 kb.tap(key) else: - print("[" + lib + "] " + coords[0] + " Tap key " + split_line[1] + " " + split_line[2] + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + \ " times for " + str(split_line[3]) + " seconds each") taps = int(split_line[2]) @@ -155,13 +155,13 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) if kb.sp(split_line[1]) == None: - return ("No key named '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) return True @@ -174,7 +174,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Press key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Press key " + split_line[1]) key = kb.sp(split_line[1]) kb.press(key) @@ -209,13 +209,13 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) if kb.sp(split_line[1]) == None: - return ("No key named '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) return True @@ -228,7 +228,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Release key " + split_line[1]) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Release key " + split_line[1]) key = kb.sp(split_line[1]) kb.release(key) @@ -263,7 +263,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True @@ -276,7 +276,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Release all keys") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Release all keys") kb.release_all() @@ -310,7 +310,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) return True @@ -325,7 +325,7 @@ def Run( type_string = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Type out string " + type_string) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Type out string " + type_string) kb.write(type_string) diff --git a/commands_mouse.py b/commands_mouse.py index de276b2..9123286 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -28,20 +28,20 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 3: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) try: temp = int(split_line[1]) except: - return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) return True @@ -56,7 +56,7 @@ def Run( # removed error for != 3 tokens - print("[" + lib + "] " + coords[0] + " Relative mouse movement (" + split_line[1] + ", " + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + split_line[1] + ", " + \ str(split_line[2]) + ")") ms.move_to_pos(float(split_line[1]), float(split_line[2])) @@ -93,20 +93,20 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 3: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) try: temp = int(split_line[1]) except: - return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) return True @@ -121,7 +121,7 @@ def Run( # removed error for != 3 tokens - print("[" + lib + "] " + coords[0] + " Set mouse position to (" + split_line[1] + ", " + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + split_line[1] + ", " + \ str(split_line[2]) + ")") ms.set_pos(float(split_line[1]), float(split_line[2])) @@ -156,26 +156,26 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 3: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) try: temp = int(split_line[1]) except: - return ("'" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = float(split_line[1]) except: - return ("Invalid scroll amount '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[1] + "'.", line) if len(split_line) > 2: try: temp = float(split_line[2]) except: - return ("Invalid scroll amount '" + split_line[2] + "'.", line) + return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[2] + "'.", line) return True @@ -189,10 +189,10 @@ def Run( ): if len(split_line) > 2: - print("[" + lib + "] " + coords + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") ms.scroll(float(split_line[2]), float(split_line[1])) else: - print("[" + lib + "] " + coords + " Scroll " + split_line[1]) + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + split_line[1]) ms.scroll(0, float(split_line[1])) return idx+1 @@ -225,44 +225,44 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 5: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 7: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) try: temp = int(split_line[1]) except: - return ("'" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) try: temp = int(split_line[3]) except: - return ("'" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) try: temp = int(split_line[4]) except: - return ("'" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) if len(split_line) >= 6: try: temp = float(split_line[5]) except: - return ("'" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) if len(split_line) >= 7: try: temp = int(split_line[6]) if temp == 0: - return ("'" + self.name + "' skip value cannot be zero.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value cannot be zero.", line) except: - return ("'" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) return True @@ -289,11 +289,11 @@ def Run( skip = int(split_line[6]) if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Mouse line from (" + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line from (" + \ split_line[1] + ", " + split_line[2] + ") to (" + \ split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step") else: - print("[" + lib + "] " + coords + " Mouse line from (" + \ + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line from (" + \ split_line[1] + ", " + split_line[2] + ") to (" + \ split_line[3] + ", " + split_line[4] + ") by " + \ str(skip) + " pixels per step and wait " + split_line[5] + " milliseconds between each step") @@ -340,31 +340,31 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: - return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' requires at least X and Y arguments.", line) try: temp = int(split_line[1]) except: - return ("'" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) if len(split_line) >= 4: try: temp = float(split_line[3]) except: - return ("'" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) if len(split_line) >= 5: try: temp = int(split_line[4]) if temp == 0: - return ("'" + split_line[0] + "' skip value cannot be zero.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("'" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) return True @@ -389,11 +389,11 @@ def Run( skip = int(split_line[4]) if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Mouse line move relative (" + \ + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step") else: - print("[" + lib + "] " + coords + " Mouse line move relative (" + \ + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step and wait " + split_line[3] + " milliseconds between each step") @@ -441,31 +441,31 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 3: - return ("'" + split_line[0] + "' requires at least X and Y arguments.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' requires at least X and Y arguments.", line) try: temp = int(split_line[1]) except: - return ("'" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("'" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) if len(split_line) >= 4: try: temp = float(split_line[3]) except: - return ("'" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) if len(split_line) >= 5: try: temp = int(split_line[4]) if temp == 0: - return ("'" + split_line[0] + "' skip value cannot be zero.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("'" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) return True @@ -490,11 +490,11 @@ def Run( skip = int(split_line[4]) if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Mouse line set (" + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step") else: - print("[" + lib + "] " + coords[0] + " Mouse line set (" + \ + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step and wait " + split_line[3] + " milliseconds between each step") @@ -542,15 +542,15 @@ def Validate( try: temp = float(split_line[1]) except: - return ("'" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) if len(split_line) > 2: try: temp = int(split_line[2]) if temp == 0: - return ("'" + split_line[0] + "' skip value cannot be zero.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("'" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) return True @@ -574,10 +574,10 @@ def Run( skip = int(split_line[2]) if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ " in a line by " + str(skip) + " pixels per step") else: - print("[" + lib + "] " + coords + " Recall mouse position " + str(symbols["m_pos"]) + \ + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ " in a line by " + str(skip) + " pixels per step and wait " + \ split_line[1] + " milliseconds between each step") @@ -624,7 +624,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: - return ("'" + split_line[0] + "' takes no arguments.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes no arguments.", line) return True @@ -637,7 +637,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Store mouse position") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Store mouse position") symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. @@ -671,7 +671,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) > 1: - return ("'" + split_line[0] + "' takes no arguments.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes no arguments.", line) return True @@ -685,9 +685,9 @@ def Run( ): if symbols['m_pos'] == tuple(): - print("[" + lib + "] " + coords[0] + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[" + lib + "] " + coords[0] + " Recall mouse position " + str(symbols['m_pos'])) + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols['m_pos'])) ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) return idx+1 diff --git a/commands_pause.py b/commands_pause.py index ce23b90..58eaa0a 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -27,15 +27,15 @@ def Validate( if pass_no == 1: # check number of split_line if len(split_line) < 2: - return ("Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) try: temp = float(split_line[1]) except: - return ("Delay time '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - Delay time '" + split_line[1] + "' not valid.", line) return True @@ -49,7 +49,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Delay for " + split_line[1] + " seconds") + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Delay for " + split_line[1] + " seconds") delay = float(split_line[1]) diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 74b0f9d..e5d797e 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -563,7 +563,7 @@ def Validate( c_len = len(split_line) # Number of tokens # check number of split_line if c_len < 2: - return ("Wrong number of parameters (at least 1 required) in " + self.name, line) + return ("Line:" + str(idx+1) + " - Wrong number of parameters (at least 1 required) in " + self.name, line) i = 1 # using a loop counter rather than an itterator because that makes the code similar to execution @@ -583,9 +583,9 @@ def Validate( if opr in self.operators: # if it's valid i = i + 1 + self.operators[opr][1] # pull of additional parameters if required if i > c_len: - return ("Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) else: # if invalid, report it - return ("Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) return True @@ -599,7 +599,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params @@ -624,10 +624,10 @@ def Run( try: i = i + self.operators[opr][0](symbols, opr, split_line[i:]) # run it except: - print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " '" + cmd + "'") + print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") break else: # if invalid, report it - print("invalid operator #" + str(i) + " '" + cmd + "'") + print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") break return idx+1 # Normal default exit to the next line diff --git a/scripts.py b/scripts.py index c2b8058..7a3e877 100644 --- a/scripts.py +++ b/scripts.py @@ -124,7 +124,7 @@ def parse_script(self): err = res # note the error errors += 1 # and 1 more error else: - msg = "Invalid command '" + split_line[0] + "' on line " + str(idx) + "." + msg = "Invalid command '" + split_line[0] + "' on line " + str(idx+1) + "." if err == True: err = (msg, line) # note the error print (msg) @@ -250,7 +250,7 @@ def run_script(self): cmd = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd in VALID_COMMANDS: command = VALID_COMMANDS[cmd] - command.Run(0, [cmd], self.symbols, (self.coords, self.x, self.y), self.is_async) + command.Run(-1, [cmd], self.symbols, (self.coords, self.x, self.y), self.is_async) if len(self.script_lines) > 0: self.running = True From 68ec6607f52d344e78f6c28c3f24d72f167f6f78 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 6 Sep 2020 21:32:35 +0800 Subject: [PATCH 12/83] Variables for mouse commands * Variables can be used in place of constants for mouse commands * variables must now start with an alpha character * refactoring begun on the validation and run code for commands to enable variables and simplify coding * documentation update * some typos fixed (especially with some command validation) This is not a final release. This version is an indication of how variables can be incorporated and still requires a lot of work. --- README.md | 3 ++ commands_external.py | 2 +- commands_mouse.py | 122 +++++++++++++++++++++++++------------------ commands_rpncalc.py | 7 +++ variables.py | 46 ++++++++++++++++ 5 files changed, 127 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d46d0e3..02e8f88 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * If (argument 3) supplied, delay (argument 3) seconds before releasing each time. #### Mouse Movement [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +All Mouse movement commands can now use variables in place of constants. Variables are taken from the local variables first, then global. Undefined variables return 0. Variable names must start with an alphabetic character and are not case sensitive. * `M_LINE` * Move the mouse in a line from absolute point (argument 1),(argument 2) to absolute point (argument 3),(argument 4). * If (argument 5) supplied, delay (argument 5) milliseconds between each step. @@ -292,6 +293,8 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * An RPN (stack-based) calculator that implements local and global variables. * Any number of commands may follow from 1 to infinity? * Commands and variables are NOT case sensitive + * Variables must begin with an alphabetic character and cannot contain spaces. + * Variables can be used in the Mouse commands in place of constants * Any numeric value is pushed onto the stack * Common functions pop their parameters off the stack and push the result. Note that any function requiring more values that there are on the stack will be returned zero for all additional parameters. * + - replaces the top two values on the stack with their sum diff --git a/commands_external.py b/commands_external.py index c3ff11c..c130a55 100644 --- a/commands_external.py +++ b/commands_external.py @@ -29,7 +29,7 @@ def Validate( return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) if len(split_line) > 2: - return ("TLine:" + str(idx+1) + " - oo many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) return True diff --git a/commands_mouse.py b/commands_mouse.py index 9123286..2a1d1cf 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -1,4 +1,4 @@ -import command_base, ms, scripts +import command_base, ms, scripts, variables lib = "cmds_mous" # name of this library (for logging) @@ -24,24 +24,18 @@ def Validate( pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - # no longer allow just 2 tokens - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 3: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + ret = variables.check_num(split_line, [2], idx, line, self.name) + if ret != True: + return ret - if len(split_line) > 3: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + ret = variables.check_param(split_line, 1, "X value", idx, self.name, line, True) + if ret != True: + return ret - try: - temp = int(split_line[1]) - except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) - - try: - temp = int(split_line[2]) - except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) + ret = variables.check_param(split_line, 2, "Y value", idx, self.name, line, True) + if ret != True: + return ret return True @@ -54,12 +48,13 @@ def Run( is_async # True if the script is running asynchronously ): - # removed error for != 3 tokens + v1 = variables.get_value(1, symbols) + v2 = variables.get_value(2, symbols) + + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(v1) + ", " + \ + str(v2) + ")") - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + split_line[1] + ", " + \ - str(split_line[2]) + ")") - - ms.move_to_pos(float(split_line[1]), float(split_line[2])) + ms.move_to_pos(float(v1), float(v2)) return idx+1 @@ -101,12 +96,14 @@ def Validate( try: temp = int(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) return True @@ -119,12 +116,20 @@ def Run( is_async # True if the script is running asynchronously ): - # removed error for != 3 tokens - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + split_line[1] + ", " + \ - str(split_line[2]) + ")") - - ms.set_pos(float(split_line[1]), float(split_line[2])) + v1 = split_line[1] + if variables.valid_var_name(v1): + with symbols['g_vars'][0]: # lock the globals while we do this + v1 = variables.get(v1, symbols['l_vars'], symbols['g_vars'][1]) + + v2 = split_line[2] + if variables.valid_var_name(v2): + with symbols['g_vars'][0]: # lock the globals while we do this + v1 = variables.get(v2, symbols['l_vars'], symbols['g_vars'][1]) + + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(v1) + ", " + \ + str(v2) + ")") + + ms.set_pos(float(v1), float(v2)) return idx+1 @@ -164,18 +169,15 @@ def Validate( try: temp = int(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) - - try: - temp = float(split_line[1]) - except: - return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[1] + "'.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) if len(split_line) > 2: try: temp = float(split_line[2]) except: - return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[2] + "'.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[2] + "'.", line) return True @@ -233,28 +235,33 @@ def Validate( try: temp = int(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) try: temp = int(split_line[3]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) + if not variables.valid_var_name(split_line[3]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) try: temp = int(split_line[4]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) + if not variables.valid_var_name(split_line[4]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) if len(split_line) >= 6: try: temp = float(split_line[5]) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) + if not variables.valid_var_name(split_line[5]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) if len(split_line) >= 7: try: @@ -262,7 +269,8 @@ def Validate( if temp == 0: return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value cannot be zero.", line) except: - return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) + if not variables.valid_var_name(split_line[6]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) return True @@ -345,18 +353,21 @@ def Validate( try: temp = int(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) if len(split_line) >= 4: try: temp = float(split_line[3]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + if not variables.valid_var_name(split_line[3]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) if len(split_line) >= 5: try: @@ -364,7 +375,8 @@ def Validate( if temp == 0: return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + if not variables.valid_var_name(split_line[4]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) return True @@ -446,18 +458,21 @@ def Validate( try: temp = int(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) try: temp = int(split_line[2]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) if len(split_line) >= 4: try: temp = float(split_line[3]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) + if not variables.valid_var_name(split_line[3]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) if len(split_line) >= 5: try: @@ -465,7 +480,8 @@ def Validate( if temp == 0: return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + if not variables.valid_var_name(split_line[4]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) return True @@ -542,7 +558,8 @@ def Validate( try: temp = float(split_line[1]) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) + if not variables.valid_var_name(split_line[1]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) if len(split_line) > 2: try: @@ -550,7 +567,8 @@ def Validate( if temp == 0: return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) except: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) + if not variables.valid_var_name(split_line[2]): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) return True diff --git a/commands_rpncalc.py b/commands_rpncalc.py index e5d797e..932a054 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -581,6 +581,13 @@ def Validate( if n == None: opr = cmd.upper() # Convert to uppercase for searching if opr in self.operators: # if it's valid + for p in range(self.operators[opr][1]): + if i + p + 1 >= c_len: + return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + else: + param = split_line[i+p+1] + if not variables.valid_var_name(param): + return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, line) i = i + 1 + self.operators[opr][1] # pull of additional parameters if required if i > c_len: return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) diff --git a/variables.py b/variables.py index 57eb084..184c9f9 100644 --- a/variables.py +++ b/variables.py @@ -63,3 +63,49 @@ def next_cmd(ret, cmds): raise Exception("Can't get next element.") else: return ret+1, v # and we return an updated pointer and the removed element + + +# variable names should start with an alpha character +def valid_var_name(v): + return len(v) > 0 and ord(v[0].upper()) in range(ord('A'), ord('Z')+1) + + +# check the number of variables allowed +def check_num(split_line, lens, idx, line, name): + n = len(split_line)-1 + if n in lens: + return True # it's OK + + msg = "Line:" + str(idx+1) + " - Incorrect number of parameters (" + str(n) + ") supplied. " + if len(lens) == 0: + msg += "No valid number of parameters" + elif len(lens) == 1: + msg += str(lens[0]) + else: + msg += ", ".join([str(el) for el in lens[0:-1]]) + ", " + str(lens[-1]) + + msg += " required for command '" + name + "'." + + return (msg, line) + + +# check a parameter +def check_param(split_line, p, desc, idx, name, line, var_ok): + try: + temp = int(split_line[p]) + except: + if not (var_ok and valid_var_name(split_line[p])): # a variable is OK here + return ("Line:" + str(idx+1) + " - '" + name + "' " + desc + " '" + split_line[p] + "' not valid.", line) + + return True + + +# get the value of a parameter +def get_value(n, symbols): + v = split_line[n] + if valid_var_name(v): + g_vars = symbols['g_vars'] + with g_vars[0]: # lock the globals while we do this + v = variables.get(v, symbols['l_vars'], g_vars[1]) + + return v From 53aa082b33fe159ff29669be6f0415c45ce9c131 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 7 Sep 2020 19:11:37 +0800 Subject: [PATCH 13/83] How to add commands * Damn! I thought I had committed this ages ago (657) --- NewCommands.md | 541 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 541 insertions(+) create mode 100644 NewCommands.md diff --git a/NewCommands.md b/NewCommands.md new file mode 100644 index 0000000..3d0acea --- /dev/null +++ b/NewCommands.md @@ -0,0 +1,541 @@ +# Types of commands + +There are two major types of commands you may wish to add, these are: + * Headers + * Regular commands + +In addition, you may also need to review: + * Registration + * The Symbol table + * Coords + * self.Name + * Passing Python arguments by reference + +In most cases, regular commands will do all you need, and that's probably where you should start. + +If you are contemplating adding a new command to an existing module, the process is quite straightforward. If you are creating a new module to hold your commands, you will also need to refer to the section "Registration". + +## Regular Commands + +You should make yourself familiar with the way existing commands work. Look in one of the `commands_*.py` files (other than `commands_header.py`) for examples of existing commands. + +As an example, We'll use the `IF_UNPRESSED_GOTO_LABEL` command. This can be found in the `commands_control.py` module. + +### Decoding the IF_UNPRESSED_GOTO_LABEL command + +The `IF_UNPRESSED_GOTO_LABEL` command looks like this: + +```python +# ################################################## +# ### CLASS Control_If_Unpressed_Goto_Label ### +# ################################################## + +# class that defines the IF_UNPRESSED_GOTO_LABEL command +class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + if len(split_line) != 2: + return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.name, line) + + return True + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed + return symbols["labels"][split_line[1]] # jump to the label + + return idx+1 + + +scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command +``` + +We will examine the 5 parts you need to consider + +#### Part 1 - The Class Header + +Each command is defined as a class. The Class Header defines that new class, and some of its important properties. + +The class should always begin with some documentation to both highlight the start of the definition of a new class, and also to inform people what the command is supposed to do. The documentation in this example is minimal rather than optimal. + +```python +# ################################################## +# ### CLASS Control_If_Unpressed_Goto_Label ### +# ################################################## + +# class that defines the IF_UNPRESSED_GOTO_LABEL command +``` + +#### Part 2 - The Class definition + +The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. + +It is also important that you define a unique name for your new command. I recommend using *Module*_*Command*, where *Module* is the part of the module name after `commands_`. + +```python +class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): +``` + +In this example, the `IF_UNPRESSED_GOTO_LABEL` command is contained in the `commands_control.py` module, so the name of the class is `Control_If_Unpressed_Goto_Label`. + +#### Part 3 - Class Initialization + +The initialization of a command class serves to define the name of the command. This is literally what you need to place inside your script. + +```python + def __init__( + self, + ): + + super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code +``` + +Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. + +#### Part 4 - Command Validation + +Every command requires a validation. If you do not provide validation code, the ancestor class will return a blank error message when this command is encountered. + +```python + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + if len(split_line) != 2: + return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + + if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist + if split_line[1] not in symbols["labels"]: + return ("Target not found for " + self.name, line) + + return True +``` + +This may appear more complex than it is. The first 10 lines are the definition and should simply be copied unchanged into your code. + +It is important to note that the validation is called twice, once for the first pass (`pass_no = 1`), and again for the second pass (`pass_no = 2`). Commands will almost always require some coding in pass 1, and where a command refers to something that may have been defined elsewhere, pass 2 will also be required. + +This example has a typical, if simple, pass 1 validation. This command simply requires that there is exactly 1 argument (we check for 2 because the command itself is also counted). Note that in this case, because the second argument is a label, it cannot be checked here. + +In this case a pass 2 check is required to check for the existance of the label referred to in the command. During pass 1, this label has (presumably) been defined on some other line of the script. + +If errors are to be returned, the correct format is a tuple of 2 strings, the first being the error message, and the second being the line you refer to. The line referenced is typically (but not always) the line being parsed. + +Finally, the method should return `True`. The simple concept being that if an error has not been detected, the validation has been successful. + +The symbol table (`symbols`) is a structure that you need to understand if you are writing more complex commands. + +#### Part 5 - Command Execution + +Every command that does something (e.g. not labels - that have their effect during pass 1 of validation, or comments - that have no function) requires code to run it. If you do not provide `Run` code, the ancestor class will do nothing (i.e., it does not generate an error). + +```python + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + + if not split_line[1] in symbols["labels"]: # The label should always exist + print("missing LABEL '" + split_line[1] + "'") # otherwise an error + return -1 + else: + if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed + return symbols["labels"][split_line[1]] # jump to the label + + return idx+1 +``` + +Again, this looks somewhat complex, but isn't really scary. Again, the first 8 lines are simply the method header and should be copied verbatim. + +The first thing this command does is write an output line describing what it is going to do. + +This command repeats some of the validation again, in this case it is possible that the symbol table could have been changed (probably due to a bug, but there's no reason why a command couldn't also do this) so the label is confirmed to exist. An error return for this method is -1 (actually any number outside the range of the lines in the script will work, but -1 is strongly recommended). + +In this case, if the label exists, and if the button is not pressed, the return value is given by the value of the label within the symbol table (this value is the line number of the label, and thus indicates the next line to be executed). + +Finally, the current line + 1 should be returned. This tells the script to continue on the next line. Some commands will never reach this line, but it should nevertheless be included (just in case you have a bug). + +#### Part 5 - Command Integration + +The final step is to include code to incorporate this command into the set of commands available for scripts. + +```python +scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command +``` + +This line creates a command object, and passes it to the routine which adds it to the list of available commands. + +It is important to note that the definition of more than one command with exactly the same name will result in only the second one being available. + +## Headers + +Headers and Commands are very similar. The basic format of creating them is the same, however there are some important differences. This section will concentrate mostly on those differences. + +You should make yourself familiar with the way existing headers work. Look in the `commands_header.py` file for examples of existing commands. + +As an example, We'll use the `@ASYNC` header. + +### Decoding the @ASYNC header + +The `@ASYNC` header looks like this: + +```python +# ################################################## +# ### CLASS Header_Async ### +# ################################################## + +class Header_Async(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@ASYNC", # the name of the header as you have to enter it in the code + True) # You also define if the header causes the script to be asynchronous + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if idx > 0: # headers normally have to check the line number + return (self.name + " must appear on the first line.", lines[0]) + + if len(split_line) > 1: + return (self.name + " takes no arguments.", lines[0]) + + return True + + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + return idx+1 + + +scripts.add_command(Header_Async()) # register the header +``` + +This consists of almost exactly the same 5 parts, but with important differences. + +#### Part 1 - The Class Header + +This is effectively identical to a regular command. In this example there is no specific documentation describing what `@ASYNC` actually does -- and that's not good. You should do better! + +```python +# ################################################## +# ### CLASS Header_Async ### +# ################################################## +``` + +#### Part 2 - The Class definition + +The most important part of the class definition is that the new class is derived from the appropriate base class. For headers, this should be `command_base.Command_Header` although experienced python programmers could also create a new header definition that derives from another header. + +The unique name for your header should be derived in a similar way to commands, other than that the leading `@` in a header should be omitted. (I recommend using *Module*_*Header*, where *Module* is the part of the module name after `commands_`). + +```python +class Header_Async(command_base.Command_Header): +``` + +In this example, the `@ASYNC` command is contained in the `commands_header.py` module, so the name of the class is `Header_Async`. + +#### Part 3 - Class Initialization + +The initialization of a header class adds an extra parameter. You must also specify if this header causes asynchronous behaviour. Note that headers (by convention) start with `@`. This is not a requirement, but you should honour it. + +```python + def __init__( + self, + ): + + super().__init__("@ASYNC", # the name of the header as you have to enter it in the code + True) # You also define if the header causes the script to be asynchronous +``` + +Note that header names are also case sensitive. + +#### Part 4 - Command Validation + +Every command requires a validation. If you do not provide validation code, the ancestor class will return a blank error message when this command is encountered. + +```python + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if idx > 0: # headers normally have to check the line number + return (self.name + " must appear on the first line.", lines[0]) + + if len(split_line) > 1: + return (self.name + " takes no arguments.", line) + + return True +``` + +Validation for headers is identical to that for commands. A difference in functionality is that you may want to ensure tha the header is placed in the script in an appropriate place. + +It would be quite rare for a header to require second pass validation, but it is still called twice, so it is available if needed. + +#### Part 5 - Header Execution + +Every command that does something (e.g. not labels - that have their effect during pass 1 of validation, or comments - that have no function) requires code to run it. If you do not provide `Run` code, the ancestor class will do nothing (i.e., it does not generate an error). + +```python + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + return idx+1 +``` + +The `Run` code for headers is also identical to commands. + +If the header's function is performed during validation (`@ASYNC` is) then there is no need to do anything in the `Run` method. This is actually the default behaviour, and in this case a `Run` method could be omitted. + +#### Part 5 - Command Integration + +The final step has the same form, behavior, and cautions as for commands. + +```python +scripts.add_command(Header_Async()) # register the header +``` + +## Registration + +Commands are not dynamically discovered. This means you need to tell the main executable that a new command module exists. + +In addition, you need to provide an abbreviated name of your module that will be printed in log messages. + +### Abbreviated name + +This is perhaps the simplest, and you may have already noticed it. Within each command module a string is defined at the top of that module: + +```python +lib = "cmds_ctrl" # name of this library (for logging) +``` + +For consistency, I recommend you retain the first 5 characters (`cmds_`), and to make the log messages line up, I recommend that the abbreviation is exactly 4 characters (e.g. `midi` for a midi module) Note that all `lib` strings should be unique. + +### Importing the module + +The most important step for a new command module is to inform `LPHK.py` of the existance of the new module. + +Edit `command_list.py` to include your new module name. That's it! + +## The Symbol table + +The symbol table is a dictionary of objects passed to `Validate` and `Run` methods that contains important information for the execution of the script. + +Currently the dictionary contains x entries: + * 'repeats' : A dictionary of loop counters for `REPEAT` commands + * 'original' : A dictionary of the starting values for `REPEAT` commands + * 'labels' : A dictionary of the label names and locations within the script + * 'm_pos' : A tuple containing the saved mouse position + +The symbol table can be modified in the `Validation` and/or `Run` methods. + +An example of adding a new label (from the `GOTO_LABEL` command) is: + +```python + # add label to symbol table # Add the new label to the labels in the symbol table + symbols["labels"][split_line[1]] = idx # key is label, data is line number +``` + +This can be checked for existance by: + +```python + if split_line[1] in symbols["labels"]: # Does the label already exist (that's bad)? + ... +``` + +Finally, it can be accessed to determine where a label is: + +```python + return symbols["labels"][split_line[1]] # normally we return the line number the label is on +``` + +### Repeats + +This dictionary within the symbol table contains entries where the key is the line number of the `REPEAT` command, and the value is the number of repeats remaining. + +### Original + +This dictionary within the symbol table contains entries where the key is the line number of the `REPEAT` command, and the value is the initial value for the number of repeats. + +### Repeats + +This dictionary within the symbol table has entries where the key is the label name, and the value is the line number. + +### M_pos + +This tuple within the dictionary contains either an empty tuple (`tuple()`) or the saved (x,y) mouse coordinates. + +## Coords + +The coords are the current x,y values passed to the command (or header). I believe these are the button coordinates. + +This array contains 3 elements: + * coords[0] - a string describing the location + * coords[1] - the X value + * coords[2] - the Y value + +## self.Name + +If an error message needs to refer to the name of the command, using `self.Name` is the preferred method. + +## Passing Python arguments by reference + +In many programming languages you can pass parameters by value or by reference. Passing a value by reference allows you to change it in the method and have the value changed in the calling code. + +Python uses a copy on assignment method with parameters passed to a method. Thus assigning a new value to a parameter does not work + +``` +Python 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> def change(a): +... a = 10 +... +>>> x = 1 +>>> print(x) +1 +>>> change(x) +>>> print(x) +1 +``` + +But if you pass a mutable object, you **can** modify it. + +``` +Python 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> def change(a): +... a[0] = 10 +... +>>> x = [1] +>>> print(x) +[1] +>>> change(x) +>>> print(x) +[10] +``` + +But you need to be careful to modify it rather than assigning to it, or you'll get the first behaviour + +``` +(LPHK-build) C:\Users\Steve\Documents\Projects\LPHK\svn-sh\branches\develop>python +Python 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> def change(a): +... a = [10] +... +>>> x = [1] +>>> print(x) +[1] +>>> change(x) +>>> print(x) +[1] +``` + +Also, the object passed must be mutable + +``` +Python 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> def change(a): +... a[0] = 10 +... +>>> x = (1) +>>> change(x) +Traceback (most recent call last): + File "", line 1, in + File "", line 2, in change +TypeError: 'int' object does not support item assignment +>>> print(x) +1 +``` + +An advantage of this method is that it allows you to poass constants too. + +``` +Python 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> def change(a): +... a[0] = 10 +... +>>> change([1]) +>>> print([1]) +[1] +``` + +You might wonder why I printed `[1]` in the last example. This is because a very old historic version of Fortran, if faced with this situation would respond by changing the constant labelled as 1. This meant that literal constants could be changed at runtime, causing all sorts of weird behaviour! Python doesn't do that. + +For the symbol table I have decided to use a dictionary. This allows new entries to be added or even removed without changing how otherwise unaffected parts of the code perform. \ No newline at end of file From 850cbc1073c4277c3bb328e96f55a46bc4a47f7f Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:33:33 +0800 Subject: [PATCH 14/83] A far more robust implementation of variables for the Mouse commands. * only the mouse commands are implemented * this is a stepping stone to a far more elegant solution where commands can validate themselves. * updated documentation showing how to add new commands using variables * variables are readable only at present in commands other than RPN_EVAL try the following examples M_MOVE 1.2 7 This will fail because the first parameter is not an integer. negative numbers also cause failure to validate. RPN_EVAL 1.5 > a RPN_EVAL -1 > b M_MOVE a b This validates correctly, but fails at run-time an error indicating the second parameter is negative. The first parameter is converted to an integer, resulting in the value 1 being used. Note that this is currently a simple truncation. --- NewCommands.md | 185 +++++++++---- commands_mouse.py | 646 ++++++++++++++++++++++++++++++---------------- variables.py | 104 ++++++-- 3 files changed, 649 insertions(+), 286 deletions(-) diff --git a/NewCommands.md b/NewCommands.md index 3d0acea..a7b2c96 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -19,24 +19,26 @@ If you are contemplating adding a new command to an existing module, the process You should make yourself familiar with the way existing commands work. Look in one of the `commands_*.py` files (other than `commands_header.py`) for examples of existing commands. -As an example, We'll use the `IF_UNPRESSED_GOTO_LABEL` command. This can be found in the `commands_control.py` module. +As an example, We'll use the `M_SCROLL` command. This can be found in the `commands_mouse.py` module. -### Decoding the IF_UNPRESSED_GOTO_LABEL command +Recent changes to add variable support to commands has increased the complexity of each command. Never fear, I have plans to make this even simpler than the previous version, with little to no validation coding needed in the near future. -The `IF_UNPRESSED_GOTO_LABEL` command looks like this: +### Decoding the MOUSE_SCROLL command + +The `M_SCROLL` command looks like this: ```python # ################################################## -# ### CLASS Control_If_Unpressed_Goto_Label ### +# ### CLASS Mouse_Scroll ### # ################################################## -# class that defines the IF_UNPRESSED_GOTO_LABEL command -class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): +# class that defines the M_SCROLL command (???) +class Mouse_Scroll(command_base.Command_Basic): def __init__( - self, + self, ): - super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code + super().__init__("M_SCROLL") # the name of the command as you have to enter it in the code def Validate( self, @@ -49,12 +51,17 @@ class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + ret = variables.check_num(split_line, [1, 2], idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) return True @@ -67,19 +74,48 @@ class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + + if v2: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False - if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + if v2: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: return -1 + + if v2: + ms.scroll(float(v2), float(v1)) else: - if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed - return symbols["labels"][split_line[1]] # jump to the label + ms.scroll(0, float(v1)) return idx+1 -scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command +scripts.add_command(Mouse_Scroll()) # register the command ``` We will examine the 5 parts you need to consider @@ -92,10 +128,10 @@ The class should always begin with some documentation to both highlight the star ```python # ################################################## -# ### CLASS Control_If_Unpressed_Goto_Label ### +# ### CLASS Mouse_Scroll ### # ################################################## -# class that defines the IF_UNPRESSED_GOTO_LABEL command +# class that defines the M_SCROLL command (???) ``` #### Part 2 - The Class definition @@ -105,10 +141,10 @@ The most important part of the class definition is that the new class is derived It is also important that you define a unique name for your new command. I recommend using *Module*_*Command*, where *Module* is the part of the module name after `commands_`. ```python -class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): +class Mouse_Scroll(command_base.Command_Basic): ``` -In this example, the `IF_UNPRESSED_GOTO_LABEL` command is contained in the `commands_control.py` module, so the name of the class is `Control_If_Unpressed_Goto_Label`. +In this example, the `M_SCROLL` command is contained in the `commands_mouse.py` module, so the name of the class should be `Mouse_M_Scroll`, but I've called it `Mouse_Scroll` because every mouse command starts with "M_". I will probably change this at some point. #### Part 3 - Class Initialization @@ -116,10 +152,10 @@ The initialization of a command class serves to define the name of the command. ```python def __init__( - self, + self, ): - super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code + super().__init__("M_SCROLL") # the name of the command as you have to enter it in the code ``` Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. @@ -129,7 +165,7 @@ Note that commands are case sensitive, so the name should be in all uppercase to Every command requires a validation. If you do not provide validation code, the ancestor class will return a blank error message when this command is encountered. ```python - def Validate( + def Validate( self, idx: int, # The current line number line, # The current line @@ -140,12 +176,17 @@ Every command requires a validation. If you do not provide validation code, the ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 2: - return ("'" + split_line[0] + "' takes exactly 1 argument.", line) + ret = variables.check_num(split_line, [1, 2], idx, line, self.name) + if ret != True: + return ret - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Target not found for " + self.name, line) + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) return True ``` @@ -154,13 +195,13 @@ This may appear more complex than it is. The first 10 lines are the definition It is important to note that the validation is called twice, once for the first pass (`pass_no = 1`), and again for the second pass (`pass_no = 2`). Commands will almost always require some coding in pass 1, and where a command refers to something that may have been defined elsewhere, pass 2 will also be required. -This example has a typical, if simple, pass 1 validation. This command simply requires that there is exactly 1 argument (we check for 2 because the command itself is also counted). Note that in this case, because the second argument is a label, it cannot be checked here. +This example has a typical, if simple, pass 1 validation. This command simply requires that there are 1 or 2 arguments (we check for \[1, 2\]). Note that in this case, the second parameter is optional -In this case a pass 2 check is required to check for the existance of the label referred to in the command. During pass 1, this label has (presumably) been defined on some other line of the script. +No pass 2 is required for this command. -If errors are to be returned, the correct format is a tuple of 2 strings, the first being the error message, and the second being the line you refer to. The line referenced is typically (but not always) the line being parsed. +If errors are to be returned, the correct format is a tuple of 2 strings, the first being the error message, and the second being the line you refer to. The line referenced is typically (but not always) the line being parsed. This string is creates automatically by the check_int_param function (that checks for an integer parameter). By default this function allows a variable to be used, and I have also specified a validation rule. This validation rule applies to literals. -Finally, the method should return `True`. The simple concept being that if an error has not been detected, the validation has been successful. +Finally, the method should return `True` if there were no errors. The symbol table (`symbols`) is a structure that you need to understand if you are writing more complex commands. @@ -178,34 +219,69 @@ Every command that does something (e.g. not labels - that have their effect duri is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " If key is not pressed goto LABEL " + split_line[1]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + + if v2: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) - if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if v2: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: return -1 + + if v2: + ms.scroll(float(v2), float(v1)) else: - if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed - return symbols["labels"][split_line[1]] # jump to the label + ms.scroll(0, float(v1)) return idx+1 ``` Again, this looks somewhat complex, but isn't really scary. Again, the first 8 lines are simply the method header and should be copied verbatim. -The first thing this command does is write an output line describing what it is going to do. +ok and p are set up to track the success, and store the number of parameters passed respectively. + +The variables.get_value function call gets the constant from the command, or the value of a variable if one was specified. If a parameter is optional (as v2 is here), None will be returned if it does not exist. + +Next this command writes an output line describing what it is going to do. In this case ther are 2 possibilities based on the existance of the second parameter. -This command repeats some of the validation again, in this case it is possible that the symbol table could have been changed (probably due to a bug, but there's no reason why a command couldn't also do this) so the label is confirmed to exist. An error return for this method is -1 (actually any number outside the range of the lines in the script will work, but -1 is strongly recommended). +The validation code is called again, since variables may have been used. This does run-time checks of values in a manner similar to the validation that is done earlier for literal values. It is possible to return multiple errors. -In this case, if the label exists, and if the button is not pressed, the return value is given by the value of the label within the symbol table (this value is the line number of the label, and thus indicates the next line to be executed). +If an error has been detected, the routine exits with -1 indicating failure. -Finally, the current line + 1 should be returned. This tells the script to continue on the next line. Some commands will never reach this line, but it should nevertheless be included (just in case you have a bug). +Finally, the work of the function is performed on the validated values. + +idx + 1 is returned. This points to the nect line to execute, which in this instance is the next line (flow control commands may return different values). #### Part 5 - Command Integration The final step is to include code to incorporate this command into the set of commands available for scripts. ```python -scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command +scripts.add_command(Mouse_Scroll()) # register the command ``` This line creates a command object, and passes it to the routine which adds it to the list of available commands. @@ -220,6 +296,8 @@ You should make yourself familiar with the way existing headers work. Look in t As an example, We'll use the `@ASYNC` header. +Note that this example has not been updated for variable usage. + ### Decoding the @ASYNC header The `@ASYNC` header looks like this: @@ -400,6 +478,9 @@ Currently the dictionary contains x entries: * 'original' : A dictionary of the starting values for `REPEAT` commands * 'labels' : A dictionary of the label names and locations within the script * 'm_pos' : A tuple containing the saved mouse position + * 'g_vars' : A tuple containing the lock object and the dictionary of global variables + * 'l_vars' : A dictionary containing local variables + * 'stack' : A mutable tuple containing the local stack The symbol table can be modified in the `Validation` and/or `Run` methods. @@ -431,7 +512,7 @@ This dictionary within the symbol table contains entries where the key is the li This dictionary within the symbol table contains entries where the key is the line number of the `REPEAT` command, and the value is the initial value for the number of repeats. -### Repeats +### Labels This dictionary within the symbol table has entries where the key is the label name, and the value is the line number. @@ -439,6 +520,18 @@ This dictionary within the symbol table has entries where the key is the label n This tuple within the dictionary contains either an empty tuple (`tuple()`) or the saved (x,y) mouse coordinates. +### Global variables + +The first element is a lock object used to synchronise access to the global variables. The second element is a dictionary where the keys are the global variable names and the values are the variable values. + +### Local variables + +This is a dictionary where the keys are the local variable names and the values are the variable values. + +### Stack + +This structure contains the list of values that make up the stack for the current command. Values are added and removed by various "RPN_EVAL" functions. + ## Coords The coords are the current x,y values passed to the command (or header). I believe these are the button coordinates. diff --git a/commands_mouse.py b/commands_mouse.py index 2a1d1cf..2e54237 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -9,7 +9,7 @@ # class that defines the M_MOVE command (wait while a button is pressed?) class Mouse_Move(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("M_MOVE") # the name of the command as you have to enter it in the code @@ -29,13 +29,13 @@ def Validate( if ret != True: return ret - ret = variables.check_param(split_line, 1, "X value", idx, self.name, line, True) + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) if ret != True: - return ret + return (ret, line) - ret = variables.check_param(split_line, 2, "Y value", idx, self.name, line, True) + ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) if ret != True: - return ret + return (ret, line) return True @@ -48,12 +48,33 @@ def Run( is_async # True if the script is running asynchronously ): - v1 = variables.get_value(1, symbols) - v2 = variables.get_value(2, symbols) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(v1) + ", " + \ str(v2) + ")") - + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + ms.move_to_pos(float(v1), float(v2)) return idx+1 @@ -69,7 +90,7 @@ def Run( # class that defines the M_SET command (put the mouse somewhere) class Mouse_Set(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("M_SET") # the name of the command as you have to enter it in the code @@ -84,26 +105,18 @@ def Validate( pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - # no longer allow just 2 tokens - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 3: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - if len(split_line) > 3: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + ret = variables.check_num(split_line, [2], idx, line, self.name) + if ret != True: + return ret - try: - temp = int(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) - try: - temp = int(split_line[2]) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y value '" + split_line[2] + "' not valid.", line) + ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) return True @@ -116,19 +129,33 @@ def Run( is_async # True if the script is running asynchronously ): - v1 = split_line[1] - if variables.valid_var_name(v1): - with symbols['g_vars'][0]: # lock the globals while we do this - v1 = variables.get(v1, symbols['l_vars'], symbols['g_vars'][1]) - - v2 = split_line[2] - if variables.valid_var_name(v2): - with symbols['g_vars'][0]: # lock the globals while we do this - v1 = variables.get(v2, symbols['l_vars'], symbols['g_vars'][1]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(v1) + ", " + \ str(v2) + ")") + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + ms.set_pos(float(v1), float(v2)) return idx+1 @@ -144,7 +171,7 @@ def Run( # class that defines the M_SCROLL command (???) class Mouse_Scroll(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("M_SCROLL") # the name of the command as you have to enter it in the code @@ -160,24 +187,17 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - if len(split_line) > 3: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + ret = variables.check_num(split_line, [1, 2], idx, line, self.name) + if ret != True: + return ret - try: - temp = int(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' X value '" + split_line[1] + "' not valid.", line) + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) - if len(split_line) > 2: - try: - temp = float(split_line[2]) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - Invalid scroll amount '" + split_line[2] + "'.", line) + ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) return True @@ -190,12 +210,43 @@ def Run( is_async # True if the script is running asynchronously ): - if len(split_line) > 2: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + split_line[1] + ", " + split_line[2] + ")") - ms.scroll(float(split_line[2]), float(split_line[1])) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + + if v2: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + split_line[1]) - ms.scroll(0, float(split_line[1])) + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if v2: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + + if v2: + ms.scroll(float(v2), float(v1)) + else: + ms.scroll(0, float(v1)) return idx+1 @@ -210,7 +261,7 @@ def Run( # class that defines the M_LINE command (draw a line?) class Mouse_Line(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("M_LINE") # the name of the command as you have to enter it in the code @@ -226,51 +277,33 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 5: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - if len(split_line) > 7: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - try: - temp = int(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' X1 value '" + split_line[1] + "' not valid.", line) - - try: - temp = int(split_line[2]) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y1 value '" + split_line[2] + "' not valid.", line) - - try: - temp = int(split_line[3]) - except: - if not variables.valid_var_name(split_line[3]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' X2 value '" + split_line[3] + "' not valid.", line) - - try: - temp = int(split_line[4]) - except: - if not variables.valid_var_name(split_line[4]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' Y2 value '" + split_line[4] + "' not valid.", line) - - if len(split_line) >= 6: - try: - temp = float(split_line[5]) - except: - if not variables.valid_var_name(split_line[5]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' wait value '" + split_line[5] + "' not valid.", line) - - if len(split_line) >= 7: - try: - temp = int(split_line[6]) - if temp == 0: - return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value cannot be zero.", line) - except: - if not variables.valid_var_name(split_line[6]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + self.name + "' skip value '" + split_line[6] + "' not valid.", line) + ret = variables.check_num(split_line, [4, 5, 6], idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_int_param(split_line, 1, "X1 value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 2, "Y1 value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 3, "X2 value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 4, "Y2 value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 5, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 6, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) + if ret != True: + return (ret, line) return True @@ -283,30 +316,93 @@ def Run( is_async # True if the script is running asynchronously ): - x1 = int(split_line[1]) - y1 = int(split_line[2]) - x2 = int(split_line[3]) - y2 = int(split_line[4]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + + v3 = variables.get_value(split_line[3], symbols) + if v3: + v3 = int(v3) + + v4 = variables.get_value(split_line[4], symbols) + if v4: + v4 = int(v4) + + if p > 4: + v5 = variables.get_value(split_line[5], symbols) + if v5: + v5 = int(v5) + else: + v5 = None + + if p > 5: + v6 = variables.get_value(split_line[6], symbols) + if v6: + v6 = int(v6) + else: + v6 = None delay = None - if len(split_line) > 5: - delay = float(split_line[5]) / 1000.0 + if v5: + delay = float(v5) / 1000.0 skip = 1 - if len(split_line) > 6: - skip = int(split_line[6]) + if v6: + skip = int(v6) if (delay == None) or (delay <= 0): print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line from (" + \ - split_line[1] + ", " + split_line[2] + ") to (" + \ - split_line[3] + ", " + split_line[4] + ") by " + str(skip) + " pixels per step") + str(v1) + ", " + str(v2) + ") to (" + \ + str(v3) + ", " + str(v4) + ") by " + str(skip) + " pixels per step") else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line from (" + \ - split_line[1] + ", " + split_line[2] + ") to (" + \ - split_line[3] + ", " + split_line[4] + ") by " + \ - str(skip) + " pixels per step and wait " + split_line[5] + " milliseconds between each step") + str(v1) + ", " + str(v2) + ") to (" + \ + str(v3) + ", " + str(v4) + ") by " + \ + str(skip) + " pixels per step and wait " + str(v6) + " milliseconds between each step") + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X1 value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y1 value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v3, idx, self.name, "X2 value", 3, split_line[3]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v4, idx, self.name, "Y2 value", 4, split_line[4]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 4: + ret = variables.validate_int_ge_zero(v5, idx, self.name, "Wait value", 5, split_line[5]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 5: + ret = variables.validate_int_ge_zero(v6, idx, self.name, "Skip value", 6, split_line[6]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 - points = ms.line_coords(x1, y1, x2, y2) + points = ms.line_coords(v1, v2, v3, v4) for x_M, y_M in points[::skip]: if check_kill(x, y, is_async): @@ -332,7 +428,7 @@ def Run( class Mouse_Line_Move(command_base.Command_Basic): def __init__( self, - ): + ): super().__init__("M_LINE_MOVE") # the name of the command as you have to enter it in the code @@ -347,36 +443,25 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' requires at least X and Y arguments.", line) - - try: - temp = int(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) - - try: - temp = int(split_line[2]) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) - - if len(split_line) >= 4: - try: - temp = float(split_line[3]) - except: - if not variables.valid_var_name(split_line[3]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) - - if len(split_line) >= 5: - try: - temp = int(split_line[4]) - if temp == 0: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) - except: - if not variables.valid_var_name(split_line[4]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + ret = variables.check_num(split_line, [2, 3, 4], idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 3, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 4, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) + if ret != True: + return (ret, line) return True @@ -389,28 +474,74 @@ def Run( is_async # True if the script is running asynchronously ): - x1 = int(split_line[1]) - y1 = int(split_line[2]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + + if p > 2: + v3 = variables.get_value(split_line[3], symbols) + if v3: + v3 = int(v3) + else: + v3 = None + + if p > 3: + v4 = variables.get_value(split_line[4], symbols) + if v4: + v4 = int(v3) + else: + v4 = None delay = None - if len(split_line) > 3: - delay = float(split_line[3]) / 1000.0 + if v3: + delay = float(v4) / 1000.0 skip = 1 - if len(split_line) > 4: - skip = int(split_line[4]) + if v4: + skip = int(v3) if (delay == None) or (delay <= 0): print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ - " pixels per step") + str(v1) + ", " + str(v2) + ") and wait " + str(v3) + " milliseconds between each step") else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ - " pixels per step and wait " + split_line[3] + " milliseconds between each step") + str(v1) + ", " + str(v2) + ") by " + str(v4) + \ + " pixels per step and wait " + str(v3) + " milliseconds between each step") + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 2: + ret = variables.validate_int_ge_zero(v3, idx, self.name, "Wait value", 3, split_line[3]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 3: + ret = variables.validate_int_gt_zero(v4, idx, self.name, "Skip value", 4, split_line[4]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 x_C, y_C = ms.get_pos() - x_N, y_N = x_C + x1, y_C + y1 + x_N, y_N = x_C + v1, y_C + v2 points = ms.line_coords(x_C, y_C, x_N, y_N) for x_M, y_M in points[::skip]: @@ -440,7 +571,7 @@ def __init__( ): super().__init__("M_LINE_SET") # the name of the command as you have to enter it in the code - + def Validate( self, idx: int, # The current line number @@ -452,37 +583,26 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' requires at least X and Y arguments.", line) - - try: - temp = int(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' X value '" + split_line[1] + "' not valid.", line) - - try: - temp = int(split_line[2]) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' Y value '" + split_line[2] + "' not valid.", line) - - if len(split_line) >= 4: - try: - temp = float(split_line[3]) - except: - if not variables.valid_var_name(split_line[3]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[3] + "' not valid.", line) - - if len(split_line) >= 5: - try: - temp = int(split_line[4]) - if temp == 0: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) - except: - if not variables.valid_var_name(split_line[4]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[4] + "' not valid.", line) + ret = variables.check_num(split_line, [2, 3, 4], idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 3, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 4, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) + if ret != True: + return (ret, line) + return True def Run( @@ -494,16 +614,38 @@ def Run( is_async # True if the script is running asynchronously ): - x1 = int(split_line[1]) - y1 = int(split_line[2]) + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + v2 = variables.get_value(split_line[2], symbols) + if v1: + v1 = int(v1) + + if p > 2: + v3 = variables.get_value(split_line[3], symbols) + if v3: + v3 = int(v3) + else: + v3 = None + + if p > 3: + v4 = variables.get_value(split_line[4], symbols) + if v4: + v4 = int(v4) + else: + v4 = None delay = None - if len(split_line) > 3: - delay = float(split_line[3]) / 1000.0 + if v3: + delay = float(v3) / 1000.0 skip = 1 - if len(split_line) > 4: - skip = int(split_line[4]) + if v4: + skip = int(v4) if (delay == None) or (delay <= 0): print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ @@ -514,6 +656,31 @@ def Run( split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ " pixels per step and wait " + split_line[3] + " milliseconds between each step") + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 2: + ret = variables.validate_int_gt_zero(v3, idx, self.name, "Wait value", 3, split_line[3]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 3: + ret = variables.validate_int_ge_zero(v4, idx, self.name, "Skip value", 4, split_line[4]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -554,21 +721,17 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 1: - try: - temp = float(split_line[1]) - except: - if not variables.valid_var_name(split_line[1]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' wait value '" + split_line[1] + "' not valid.", line) - - if len(split_line) > 2: - try: - temp = int(split_line[2]) - if temp == 0: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value cannot be zero.", line) - except: - if not variables.valid_var_name(split_line[2]): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' skip value '" + split_line[2] + "' not valid.", line) + ret = variables.check_num(split_line, [1, 2], idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_int_param(split_line, 1, "Wait value", idx, self.name, line, variables.validate_int_ge_zero) + if ret != True: + return (ret, line) + + ret = variables.check_int_param(split_line, 2, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) + if ret != True: + return (ret, line) return True @@ -581,15 +744,32 @@ def Run( is_async # True if the script is running asynchronously ): + ok = True + p = len(split_line) - 1 + + if p > 0: + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + else: + v1 = None + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + x1, y1 = symbols['m_pos'] delay = None - if len(split_line) > 1: - delay = float(split_line[1]) / 1000.0 + if v3: + delay = float(v3) / 1000.0 skip = 1 - if len(split_line) > 2: - skip = int(split_line[2]) + if v4: + skip = int(v4) if (delay == None) or (delay <= 0): print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ @@ -597,7 +777,22 @@ def Run( else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ " in a line by " + str(skip) + " pixels per step and wait " + \ - split_line[1] + " milliseconds between each step") + str(delay) + " milliseconds between each step") + + if p > 0: + ret = variables.validate_int_ge_zero(v1, idx, self.name, "Skip value", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if p > 3: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Wait value", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -628,8 +823,8 @@ def __init__( self, ): - super().__init__("M_STORE") # the name of the command as you have to enter it in the code - + super().__init__("M_STORE" ) # the name of the command as you have to enter it in the code + def Validate( self, idx: int, # The current line number @@ -641,8 +836,9 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes no arguments.", line) + ret = variables.check_num(split_line, [0], idx, line, self.name) + if ret != True: + return ret return True @@ -672,10 +868,11 @@ def Run( # class that defines the M_RECALL command class Mouse_Recall(command_base.Command_Basic): def __init__( - self, + self, ): - super().__init__("M_RECALL") # the name of the command as you have to enter it in the code + super().__init__("M_RECALL" ) # the name of the command as you have to enter it in the code + def Validate( self, @@ -688,8 +885,9 @@ def Validate( ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes no arguments.", line) + ret = variables.check_num(split_line, [0], idx, line, self.name) + if ret != True: + return ret return True diff --git a/variables.py b/variables.py index 184c9f9..9a7015b 100644 --- a/variables.py +++ b/variables.py @@ -68,44 +68,116 @@ def next_cmd(ret, cmds): # variable names should start with an alpha character def valid_var_name(v): return len(v) > 0 and ord(v[0].upper()) in range(ord('A'), ord('Z')+1) + + +# return a properly formatted error message +def error_msg(idx, name, desc, p, param, err): + ret = "Line:" + str(idx+1) + " -" + + if name: + ret += " '" + name + "'" + if desc: + ret += " " + desc + if p: + ret += " : param#" + str(p) + if param: + if p: + ret += " '" + param + "'" + else: + ret += " (" + param + ")" + if err: + ret += " " + err + + ret += "." + + return ret # check the number of variables allowed def check_num(split_line, lens, idx, line, name): n = len(split_line)-1 if n in lens: - return True # it's OK + return True - msg = "Line:" + str(idx+1) + " - Incorrect number of parameters (" + str(n) + ") supplied. " + # create a properly formatted error message if len(lens) == 0: - msg += "No valid number of parameters" - elif len(lens) == 1: - msg += str(lens[0]) + msg = "Has no valid number of parameters described. " + return (error_msg(idx, name, msg, None, None, "Please correct the definition"), line) + + msg = "Incorrect number of parameters" + if lens == [0]: + return (error_msg(idx, name, msg, str(n), "supplied. None are permitted"), line) else: - msg += ", ".join([str(el) for el in lens[0:-1]]) + ", " + str(lens[-1]) + cnt = "" + if len(lens) == 1: + cnt += str(lens[0]) + else: + cnt += ", ".join([str(el) for el in lens[0:-1]]) + ", " + str(lens[-1]) - msg += " required for command '" + name + "'." - - return (msg, line) + return (error_msg(idx, name, msg, None, str(n), "supplied, " + cnt + " are required"), line) + +# check an integer parameter +def check_int_param(split_line, p, desc, idx, name, line, validation=None, optional=False, var_ok=True): + temp = None -# check a parameter -def check_param(split_line, p, desc, idx, name, line, var_ok): + if p >= len(split_line): + if optional: + return True + else: + return (error_msg(idx, name, desc, p, None, 'required parameter not present'), line) + try: temp = int(split_line[p]) except: - if not (var_ok and valid_var_name(split_line[p])): # a variable is OK here - return ("Line:" + str(idx+1) + " - '" + name + "' " + desc + " '" + split_line[p] + "' not valid.", line) + if var_ok and valid_var_name(split_line[p]): # a variable is OK here + return True + return (error_msg(idx, name, desc, p, split_line[p], 'not valid'), line) + + if validation: + return validation(temp, idx, name, desc, p, split_line[p]) return True # get the value of a parameter -def get_value(n, symbols): - v = split_line[n] +def get_value(v, symbols): if valid_var_name(v): g_vars = symbols['g_vars'] with g_vars[0]: # lock the globals while we do this - v = variables.get(v, symbols['l_vars'], g_vars[1]) + v = get(v, symbols['l_vars'], g_vars[1]) return v + + +def validate_int_non_zero(v, idx, name, desc, p, param): + if v: + if int(float(v)) != 0: + return True + else: + return error_msg(idx, name, desc, p, param, 'must not be zero') + else: + return error_msg(idx, name, desc, p, param, 'must be an integer') + + +def validate_int_gt_zero(v, idx, name, desc, p, param): + if v: + if int(float(v)) > 0: + return True + else: + return error_msg(idx, name, desc, p, param, 'must be greater than zero') + else: + return error_msg(idx, name, desc, p, param, 'must be an integer') + + +def validate_int_ge_zero(v, idx, name, desc, p, param): + if v: + if int(float(v)) >= 0: + return True + else: + return error_msg(idx, name, desc, p, param, 'must not be less than zero') + else: + return error_msg(idx, name, desc, p, param, 'must be an integer') + + + \ No newline at end of file From 11df3cf4e71f52f04a79c90225bd8aa1d8913f43 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 10 Sep 2020 01:31:03 +0800 Subject: [PATCH 15/83] More refactoring to simplify the creation of new functions * Parameters are defined in a tuple * No need to code a Validate function in most cases. * Only implemented for the Mouse commands_mouse.py * Significant simplification of the Run command * NewCommands.md not updated yet. :-( --- command_base.py | 200 ++++++++++- commands_mouse.py | 900 +++++++++++++--------------------------------- variables.py | 47 ++- 3 files changed, 472 insertions(+), 675 deletions(-) diff --git a/command_base.py b/command_base.py index 50ac752..964d763 100644 --- a/command_base.py +++ b/command_base.py @@ -1,3 +1,26 @@ +import variables + + +# Constants for auto validation +P_DESCRIPTION = 0 +P_OPTIONAL = 1 +P_VAR_OK = 2 +P_TYPE = 3 +P_CONVERT = 4 +P_P1_VALIDATION = 5 +P_P2_VALIDATION = 6 + + +# constants for run state +R_INIT = 0 +R_GET = 1 +R_PRE_INFO = 2 +R_VALIDATE = 3 +R_INFO = 4 +R_RUN = 5 +R_FINAL = 6 + + # ################################################## # ### CLASS Command_Basic ### # ################################################## @@ -6,10 +29,21 @@ class Command_Basic: def __init__( self, - Name: str # The name of the command (what you put in the script) + Name: str, # The name of the command (what you put in the script) + Lib="LIB_UNSET", + Auto_validate=None ): self.name = Name + self.lib = Lib + self.auto_validate = Auto_validate + + self.valid_max_params = self.Calc_valid_max_params() + self.valid_num_params = self.Calc_valid_param_counts() + + self.run_states = [R_INIT, R_GET, R_PRE_INFO, R_VALIDATE, R_INFO, R_RUN, R_FINAL] + self.param = None + self.param_cnt = None def Validate( @@ -24,8 +58,21 @@ def Validate( # pass 2. For example, goto can be checked on pass 2 to ensure the label # exists ): + + ret = None + + if self.auto_validate: + if pass_no == 1: + ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols, pass_no) + ret = self.Validate_params(ret, P_P1_VALIDATION, idx, line, lines, split_line, symbols, pass_no) + + if pass_no == 2: + ret = self.Validate_params(ret, P_P2_VALIDATION, idx, line, lines, split_line, symbols, pass_no) - return ("", "") # error value! + if ret == None: + return ("", "") # error value! + + return ret def Parse( @@ -65,7 +112,146 @@ def Run( is_async ): - return idx+1 # just move to next line + ret = self.Run_params(None, idx, split_line, symbols, coords, is_async, 1) + + if ret == -1: + return ret + else: + return idx+1 + + + def Partial_run(self, idx, split_line, symbols, coords, is_async, run_subset): + ret = None + + try: + if R_INIT in run_subset: + pass + + if R_GET in run_subset: + ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 1) + if ret == -1: + return ret + + if R_PRE_INFO in run_subset: + print("[" + self.Lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + " parameters (" + str(v) + ")") + + if R_VALIDATE in run_subset: + ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 2) + if ret == -1: + return ret + + if R_INFO in run_subset: + print("[" + self.Lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + " values (" + str(v) + ")") + + if R_RUN in run_subset: + pass + + finally: + if R_FINAL in run_subset: + self.param = None + self.param_cnt = None + + return ret + + + def Calc_valid_max_params(self): + # Return the maximum number of parameters. We can calculate this simply based on the number defined + if self.auto_validate: + return len(self.auto_validate) + + return None + + + def Calc_valid_param_counts(self): + # Return a set of numbers of parameters that are acceptable. This is defined by which are optional + ret = None + + if self.auto_validate: + ret = [] + vn = len(self.auto_validate) + for i in range(vn): + i_val = self.auto_validate[i] + if (i_val[P_OPTIONAL] == True) or (i+1 == vn): + ret += [i+1] + + if ret: + return set(ret) + + return ret + + + def Validate_param_count(self, ret, idx, line, lines, split_line, symbols, pass_no): + if not (ret == None or ret == True): + return ret + + if pass_no == 1: + return variables.Check_num_params(split_line, self.valid_num_params, idx, line, self.name) + + return ret + + + def Validate_params(self, ret, val_const, idx, line, lines, split_line, symbols, pass_no): + if not (ret == None or ret == True): + return ret + + for i in range(self.valid_max_params): + ret = self.Validate_param_n(ret, i+1, val_const, idx, line, lines, split_line, symbols, pass_no) + if ret != True: + return ret + + return ret + + + def Validate_param_n(self, ret, n, val_const, idx, line, lines, split_line, symbols, pass_no): + if not (ret == None or ret == True): + return ret + + val = self.auto_validate[n-1] + + opt = self.valid_num_params == {} or (set(range(1,n)) & self.valid_num_params) != [] + + ret = variables.Check_generic_param(split_line, n, val[P_DESCRIPTION], idx, self.name, line, val[P_CONVERT], val[P_TYPE], val[val_const], val[P_OPTIONAL], val[P_VAR_OK]) + if ret == True or ret == None: + return True + + return (ret, line) + + + def Run_params(self, ret, idx, split_line, symbols, coords, is_async, pass_no): + if ret == None: + ret = True + + if pass_no == 1: + self.param = [self.name] + self.param_cnt = len(split_line) + + for i in range(self.valid_max_params): + if i < self.param_cnt: + self.param += [self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no)] + else: + self.param += [None] + elif pass_no == 2: + for i in range(self.valid_max_params): + if self.param[i+1] != None: + ret = self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no) + + return ret + + + def Run_param_n(self, ret, idx, n, split_line, symbols, coords, is_async, pass_no): + if pass_no == 1: + return variables.get_value(split_line[n], symbols) + elif pass_no == 2: + val = self.auto_validate[n-1] + ok = ret + + if val[P_P1_VALIDATION]: + ok = val[P_P1_VALIDATION](self.param[n], idx, self.name, val[P_DESCRIPTION], n, split_line[n]) + if ok != True: + print("[" + self.lib + "] " + coords[0] + " " + ok) + ret = -1 + + return ret # ################################################## @@ -77,11 +263,13 @@ class Command_Header(Command_Basic): def __init__( self, - Name: str, # The name of the command (what you put in the script) - Is_async: bool # is this async? + Name: str, # The name of the command (what you put in the script) + Is_async: bool, # is this async? + Lib="LIB_UNSET", + Auto_validate=None ): - super().__init__(Name) + super().__init__(Name, Lib, Auto_validate) self.is_async = Is_async def Validate( diff --git a/commands_mouse.py b/commands_mouse.py index 2e54237..b3ce418 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -1,4 +1,5 @@ import command_base, ms, scripts, variables +from command_base import R_INIT, R_GET, R_PRE_INFO, R_VALIDATE, R_INFO, R_RUN, R_FINAL lib = "cmds_mous" # name of this library (for logging) @@ -12,32 +13,11 @@ def __init__( self, ): - super().__init__("M_MOVE") # the name of the command as you have to enter it in the code + super().__init__("M_MOVE", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Y value", False, True, "integer", int, None, None) ) ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [2], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - return True def Run( self, @@ -48,36 +28,19 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 - - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - v2 = variables.get_value(split_line[2], symbols) - if v2: - v2 = int(v2) - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(v1) + ", " + \ - str(v2) + ")") - - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - ms.move_to_pos(float(v1), float(v2)) + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 + + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") + + ms.move_to_pos(float(self.param[1]), float(self.param[2])) - return idx+1 + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Move()) # register the command @@ -93,32 +56,11 @@ def __init__( self, ): - super().__init__("M_SET") # the name of the command as you have to enter it in the code + super().__init__("M_SET", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None) ) ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [2], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - return True def Run( self, @@ -129,36 +71,20 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 - - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - v2 = variables.get_value(split_line[2], symbols) - if v2: - v2 = int(v2) - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(v1) + ", " + \ - str(v2) + ")") + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(self.param[1]) + ", " + \ + str(self.param[2]) + ")") - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False + ms.set_pos(float(self.param[1]), float(self.param[2])) - if not ok: - return -1 + return idx+1 - ms.set_pos(float(v1), float(v2)) - - return idx+1 + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Set()) # register the command @@ -174,32 +100,12 @@ def __init__( self, ): - super().__init__("M_SCROLL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [1, 2], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) - if ret != True: - return (ret, line) + super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code + lib, + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Scroll amount", False, True, "integer", int, None, None) ) ) - return True def Run( self, @@ -210,45 +116,25 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 + + if self.param[2]: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - if p > 1: - v2 = variables.get_value(split_line[2], symbols) if v2: - v2 = int(v2) - else: - v2 = None - - if v2: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) - - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if v2: - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - if v2: - ms.scroll(float(v2), float(v1)) - else: - ms.scroll(0, float(v1)) + ms.scroll(float(self.param[2]), float(self.param[1])) + else: + ms.scroll(0, float(self.param[1])) - return idx+1 + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Scroll()) # register the command @@ -264,48 +150,15 @@ def __init__( self, ): - super().__init__("M_LINE") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [4, 5, 6], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "X1 value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) + super().__init__("M_LINE", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X1 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Y1 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("X2 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Y2 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - ret = variables.check_int_param(split_line, 2, "Y1 value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 3, "X2 value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 4, "Y2 value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 5, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 6, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) - if ret != True: - return (ret, line) - - return True def Run( self, @@ -316,105 +169,45 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 - - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - v2 = variables.get_value(split_line[2], symbols) - if v2: - v2 = int(v2) - - v3 = variables.get_value(split_line[3], symbols) - if v3: - v3 = int(v3) - - v4 = variables.get_value(split_line[4], symbols) - if v4: - v4 = int(v4) - - if p > 4: - v5 = variables.get_value(split_line[5], symbols) - if v5: - v5 = int(v5) - else: - v5 = None - - if p > 5: - v6 = variables.get_value(split_line[6], symbols) - if v6: - v6 = int(v6) - else: - v6 = None - - delay = None - if v5: - delay = float(v5) / 1000.0 - - skip = 1 - if v6: - skip = int(v6) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line from (" + \ - str(v1) + ", " + str(v2) + ") to (" + \ - str(v3) + ", " + str(v4) + ") by " + str(skip) + " pixels per step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line from (" + \ - str(v1) + ", " + str(v2) + ") to (" + \ - str(v3) + ", " + str(v4) + ") by " + \ - str(skip) + " pixels per step and wait " + str(v6) + " milliseconds between each step") - - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X1 value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y1 value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v3, idx, self.name, "X2 value", 3, split_line[3]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v4, idx, self.name, "Y2 value", 4, split_line[4]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 4: - ret = variables.validate_int_ge_zero(v5, idx, self.name, "Wait value", 5, split_line[5]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 5: - ret = variables.validate_int_ge_zero(v6, idx, self.name, "Skip value", 6, split_line[6]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - points = ms.line_coords(v1, v2, v3, v4) - - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: return -1 - ms.set_pos(x_M, y_M) + delay = None + if self.param[5]: + delay = float(self.param[5]) / 1000.0 + + skip = 1 + if self.param[6]: + skip = int(self.param[6]) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): + if (delay == None) or (delay <= 0): + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line from (" + \ + str(self.param[1]) + ", " + str(self.param[2]) + ") to (" + \ + str(self.param[3]) + ", " + str(self.param[4]) + ") by " + str(skip) + " pixels per step") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line from (" + \ + str(self.param[1]) + ", " + str(self.param[2]) + ") to (" + \ + str(self.param[3]) + ", " + str(self,param[4]) + ") by " + \ + str(skip) + " pixels per step and wait " + str(self.param[5]) + " milliseconds between each step") + + points = ms.line_coords(self.param[1], self.param[2], self.param[3], self.param[4]) + + for x_M, y_M in points[::skip]: + if check_kill(x, y, is_async): return -1 - return idx+1 + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, x, y, is_async): + return -1 + + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Line()) # register the command @@ -424,46 +217,19 @@ def Run( # ### CLASS Mouse_Line_Move ### # ################################################## -# class that defines the M_LINE_MOVE command +# class that defines the M_LINE_MOVE command class Mouse_Line_Move(command_base.Command_Basic): def __init__( - self, - ): - - super().__init__("M_LINE_MOVE") # the name of the command as you have to enter it in the code - - def Validate( self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [2, 3, 4], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 3, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) - if ret != True: - return (ret, line) + ): - ret = variables.check_int_param(split_line, 4, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) - if ret != True: - return (ret, line) + super().__init__("M_LINE_MOVE", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - return True def Run( self, @@ -474,87 +240,45 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 - - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - v2 = variables.get_value(split_line[2], symbols) - if v2: - v2 = int(v2) - - if p > 2: - v3 = variables.get_value(split_line[3], symbols) - if v3: - v3 = int(v3) - else: - v3 = None - - if p > 3: - v4 = variables.get_value(split_line[4], symbols) - if v4: - v4 = int(v3) - else: - v4 = None - - delay = None - if v3: - delay = float(v4) / 1000.0 - - skip = 1 - if v4: - skip = int(v3) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - str(v1) + ", " + str(v2) + ") and wait " + str(v3) + " milliseconds between each step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - str(v1) + ", " + str(v2) + ") by " + str(v4) + \ - " pixels per step and wait " + str(v3) + " milliseconds between each step") - - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 2: - ret = variables.validate_int_ge_zero(v3, idx, self.name, "Wait value", 3, split_line[3]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 3: - ret = variables.validate_int_gt_zero(v4, idx, self.name, "Skip value", 4, split_line[4]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - x_C, y_C = ms.get_pos() - x_N, y_N = x_C + v1, y_C + v2 - points = ms.line_coords(x_C, y_C, x_N, y_N) - - for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: return -1 - ms.set_pos(x_M, y_M) + delay = None + if self.param[3]: + delay = float(self.param[3]) / 1000.0 + + skip = 1 + if self.param[4]: + skip = int(self.param[4]) + + if (delay == None) or (delay <= 0): + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ + str(self.param[1]) + ", " + str(self.param[2]) + ") and wait " + str(self.param[3]) + " milliseconds between each step") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ + str(self.param[1]) + ", " + str(self.param[2]) + ") by " + str(self.param[4]) + \ + " pixels per step and wait " + str(self.param[3]) + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + x_N, y_N = x_C + self.param[1], y_C + self.param[2] + points = ms.line_coords(x_C, y_C, x_N, y_N) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): return -1 - return idx+1 + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Line_Move()) # register the command @@ -564,46 +288,19 @@ def Run( # ### CLASS Mouse_Line_Set ### # ################################################## -# class that defines the M_LINE_SET command +# class that defines the M_LINE_SET command class Mouse_Line_Set(command_base.Command_Basic): def __init__( - self, - ): - - super().__init__("M_LINE_SET") # the name of the command as you have to enter it in the code - - def Validate( self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [2, 3, 4], idx, line, self.name) - if ret != True: - return ret + super().__init__("M_LINE_SET", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ + ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 2, "Y value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 3, "Wait value", idx, self.name, line, variables.validate_int_ge_zero, True) - if ret != True: - return (ret, line) - - ret = variables.check_int_param(split_line, 4, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) - if ret != True: - return (ret, line) - - return True def Run( self, @@ -614,85 +311,43 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 - - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - - v2 = variables.get_value(split_line[2], symbols) - if v1: - v1 = int(v1) - - if p > 2: - v3 = variables.get_value(split_line[3], symbols) - if v3: - v3 = int(v3) - else: - v3 = None - - if p > 3: - v4 = variables.get_value(split_line[4], symbols) - if v4: - v4 = int(v4) - else: - v4 = None - - delay = None - if v3: - delay = float(v3) / 1000.0 - - skip = 1 - if v4: - skip = int(v4) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ - split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ - " pixels per step") - else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ - split_line[1] + ", " + split_line[2] + ") by " + str(skip) + \ - " pixels per step and wait " + split_line[3] + " milliseconds between each step") - - ret = variables.validate_int_ge_zero(v1, idx, self.name, "X value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Y value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 2: - ret = variables.validate_int_gt_zero(v3, idx, self.name, "Wait value", 3, split_line[3]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 3: - ret = variables.validate_int_ge_zero(v4, idx, self.name, "Skip value", 4, split_line[4]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, x1, y1) - - for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): + + delay = None + if self.params[3]: + delay = float(self.params[3]) / 1000.0 + + skip = 1 + if self.params[4]: + skip = int(self.params[4]) + + if (delay == None) or (delay <= 0): + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ + self.params[1] + ", " + self.params[2] + ") by " + str(skip) + \ + " pixels per step") + else: + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ + self.params[1] + ", " + self.params[2] + ") by " + str(skip) + \ + " pixels per step and wait " + self.params[3] + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + points = ms.line_coords(x_C, y_C, x1, y1) + + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): return -1 + ms.set_pos(x_M, y_M) + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 - return idx+1 + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Line_Set()) # register the command @@ -702,38 +357,17 @@ def Run( # ### CLASS Mouse_Recall_Line ### # ################################################## -# class that defines the M_RECALL_LINE command +# class that defines the M_RECALL_LINE command class Mouse_Recall_Line(command_base.Command_Basic): def __init__( - self, - ): - - super().__init__("M_RECALL_LINE") # the name of the command as you have to enter it in the code - - def Validate( self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [1, 2], idx, line, self.name) - if ret != True: - return ret - - ret = variables.check_int_param(split_line, 1, "Wait value", idx, self.name, line, variables.validate_int_ge_zero) - if ret != True: - return (ret, line) + super().__init__("M_RECALL_LINE", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("Wait value", True , True, "integer", int, variables.Validate_ge_zero, None), \ + ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - ret = variables.check_int_param(split_line, 2, "Skip value", idx, self.name, line, variables.validate_int_gt_zero, True) - if ret != True: - return (ret, line) - - return True def Run( self, @@ -744,70 +378,46 @@ def Run( is_async # True if the script is running asynchronously ): - ok = True - p = len(split_line) - 1 + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 - if p > 0: - v1 = variables.get_value(split_line[1], symbols) - if v1: - v1 = int(v1) - else: - v1 = None + x1, y1 = symbols['m_pos'] - if p > 1: - v2 = variables.get_value(split_line[2], symbols) - if v2: - v2 = int(v2) - else: - v2 = None - - x1, y1 = symbols['m_pos'] - - delay = None - if v3: - delay = float(v3) / 1000.0 - - skip = 1 - if v4: - skip = int(v4) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ - " in a line by " + str(skip) + " pixels per step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ - " in a line by " + str(skip) + " pixels per step and wait " + \ - str(delay) + " milliseconds between each step") - - if p > 0: - ret = variables.validate_int_ge_zero(v1, idx, self.name, "Skip value", 1, split_line[1]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if p > 3: - ret = variables.validate_int_ge_zero(v2, idx, self.name, "Wait value", 2, split_line[2]) - if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) - ok = False - - if not ok: - return -1 - - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, x1, y1) - - for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): - return -1 + delay = 0 + if self.params[3]: + delay = float(self.params[3]) / 1000.0 - ms.set_pos(x_M, y_M) + skip = 1 + if self.params[4]: + skip = int(self.params[4]) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): + if (delay == None) or (delay <= 0): + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ + " in a line by " + str(skip) + " pixels per step") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ + " in a line by " + str(skip) + " pixels per step and wait " + \ + str(delay) + " milliseconds between each step") + + x_C, y_C = ms.get_pos() + points = ms.line_coords(x_C, y_C, x1, y1) + + for x_M, y_M in points[::skip]: + if check_kill(coords[1], coords[2], is_async): return -1 - return idx+1 + ms.set_pos(x_M, y_M) + + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[1], coords[2], is_async): + return -1 + + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Recall_Line()) # register the command @@ -817,30 +427,13 @@ def Run( # ### CLASS Mouse_Store ### # ################################################## -# class that defines the M_STORE command +# class that defines the M_STORE command class Mouse_Store(command_base.Command_Basic): def __init__( - self, - ): - - super().__init__("M_STORE" ) # the name of the command as you have to enter it in the code - - def Validate( self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [0], idx, line, self.name) - if ret != True: - return ret - - return True + super().__init__("M_STORE", lib, () ) # the name of the command as you have to enter it in the code def Run( self, @@ -851,11 +444,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Store mouse position") + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 + + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Store mouse position") - symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. + symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. - return idx+1 + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Store()) # register the command @@ -865,31 +466,14 @@ def Run( # ### CLASS Mouse_Recall ### # ################################################## -# class that defines the M_RECALL command +# class that defines the M_RECALL command class Mouse_Recall(command_base.Command_Basic): def __init__( self, ): - super().__init__("M_RECALL" ) # the name of the command as you have to enter it in the code - - - def Validate( - self, - idx: int, - line, - lines, - split_line, - symbols, - pass_no - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - ret = variables.check_num(split_line, [0], idx, line, self.name) - if ret != True: - return ret + super().__init__("M_RECALL", lib, () ) # the name of the command as you have to enter it in the code - return True def Run( self, @@ -900,15 +484,21 @@ def Run( is_async # True if the script is running asynchronously ): - if symbols['m_pos'] == tuple(): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") - else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols['m_pos'])) - ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) - - return idx+1 + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + return -1 + if symbols['m_pos'] == tuple(): + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + else: + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols['m_pos'])) + ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) -scripts.add_command(Mouse_Recall()) # register the command + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) +scripts.add_command(Mouse_Recall()) # register the command diff --git a/variables.py b/variables.py index 9a7015b..41eb37e 100644 --- a/variables.py +++ b/variables.py @@ -93,8 +93,8 @@ def error_msg(idx, name, desc, p, param, err): return ret -# check the number of variables allowed -def check_num(split_line, lens, idx, line, name): +# check the number of parameters allowed +def Check_num_params(split_line, lens, idx, line, name): n = len(split_line)-1 if n in lens: return True @@ -117,8 +117,30 @@ def check_num(split_line, lens, idx, line, name): return (error_msg(idx, name, msg, None, str(n), "supplied, " + cnt + " are required"), line) -# check an integer parameter -def check_int_param(split_line, p, desc, idx, name, line, validation=None, optional=False, var_ok=True): +# check a generic parameter +def Check_generic_param(split_line, p, desc, idx, name, line, conv, conv_name, validation=None, optional=False, var_ok=True): + temp = None + + if p >= len(split_line): + if optional: + return True + else: + return (error_msg(idx, name, desc, p, None, 'required ' + conv_name + ' parameter not present'), line) + + try: + temp = conv(split_line[p]) + except: + if var_ok and valid_var_name(split_line[p]): # a variable is OK here + return True + return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + conv_name), line) + + if validation: + return validation(temp, idx, name, desc, p, split_line[p]) + + return True + + +def Check_numeric_param(split_line, p, desc, idx, name, line, validation, optional=False, var_ok=True): temp = None if p >= len(split_line): @@ -128,7 +150,7 @@ def check_int_param(split_line, p, desc, idx, name, line, validation=None, optio return (error_msg(idx, name, desc, p, None, 'required parameter not present'), line) try: - temp = int(split_line[p]) + temp = conv(split_line[p]) except: if var_ok and valid_var_name(split_line[p]): # a variable is OK here return True @@ -150,9 +172,9 @@ def get_value(v, symbols): return v -def validate_int_non_zero(v, idx, name, desc, p, param): +def Validate_non_zero(v, idx, name, desc, p, param): if v: - if int(float(v)) != 0: + if float(v) != 0: return True else: return error_msg(idx, name, desc, p, param, 'must not be zero') @@ -160,9 +182,9 @@ def validate_int_non_zero(v, idx, name, desc, p, param): return error_msg(idx, name, desc, p, param, 'must be an integer') -def validate_int_gt_zero(v, idx, name, desc, p, param): +def Validate_gt_zero(v, idx, name, desc, p, param): if v: - if int(float(v)) > 0: + if v > 0: return True else: return error_msg(idx, name, desc, p, param, 'must be greater than zero') @@ -170,14 +192,11 @@ def validate_int_gt_zero(v, idx, name, desc, p, param): return error_msg(idx, name, desc, p, param, 'must be an integer') -def validate_int_ge_zero(v, idx, name, desc, p, param): +def Validate_ge_zero(v, idx, name, desc, p, param): if v: - if int(float(v)) >= 0: + if v >= 0: return True else: return error_msg(idx, name, desc, p, param, 'must not be less than zero') else: return error_msg(idx, name, desc, p, param, 'must be an integer') - - - \ No newline at end of file From e41c5e1f343975a35d17fa5bf5814ffefb0a6974 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 10 Sep 2020 14:22:00 +0800 Subject: [PATCH 16/83] Improved documentation for creating new commands. * mostly changes to NewCommands.md * some bug fixes etc discovered while documenting --- NewCommands.md | 580 +++++++++++++++++++++++++++++++++++++++++++++- command_base.py | 16 +- commands_mouse.py | 54 +++-- 3 files changed, 617 insertions(+), 33 deletions(-) diff --git a/NewCommands.md b/NewCommands.md index a7b2c96..e76ac0d 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -19,13 +19,535 @@ If you are contemplating adding a new command to an existing module, the process You should make yourself familiar with the way existing commands work. Look in one of the `commands_*.py` files (other than `commands_header.py`) for examples of existing commands. -As an example, We'll use the `M_SCROLL` command. This can be found in the `commands_mouse.py` module. +You can take advantage of several levels of abstraction when writing your own commands. Currently there are five obvious options: -Recent changes to add variable support to commands has increased the complexity of each command. Never fear, I have plans to make this even simpler than the previous version, with little to no validation coding needed in the near future. + * Declarative definition with *no* coding (Just teasing -- this one doesn't exist yet) + * Declarative definition, with minimal coding + * Declarative definition, with manual control over parameter handling + * Non-declarative definition, with manual coding of parameter handling + * Completely roll-your-own -### Decoding the MOUSE_SCROLL command +### Declarative definition with *no* coding + +This is where I'm aiming to get. You will still need to write some code to perform the actual function, but you can do so by just writing that code, and all the rest is handled for you. (I'm doing all this work because I'm lazy, and I want you to be able to be lazy too!) + +#### Requirements: + * Declare your parameters + * Declare your output message format(s) + * Define a method to perform the function on prevalidated parameters and include this method in the definition + * Register your command + +#### Benefits: + * NO individual Validate method + * NO individual Run method + * Optional parameters supported + * Allows use of internally managed numeric variables as well as literal values + * Minimal debugging needed + * Easily understandable, without needing to read code + * Unlikely to introduce maintenance overhead + +#### Costs: + * Parameters must conform to "standard" declarations + * Less flexible execution of command + +#### An example? + +There will be one as soon as this is developed! + +### Declarative definition, with minimal coding + +Currently working, this involves declarative definition of the parameters, with manual control over the stages of execution of the Run method. + +This has a shorter list of requirements, but it's more coding, and requires more knowledge. + +#### Requirements: + * Declare your parameters + * Write a basic Run method calling the stages of execution around your output messages and command functionality + * Register your command + +#### Benefits: + * NO individual Validate method + * Optional parameters supported + * Allows use of internally managed numeric variables as well as literal values + * Low debugging overhead + * Easily understandable after viewing limited code + * Slight risk of maintenance overhead + +#### Costs: + * Parameters must conform to "standard" declarations + * Required basic understanding of stages of execution of a command + + +#### An example - Decoding an old version of the MOUSE_SCROLL command The `M_SCROLL` command looks like this: +```python + def __init__( + self, + ): + + super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Scroll amount", False, True, "integer", int, None, None) ) ) + + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + return -1 + + if self.param[2]: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) + + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + + if self.param[2]: + ms.scroll(float(self.param[2]), float(self.param[1])) + else: + ms.scroll(0, float(self.param[1])) + + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) + + +scripts.add_command(Mouse_Scroll()) # register the command +``` + +We will examine the 6 parts you need to consider + +##### Part 1 - The Class Header + +Each command is defined as a class. The Class Header defines that new class, and some of its important properties. + +The class should always begin with some documentation to both highlight the start of the definition of a new class, and also to inform people what the command is supposed to do. The documentation in this example is minimal rather than optimal. + +```python +# ################################################## +# ### CLASS Mouse_Scroll ### +# ################################################## + +# class that defines the M_SCROLL command (???) +``` + +##### Part 2 - The Class definition + +The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. + +It is also important that you define a unique name for your new command. I recommend using *Module*_*Command*, where *Module* is the part of the module name after `commands_`. + +```python +class Mouse_Scroll(command_base.Command_Basic): +``` + +##### Part 3 - Class Initialization + +The initialization of a command class serves to define the name of the command. This is also the place you define your parameters. + +```python + def __init__( + self, + ): + + super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Scroll amount", False, True, "integer", int, None, None) ) ) +``` + +The 5th line defines the name of the command and also passes the name of the current library (lib) to the the object. Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. The current library will be used to define where the command originates from in some of the low level reporting functions. + +Lines 7 and 8 define the two parameters expected for this function. Note that this definition actually commences on line 5 with the opening parenthesis. + +7 values must be entered in each tuple. The parameters are: + * The name of the parameter -- this will be used in multiple places to create messages that are understandable. + * Is this parameter optional? -- Entering True here means that it is valid to pass all the previous parameters, but stop here. Entering False means that if you have supplied one fewer than this parameter, then you must also supply this one. + * Can a variable be substituted? - Entering True allows a variable name to be used here instead of a literal value. Entering False means that only literal values are permitted. Note that literal values are validated before execution, variables are validated at execution. + * What is the type of the parameter -- This is a human-readable description of the datatype required. It is used in messages. + * Conversion function to desired type -- as an example used here, "int()" is used to convert strings (and other types) to integers. Other functions, like float, or str can be used. Note that this is an actual function name, so it is acceptable to define your own function if needed. + * Pass 1 Validation - this is a function used to perform pass 1 evaluation of the parameter value. This will be called at validation for literal values, and at execution for variable values. Examples of this are variables.Validate_ge_zero, a function that validates a numeric parameter value is greater than or equal to zero. Use None if no validation is required. Write your own pass 1 validation routine if you wish! + * Pass 2 Validation - this is a function called only on pass 2 of the validation. It is required for commands that reference labels (to determine if the label exists). None are defined yet, but that will change... + +##### Part 4 - Command Validation + +No code is required here! The default validation code for the class can handle it all for you! + +##### Part 5 - Command Execution + +```python + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + params = [idx, split_line, symbols, coords, is_async] + try: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + return -1 + + if self.param[2]: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) + + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + + if self.param[2]: + ms.scroll(float(self.param[2]), float(self.param[1])) + else: + ms.scroll(0, float(self.param[1])) + + return idx+1 + + finally: + self.Partial_run(*params, [R_FINAL]) +``` + +The first 8 lines are the standard header and should be copied verbatim. + +The definition of "params" on line 10 is simply a shortcut so we don't have to type the exact same set of parameters over and over again for various management functions that need to know this stuff. + +The main code is placed inside a TRY...FINALLY block to ensure that the finalization code is called. Currently this doesn't do a lot, but as it may become more necessary in later versions, it is recommended that you code the execution in this manner. + +Within the TRY...FINALLY block, we call "self.Partial_run()" to execute parts of the standard execution flow. The parts you can call are: + * R_INIT - Perform any initialization required (should ALWAYS be called) + * R_GET - Get the parameters, and evaluates variables (not strictly required for a command with no parameters, but STRONGLY encouraged) + * R_INFO - Display the command with the values. In most cases you'll want to implement this yourself as the default is pretty basic + * R_VALIDATE - Perform run-time validation of them, possibly printing messages. (not requied if you're not using variables, but STRONGLY encouraged) + * R_RUN - Perform the standard process of running the command. This should NEVER be used in this situation, as you will code this part! + * R_FINAL - Perform any finalization required. (should ALWAYS be called - in the FINALLY) + +In this example, we are first running R_INIT, and R_GET. If a Partial_run returns -1, you should return immediately with -1. + +After the R_GET, you can refer to self.params[n] where n is from 0 to the maximum number of parameters. self.params[0] is the command name, and self.params[1] to self.params[n] are the parameters from 1 to n. At this point, optional parameters that have not been passed will be None, literal values will be validated and be the correct type, and variable values will be thethe correct types, but not yet validated. + +After performing the 3 initial steps, we do our own implementation of the R_INFO. + +Next, the R_VALIDATE step is performed. After this, variable values will be validated. + +After validation, we do what is required based on the parameters passed, and return idx + 1 (that's the next line). If we had a serious error we could return -1 to abort the script. Instead of returning idx+1, we could possibly just return the value of the R_RUN process, however this requires more code, and introduces additional complexities. Just do the simple thing and return the correct value! + +No matter how we return, the FINALLY block will ensure that the finalization is called. + +##### Part 6 - Command Integration + +The final step is to include code to incorporate this command into the set of commands available for scripts. + +```python +scripts.add_command(Mouse_Scroll()) # register the command +``` + +This line creates a command object, and passes it to the routine which adds it to the list of available commands. + +It is important to note that the definition of more than one command with exactly the same name will result in only the second one being available. + + +### Declarative definition, with manual control over parameter handling + +More flexible, and required if the variables are not a fixed type (e.g. string followed by a number, or just a number) + +#### Requirements: + * Declare your variables + * Write a Validate method using the mid-level parameter support functions + * Write a Run method using the mid-level parameter support functions + * Register your command + +#### Benefits: + * More flexibility with parameters + +#### Costs: + * More coding required + * Requires knowledge of the command framework to understand + * Greater understanding of internal structures needed. + * Greater probability of introducing bugs and having unexpected behaviour to the command + * Small risk of maintenance overhead + +#### An example - Decoding an old version of the MOUSE_SCROLL command + +The `M_SCROLL` command once looked like this: +```python +# ################################################## +# ### CLASS Mouse_Scroll ### +# ################################################## + +# class that defines the M_SCROLL command (???) +class Mouse_Scroll(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("M_SCROLL", ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Scroll amount", False, True, "integer", int, None, None) ) ) + + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if not self.auto_validate or len(self.auto_validate) != 2: + return ("Invalid command setup", line) + + ret = True + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + ret = variables.check_num(split_line, {1, 2}, idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_numeric_param(split_line, 1, self.auto_validate[1][0], idx, self.name, line, variables.validate_ge_zero, self.auto_validate[1][1], self.auto_validate[1][2]) + if ret != True: + return (ret, line) + + ret = variables.check_generic_param(split_line, 2, self.auto_validate[1][0], idx, self.name, line, self.auto_validate[1][4], self.auto_validate[1][3], None, False, True) + if ret != True: + return (ret, line) + + return ret + + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + + if v2: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if v2: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + + if v2: + ms.scroll(float(v2), float(v1)) + else: + ms.scroll(0, float(v1)) + + return idx+1 + + +scripts.add_command(Mouse_Scroll()) # register the command +``` + +We will examine the 6 parts you need to consider + +##### Part 1 - The Class Header + +Each command is defined as a class. The Class Header defines that new class, and some of its important properties. + +The class should always begin with some documentation to both highlight the start of the definition of a new class, and also to inform people what the command is supposed to do. The documentation in this example is minimal rather than optimal. + +```python +# ################################################## +# ### CLASS Mouse_Scroll ### +# ################################################## + +# class that defines the M_SCROLL command (???) +``` + +##### Part 2 - The Class definition + +The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. + +It is also important that you define a unique name for your new command. I recommend using *Module*_*Command*, where *Module* is the part of the module name after `commands_`. + +```python +class Mouse_Scroll(command_base.Command_Basic): +``` + +##### Part 3 - Class Initialization + +The initialization of a command class serves to define the name of the command. This is literally what you need to place inside your script. + + def __init__( + self, + ): + + super().__init__("M_SCROLL", ( # the name of the command as you have to enter it in the code + # Desc Opt Var type conv p1_val p2_val + ("X value", False, True, "integer", int, None, None), \ + ("Scroll amount", False, True, "integer", int, None, None) ) ) +``` + +Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. + +##### Part 4 - Command Validation + +```python + def Validate( + self, + idx: int, # The current line number + line, # The current line + lines, # The current script + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if not self.auto_validate or len(self.auto_validate) != 2: + return ("Invalid command setup", line) + + ret = True + + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + ret = variables.check_num(split_line, {1, 2}, idx, line, self.name) + if ret != True: + return ret + + ret = variables.check_numeric_param(split_line, 1, self.auto_validate[1][0], idx, self.name, line, variables.validate_ge_zero, self.auto_validate[1][1], self.auto_validate[1][2]) + if ret != True: + return (ret, line) + + ret = variables.check_generic_param(split_line, 2, self.auto_validate[1][0], idx, self.name, line, self.auto_validate[1][4], self.auto_validate[1][3], None, False, True) + if ret != True: + return (ret, line) + + return ret +``` + +##### Part 5 - Command Execution + +```python + def Run( + self, + idx: int, # The current line number + split_line, # The current line, split + symbols, # The symbol table (a dictionary containing labels, loop counters etc.) + coords, # Tuple of printable coords as well as the individual x and y values + is_async # True if the script is running asynchronously + ): + + ok = True + p = len(split_line) - 1 + + v1 = variables.get_value(split_line[1], symbols) + if v1: + v1 = int(v1) + + if p > 1: + v2 = variables.get_value(split_line[2], symbols) + if v2: + v2 = int(v2) + else: + v2 = None + + if v2: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(v1) + ", " + str(v2) + ")") + else: + print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(v1)) + + ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if v2: + ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) + if ret != True: + print("[" + lib + "] " + coords[0] + " " + ret) + ok = False + + if not ok: + return -1 + + if v2: + ms.scroll(float(v2), float(v1)) + else: + ms.scroll(0, float(v1)) + + return idx+1 + + +scripts.add_command(Mouse_Scroll()) # register the command +``` + +##### Part 6 - Command Integration + +The final step is to include code to incorporate this command into the set of commands available for scripts. + +```python +``` + +This line creates a command object, and passes it to the routine which adds it to the list of available commands. + +It is important to note that the definition of more than one command with exactly the same name will result in only the second one being available. + + +### Non-declarative definition, with manual coding of parameter handling + +#### Requirements: + * Manual coding of 2 pass Validation method, with support from low level parameter access & validation + * Manual coding of Run method using low-level variable access and validation. + * Register your command + +#### Benefits: + * Huge flexibility. + * Still operates in a standard environment. + +#### Costs: + * Requires detailed knowledge of the command framework to understand + * Sound understanding of internal structures. + * Sound understanding of how commands are validated and executed + * Lots of debugging (but bugs limited to scope of command) + * Potential maintenance overhead + +#### An example - Decoding an old version of the MOUSE_SCROLL command + +The `M_SCROLL` command once looked like this: ```python # ################################################## @@ -118,9 +640,9 @@ class Mouse_Scroll(command_base.Command_Basic): scripts.add_command(Mouse_Scroll()) # register the command ``` -We will examine the 5 parts you need to consider +We will examine the 6 parts you need to consider -#### Part 1 - The Class Header +##### Part 1 - The Class Header Each command is defined as a class. The Class Header defines that new class, and some of its important properties. @@ -134,7 +656,7 @@ The class should always begin with some documentation to both highlight the star # class that defines the M_SCROLL command (???) ``` -#### Part 2 - The Class definition +##### Part 2 - The Class definition The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. @@ -146,7 +668,7 @@ class Mouse_Scroll(command_base.Command_Basic): In this example, the `M_SCROLL` command is contained in the `commands_mouse.py` module, so the name of the class should be `Mouse_M_Scroll`, but I've called it `Mouse_Scroll` because every mouse command starts with "M_". I will probably change this at some point. -#### Part 3 - Class Initialization +##### Part 3 - Class Initialization The initialization of a command class serves to define the name of the command. This is literally what you need to place inside your script. @@ -160,7 +682,7 @@ The initialization of a command class serves to define the name of the command. Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. -#### Part 4 - Command Validation +##### Part 4 - Command Validation Every command requires a validation. If you do not provide validation code, the ancestor class will return a blank error message when this command is encountered. @@ -175,6 +697,20 @@ Every command requires a validation. If you do not provide validation code, the pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions + # check number of split_line + if len(split_line) != 2: + return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.ame, line) + + try: + temp = int(split_line[1]) + if valid_var_name(temp): + + if temp < 1: + return ("Line:" + str(idx+1) + " - '" + split_line[0] + " parameter 1 must be a positive number.", line) + except: + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions ret = variables.check_num(split_line, [1, 2], idx, line, self.name) if ret != True: @@ -205,7 +741,7 @@ Finally, the method should return `True` if there were no errors. The symbol table (`symbols`) is a structure that you need to understand if you are writing more complex commands. -#### Part 5 - Command Execution +##### Part 5 - Command Execution Every command that does something (e.g. not labels - that have their effect during pass 1 of validation, or comments - that have no function) requires code to run it. If you do not provide `Run` code, the ancestor class will do nothing (i.e., it does not generate an error). @@ -276,7 +812,7 @@ Finally, the work of the function is performed on the validated values. idx + 1 is returned. This points to the nect line to execute, which in this instance is the next line (flow control commands may return different values). -#### Part 5 - Command Integration +##### Part 6 - Command Integration The final step is to include code to incorporate this command into the set of commands available for scripts. @@ -288,6 +824,30 @@ This line creates a command object, and passes it to the routine which adds it t It is important to note that the definition of more than one command with exactly the same name will result in only the second one being available. + +### Completely roll-your-own + +This requires the most knowledge, creates the possibility of the worst sort of bugs, may be hard to maintain, but is ultimately the most flexible way to add a command because you don't have to follow any rules. + +Please consider using an earlier option if you can. + +Requirements + * Expert understanding of LPHK + * Decisions as to how you will integrate with existing commands + * Patience + +Benefits: + * Anything goes + +Costs: + * Requires python-fu to understand + * Potential high maintenance overhead + * High risk of introducing bugs outside the scope of the current command + +### An example? + +Sorry, if you're going to roll your own, there is really no model to follow. + ## Headers Headers and Commands are very similar. The basic format of creating them is the same, however there are some important differences. This section will concentrate mostly on those differences. diff --git a/command_base.py b/command_base.py index 964d763..a5c1cf8 100644 --- a/command_base.py +++ b/command_base.py @@ -14,11 +14,10 @@ # constants for run state R_INIT = 0 R_GET = 1 -R_PRE_INFO = 2 +R_INFO = 2 R_VALIDATE = 3 -R_INFO = 4 -R_RUN = 5 -R_FINAL = 6 +R_RUN = 4 +R_FINAL = 5 # ################################################## @@ -41,7 +40,7 @@ def __init__( self.valid_max_params = self.Calc_valid_max_params() self.valid_num_params = self.Calc_valid_param_counts() - self.run_states = [R_INIT, R_GET, R_PRE_INFO, R_VALIDATE, R_INFO, R_RUN, R_FINAL] + self.run_states = [R_INIT, R_GET, R_INFO, R_VALIDATE, R_RUN, R_FINAL] self.param = None self.param_cnt = None @@ -112,7 +111,7 @@ def Run( is_async ): - ret = self.Run_params(None, idx, split_line, symbols, coords, is_async, 1) + ret = self.Partial_run(idx, split_line, symbols, coords, is_async, self.run_states) if ret == -1: return ret @@ -132,7 +131,7 @@ def Partial_run(self, idx, split_line, symbols, coords, is_async, run_subset): if ret == -1: return ret - if R_PRE_INFO in run_subset: + if R_INFO in run_subset: print("[" + self.Lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + " parameters (" + str(v) + ")") if R_VALIDATE in run_subset: @@ -140,9 +139,6 @@ def Partial_run(self, idx, split_line, symbols, coords, is_async, run_subset): if ret == -1: return ret - if R_INFO in run_subset: - print("[" + self.Lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + " values (" + str(v) + ")") - if R_RUN in run_subset: pass diff --git a/commands_mouse.py b/commands_mouse.py index b3ce418..489c808 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -1,5 +1,5 @@ import command_base, ms, scripts, variables -from command_base import R_INIT, R_GET, R_PRE_INFO, R_VALIDATE, R_INFO, R_RUN, R_FINAL +from command_base import R_INIT, R_GET, R_INFO, R_VALIDATE, R_RUN, R_FINAL lib = "cmds_mous" # name of this library (for logging) @@ -30,11 +30,14 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + ms.move_to_pos(float(self.param[1]), float(self.param[2])) return idx+1 @@ -73,12 +76,15 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 - + print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(self.param[1]) + ", " + \ str(self.param[2]) + ")") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + ms.set_pos(float(self.param[1]), float(self.param[2])) return idx+1 @@ -101,7 +107,6 @@ def __init__( ): super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code - lib, # Desc Opt Var type conv p1_val p2_val ("X value", False, True, "integer", int, None, None), \ ("Scroll amount", False, True, "integer", int, None, None) ) ) @@ -118,7 +123,7 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 if self.param[2]: @@ -126,7 +131,10 @@ def Run( else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - if v2: + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + + if self.param[2]: ms.scroll(float(self.param[2]), float(self.param[1])) else: ms.scroll(0, float(self.param[1])) @@ -171,7 +179,7 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 delay = None @@ -192,6 +200,9 @@ def Run( str(self.param[3]) + ", " + str(self,param[4]) + ") by " + \ str(skip) + " pixels per step and wait " + str(self.param[5]) + " milliseconds between each step") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + points = ms.line_coords(self.param[1], self.param[2], self.param[3], self.param[4]) for x_M, y_M in points[::skip]: @@ -242,7 +253,7 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 delay = None @@ -261,6 +272,9 @@ def Run( str(self.param[1]) + ", " + str(self.param[2]) + ") by " + str(self.param[4]) + \ " pixels per step and wait " + str(self.param[3]) + " milliseconds between each step") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + x_C, y_C = ms.get_pos() x_N, y_N = x_C + self.param[1], y_C + self.param[2] points = ms.line_coords(x_C, y_C, x_N, y_N) @@ -313,7 +327,7 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 delay = None @@ -333,6 +347,9 @@ def Run( self.params[1] + ", " + self.params[2] + ") by " + str(skip) + \ " pixels per step and wait " + self.params[3] + " milliseconds between each step") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -380,7 +397,7 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 x1, y1 = symbols['m_pos'] @@ -401,6 +418,9 @@ def Run( " in a line by " + str(skip) + " pixels per step and wait " + \ str(delay) + " milliseconds between each step") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -446,11 +466,14 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Store mouse position") + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. return idx+1 @@ -486,13 +509,18 @@ def Run( params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET, R_VALIDATE]) == -1: + if self.Partial_run(*params, [R_INIT, R_GET]) == -1: return -1 if symbols['m_pos'] == tuple(): print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols['m_pos'])) + + if self.Partial_run(*params, [R_VALIDATE]) == -1: + return -1 + + if symbols['m_pos'] != tuple(): ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) return idx+1 From 822d941ed3ab0ef5cee03da886728234eb1a9567 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 13 Sep 2020 21:36:19 +0800 Subject: [PATCH 17/83] Significant improvements in internal documentation and consistency, but not ready for prime time * Lots more internal documentation (especially in command_base.py) * constants pulled out into their own module. * Using constants instead of magic numbers for better internal documentation and clarity * Simplification of methods, removing unnecessary intermediate routines. * re-refactoring of commands structure to make it easier to understand. * improvements to validation and messaging structures * Lots of bugs erased! * parameters that aren't numbers don't get converted to 0 by silly code thinking they're variable references! * Added method of declaring "n or more parameters" * methods in RPN_CALC re-ordered to be the same as other commands * added a comment that if you like algebraic notation, you can write a simple infix to postfix converter and use the RPN_EVAL * improved layout in some areas * implementation of mouse commands converted to new structure * working on converting control commands to new structure Whilst a lot of bugs are appearing as I work through this, a great number of them are due to errors in my initial refactoring. The new structure makes it a lot harder to make mistakes (well, some mistakes) --- NewCommands.md | 72 +++--- command_base.py | 428 ++++++++++++++++++++++++--------- commands_control.py | 467 +++++++++++++---------------------- commands_external.py | 14 +- commands_keys.py | 22 +- commands_mouse.py | 561 ++++++++++++++++--------------------------- commands_pause.py | 4 +- commands_rpncalc.py | 394 +++++++++++++++--------------- constants.py | 62 +++++ parse.py | 2 + scripts.py | 37 +-- variables.py | 35 ++- 12 files changed, 1043 insertions(+), 1055 deletions(-) create mode 100644 constants.py diff --git a/NewCommands.md b/NewCommands.md index e76ac0d..9181c0e 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -103,7 +103,7 @@ The `M_SCROLL` command looks like this: params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + if self.Partial_run(*params, [RS_INIT, RS_GET]) == -1: return -1 if self.param[2]: @@ -111,7 +111,7 @@ The `M_SCROLL` command looks like this: else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - if self.Partial_run(*params, [R_VALIDATE]) == -1: + if self.Partial_run(*params, [RS_VALIDATE]) == -1: return -1 if self.param[2]: @@ -122,7 +122,7 @@ The `M_SCROLL` command looks like this: return idx+1 finally: - self.Partial_run(*params, [R_FINAL]) + self.Partial_run(*params, [RS_FINAL]) scripts.add_command(Mouse_Scroll()) # register the command @@ -200,7 +200,7 @@ No code is required here! The default validation code for the class can handle params = [idx, split_line, symbols, coords, is_async] try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + if self.Partial_run(*params, [RS_INIT, RS_GET]) == -1: return -1 if self.param[2]: @@ -208,7 +208,7 @@ No code is required here! The default validation code for the class can handle else: print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - if self.Partial_run(*params, [R_VALIDATE]) == -1: + if self.Partial_run(*params, [RS_VALIDATE]) == -1: return -1 if self.param[2]: @@ -219,32 +219,32 @@ No code is required here! The default validation code for the class can handle return idx+1 finally: - self.Partial_run(*params, [R_FINAL]) + self.Partial_run(*params, [RS_FINAL]) ``` The first 8 lines are the standard header and should be copied verbatim. -The definition of "params" on line 10 is simply a shortcut so we don't have to type the exact same set of parameters over and over again for various management functions that need to know this stuff. +The definition of SYM_PARAMS on line 10 is simply a shortcut so we don't have to type the exact same set of parameters over and over again for various management functions that need to know this stuff. The main code is placed inside a TRY...FINALLY block to ensure that the finalization code is called. Currently this doesn't do a lot, but as it may become more necessary in later versions, it is recommended that you code the execution in this manner. Within the TRY...FINALLY block, we call "self.Partial_run()" to execute parts of the standard execution flow. The parts you can call are: - * R_INIT - Perform any initialization required (should ALWAYS be called) - * R_GET - Get the parameters, and evaluates variables (not strictly required for a command with no parameters, but STRONGLY encouraged) - * R_INFO - Display the command with the values. In most cases you'll want to implement this yourself as the default is pretty basic - * R_VALIDATE - Perform run-time validation of them, possibly printing messages. (not requied if you're not using variables, but STRONGLY encouraged) - * R_RUN - Perform the standard process of running the command. This should NEVER be used in this situation, as you will code this part! - * R_FINAL - Perform any finalization required. (should ALWAYS be called - in the FINALLY) + * RS_INIT - Perform any initialization required (should ALWAYS be called) + * RS_GET - Get the parameters, and evaluates variables (not strictly required for a command with no parameters, but STRONGLY encouraged) + * RS_INFO - Display the command with the values. In most cases you'll want to implement this yourself as the default is pretty basic + * RS_VALIDATE - Perform run-time validation of them, possibly printing messages. (not requied if you're not using variables, but STRONGLY encouraged) + * RS_RUN - Perform the standard process of running the command. This should NEVER be used in this situation, as you will code this part! + * RS_FINAL - Perform any finalization required. (should ALWAYS be called - in the FINALLY) -In this example, we are first running R_INIT, and R_GET. If a Partial_run returns -1, you should return immediately with -1. +In this example, we are first running RS_INIT, and RS_GET. If a Partial_run returns -1, you should return immediately with -1. -After the R_GET, you can refer to self.params[n] where n is from 0 to the maximum number of parameters. self.params[0] is the command name, and self.params[1] to self.params[n] are the parameters from 1 to n. At this point, optional parameters that have not been passed will be None, literal values will be validated and be the correct type, and variable values will be thethe correct types, but not yet validated. +After the RS_GET, you can refer to self.params[n] where n is from 0 to the maximum number of parameters. self.params[0] is the command name, and self.params[1] to self.params[n] are the parameters from 1 to n. At this point, optional parameters that have not been passed will be None, literal values will be validated and be the correct type, and variable values will be thethe correct types, but not yet validated. -After performing the 3 initial steps, we do our own implementation of the R_INFO. +After performing the 3 initial steps, we do our own implementation of the RS_INFO. -Next, the R_VALIDATE step is performed. After this, variable values will be validated. +Next, the RS_VALIDATE step is performed. After this, variable values will be validated. -After validation, we do what is required based on the parameters passed, and return idx + 1 (that's the next line). If we had a serious error we could return -1 to abort the script. Instead of returning idx+1, we could possibly just return the value of the R_RUN process, however this requires more code, and introduces additional complexities. Just do the simple thing and return the correct value! +After validation, we do what is required based on the parameters passed, and return idx + 1 (that's the next line). If we had a serious error we could return -1 to abort the script. Instead of returning idx+1, we could possibly just return the value of the RS_RUN process, however this requires more code, and introduces additional complexities. Just do the simple thing and return the correct value! No matter how we return, the FINALLY block will ensure that the finalization is called. @@ -360,13 +360,13 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if not ok: @@ -492,13 +492,13 @@ Note that commands are case sensitive, so the name should be in all uppercase to ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if not ok: @@ -617,13 +617,13 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if not ok: @@ -776,13 +776,13 @@ Every command that does something (e.g. not labels - that have their effect duri ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[0] + " " + ret) + print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) ok = False if not ok: @@ -1037,10 +1037,10 @@ Currently the dictionary contains x entries: * 'repeats' : A dictionary of loop counters for `REPEAT` commands * 'original' : A dictionary of the starting values for `REPEAT` commands * 'labels' : A dictionary of the label names and locations within the script - * 'm_pos' : A tuple containing the saved mouse position - * 'g_vars' : A tuple containing the lock object and the dictionary of global variables - * 'l_vars' : A dictionary containing local variables - * 'stack' : A mutable tuple containing the local stack + * SYM_MOUSE : A tuple containing the saved mouse position + * SYM_GLOBAL : A tuple containing the lock object and the dictionary of global variables + * SYM_LOCAL : A dictionary containing local variables + * SYM_STACK : A mutable tuple containing the local stack The symbol table can be modified in the `Validation` and/or `Run` methods. @@ -1048,20 +1048,20 @@ An example of adding a new label (from the `GOTO_LABEL` command) is: ```python # add label to symbol table # Add the new label to the labels in the symbol table - symbols["labels"][split_line[1]] = idx # key is label, data is line number + symbols[SYM_LABELS][split_line[1]] = idx # key is label, data is line number ``` This can be checked for existance by: ```python - if split_line[1] in symbols["labels"]: # Does the label already exist (that's bad)? + if split_line[1] in symbols[SYM_LABELS]: # Does the label already exist (that's bad)? ... ``` Finally, it can be accessed to determine where a label is: ```python - return symbols["labels"][split_line[1]] # normally we return the line number the label is on + return symbols[SYM_LABELS][split_line[1]] # normally we return the line number the label is on ``` ### Repeats @@ -1097,9 +1097,9 @@ This structure contains the list of values that make up the stack for the curren The coords are the current x,y values passed to the command (or header). I believe these are the button coordinates. This array contains 3 elements: - * coords[0] - a string describing the location - * coords[1] - the X value - * coords[2] - the Y value + * coords[BC_TEXT] - a string describing the location + * coords[BC_X] - the X value + * coords[BC_Y] - the Y value ## self.Name diff --git a/command_base.py b/command_base.py index a5c1cf8..d4f0757 100644 --- a/command_base.py +++ b/command_base.py @@ -1,24 +1,5 @@ import variables - - -# Constants for auto validation -P_DESCRIPTION = 0 -P_OPTIONAL = 1 -P_VAR_OK = 2 -P_TYPE = 3 -P_CONVERT = 4 -P_P1_VALIDATION = 5 -P_P2_VALIDATION = 6 - - -# constants for run state -R_INIT = 0 -R_GET = 1 -R_INFO = 2 -R_VALIDATE = 3 -R_RUN = 4 -R_FINAL = 5 - +from constants import * # ################################################## # ### CLASS Command_Basic ### @@ -28,24 +9,43 @@ class Command_Basic: def __init__( self, - Name: str, # The name of the command (what you put in the script) - Lib="LIB_UNSET", - Auto_validate=None + name: str, # The name of the command (what you put in the script) + lib="LIB_UNSET", + auto_validate=None, # Definition of the input parameters + auto_message=None, # Definition of the message format ): - self.name = Name - self.lib = Lib - self.auto_validate = Auto_validate - - self.valid_max_params = self.Calc_valid_max_params() - self.valid_num_params = self.Calc_valid_param_counts() - - self.run_states = [R_INIT, R_GET, R_INFO, R_VALIDATE, R_RUN, R_FINAL] - self.param = None - self.param_cnt = None + # The information below MUST NOT be changed outside __init__ . + # Remember - more than one command may be in execution at a time + # and we rely on the parameters to the methods to contain things + # unique to each one! Local variables are fine, self.anything is BAD + self.name = name # the literal name of our command + self.lib = lib # the library we're part of + self.auto_validate = auto_validate # any auto-validation, if defined + self.auto_message = auto_message # format for any messages we need + + self.valid_max_params = self.Calc_valid_max_params() # calculate the max number of parmeters + self.valid_num_params = self.Calc_valid_param_counts() # calculate the set of valid numbers of parameters + self.run_states = [RS_INIT, RS_GET, RS_INFO, RS_VALIDATE, RS_RUN, RS_FINAL] # by default we'll do everything if you don't override + self.validation_states = [VS_COUNT, VS_PASS_1, VS_PASS_2] # by default we'll do a count and both passes if you don't override + + def Validate( + # This is a low level validation routine. If you take over this function you must take + # responsibility for all validation. + + # If you have set up the auto_valudate structure, it will do most of the validation for you. + # If you need to do more, you may be able to override a more specific routine. + + # This routine will be called twice, once for pass_no 1 and again for pass_no 2 + # Pass 1 is for general validation of literal commands and literal parameters, and also + # for adding symbols (for example, labels). + # Pass 2 is typically used for checking for the presence of symbols (labels, for example) + + # This method should return True if the validation was successful, otherwise it should + # return a tuple of the error message and the line causing the error. self, idx: int, line, @@ -57,24 +57,66 @@ def Validate( # pass 2. For example, goto can be checked on pass 2 to ensure the label # exists ): - - ret = None - - if self.auto_validate: - if pass_no == 1: - ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols, pass_no) - ret = self.Validate_params(ret, P_P1_VALIDATION, idx, line, lines, split_line, symbols, pass_no) - - if pass_no == 2: - ret = self.Validate_params(ret, P_P2_VALIDATION, idx, line, lines, split_line, symbols, pass_no) - if ret == None: - return ("", "") # error value! + try: + # invalid return, but indicates nothing done yet. + ret = None - return ret + # If it's pass 1 + if pass_no == VS_PASS_1: + # validate the count if required + if VS_COUNT in self.validation_states: + ret = self.Partial_validate_step_count(ret, idx, line, lines, split_line, symbols) + + # do pass 1 validation if required + if VS_PASS_1 in self.validation_states: + ret = self.Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) + + # if it's pass 2 + elif pass_no == VS_PASS_2: + # call Pass 2 if required + if VS_PASS_1 in self.validation_states: + ret = self.Partial_validate_step_pass_2(ret, idx, line, lines, split_line, symbols) + + except: + import traceback + traceback.print_exc() + ret = ("", "") + + finally: + if type(ret) == tuple: + return ret + elif ret == None or ret == True: + return True + else: + return ("", "") + + + def Partial_validate_step_count(self, ret, idx, line, lines, split_line, symbols): + # Validation of the count is separated from the pass 1 validation because sometinmes + # you want to override one but not the other. You would override this if you have some + # odd way of counting parameters, or the count depends on something complex. + ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols) + + + def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + # Pass 1 Validation is typically defined by the auto_validate structure set up in the + # command initialisation. You would override this if you haven't defined this, or the + # structure cannot pass something important about the validation. If you override this, + # you may wish to call the ancestor first. + ret = self.Validate_params(ret, AV_P1_VALIDATION, idx, line, lines, split_line, symbols) + + + def Partial_validate_step_pass_2(self, ret, idx, line, lines, split_line, symbols): + # Pass 2 Validation is typically defined by the auto_validate structure set up in the + # command initialisation. You would override this if you haven't defined this, or the + # structure cannot pass something important about the validation. If you override this, + # you may wish to call the ancestor first. + ret = self.Validate_params(ret, AV_P2_VALIDATION, idx, line, lines, split_line, symbols) def Parse( + # Parse is pretty much a call to Validate, except the output of the validation is immediately printed. self, idx: int, line, @@ -90,10 +132,11 @@ def Parse( ): ret = self.Validate(idx, line, lines, split_line, symbols, pass_no) + if ret == True: return True - if len(ret) != 2: + if ret == None or ret == False or len(ret) != 2: ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), line) if ret[0]: @@ -103,6 +146,17 @@ def Parse( def Run( + # The low level run command. Override this if you want to take complete control of the execution of + # the command. Typically you'll want to override one of the Partial_run... methods or the Perform() + # method + + # This should return idx+1 normally (this causes the script to continue at the next line (or exit + # when it falls off the end. + + # If you wish to abort the script, you should return a value outside of the range of valid line numbers. + # Typically -1 is returned, however in some cases a very high number can also be returned. + + # To cause the script to jump to a different line, simply return the line number you wish to go to. self, idx: int, split_line, @@ -111,47 +165,141 @@ def Run( is_async ): - ret = self.Partial_run(idx, split_line, symbols, coords, is_async, self.run_states) - - if ret == -1: - return ret - else: - return idx+1 - - - def Partial_run(self, idx, split_line, symbols, coords, is_async, run_subset): - ret = None - try: - if R_INIT in run_subset: - pass + ret = None # this is an invalid return value, but it indicates nothing has happened yet + + if RS_INIT in self.run_states: # Do the initialisation if required (highly recommended) + ret = self.Partial_run_step_init(ret, idx, split_line, symbols, coords, is_async) + if ret == -1 or ret == False: + return ret - if R_GET in run_subset: - ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 1) - if ret == -1: + if RS_GET in self.run_states: # Get the parameters if required + ret = self.Partial_run_step_get(ret, idx, split_line, symbols, coords, is_async) + if ret == -1 or ret == False: return ret - if R_INFO in run_subset: - print("[" + self.Lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + " parameters (" + str(v) + ")") + if RS_INFO in self.run_states: # Display info if required + ret = self.Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) + if ret == -1 or ret == False: + return ret - if R_VALIDATE in run_subset: - ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 2) - if ret == -1: + if RS_VALIDATE in self.run_states: # Validate the parameters if required + ret = self.Partial_run_step_validate(ret, idx, split_line, symbols, coords, is_async) + if ret == -1 or ret == False: return ret - if R_RUN in run_subset: - pass + if RS_RUN in self.run_states: # Actualy do the command! (calls Perform() + ret = self.Partial_run_step_run(ret, idx, split_line, symbols, coords, is_async) + if ret == -1 or ret == False: + return ret + except: + import traceback + traceback.print_exc() + ret = -1 + finally: - if R_FINAL in run_subset: - self.param = None - self.param_cnt = None + if RS_FINAL in self.run_states: # Do the finalisation if required (highly recommended) + self.Partial_run_step_final(ret, idx, split_line, symbols, coords, is_async) + + # Make sure the return values are tidied up. + if type(ret) == int: + return ret + elif ret == None or ret == True: + return idx+1 + else: + return -1 - return ret + + def Partial_run_step_init(self, ret, idx, split_line, symbols, coords, is_async): + # information about *this* run of the command MUST be in the symbol table + + # You might be tempted to not run the init if the variables below aren't needed, + # however this could have consequences in the future, so it's best to run it. + + # If you need more temporary data, you can override this, call the ancestor, and + # create what you need. + symbols[SYM_PARAMS] = [self.name] + [None] * self.valid_max_params + symbols[SYM_PARAM_CNT] = 0 + + return ret + + + def Partial_run_step_get(self, ret, idx, split_line, symbols, coords, is_async): + # This gets the values from the command, including fetching variable values. + # After this is run, parameters will be in the symbol table, but those coming + # from variables will not have been validated. + ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 1) + return ret + + + def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + # This step matches the number of parameters passed with the definitions for messages, + # printing the matching message, or a default message if no matching message can be found. + + # If you have messages that don't fit a simple template (e.g. 2 different possible messages + # for the same number of parameters then you're going to want to override this method. + # If you're overriding the method, you will rarely want to call the ancestor method. + msg = False + if self.auto_message: + params = symbols[SYM_PARAMS] + param_cnt = symbols[SYM_PARAM_CNT] - 1 + for msg_def in self.auto_message: + if msg_def[AM_COUNT] == param_cnt: + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + msg_def[AM_FORMAT].format(*params)) + msg = True + break + + if not msg: + print(AM_DEFAULT.format(self.lib, coords[BC_TEXT], str(idx+1), self.name, str(params))) + + return ret + + + def Partial_run_step_validate(self, ret, idx, split_line, symbols, coords, is_async): + # This step performs run-time validation of values passed from variables. + # Those that come from variables are checked using the validation method passed. + ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 2) + return ret + + + def Partial_run_step_run(self, ret, idx, split_line, symbols, coords, is_async): + # This performs the running of the command. Because this does nothing it is pretty much + # ALWAYS overridden. However, to make life easier, this function calls the Process() + # method. That's simpler to override. + ret = self.Process(idx, split_line, symbols, coords, is_async) + if ret == None: + ret = idx + 1 + + return ret + + + def Partial_run_step_final(self, ret, idx, split_line, symbols, coords, is_async): + # This removes stuff from the symbol table that isn't needed any more. Whilst it may be fairly + # superfluous at present, if you start to put more stuff in the symbol table during the execution + # of a command this might start to get more important. + + # If you override this, it is conventional to call the ancestor function last, but there's no reason + # at present that you must. + del symbols[SYM_PARAMS] + del symbols[SYM_PARAM_CNT] + + return ret + + + def Process(self, idx, split_line, symbols, coords, is_async): + # This is the default process called to run a command. Override it to do something other than + # nothing at runtime. + + # This is probably the most common method you will override. It is designed in such a way that + # you do not need to call the ancestor. + pass # default process is to do nothing def Calc_valid_max_params(self): # Return the maximum number of parameters. We can calculate this simply based on the number defined + # in the auto_validate. If you aren't using the auto_validate, then you may need to set this yourself + # in the __init__() if self.auto_validate: return len(self.auto_validate) @@ -160,6 +308,12 @@ def Calc_valid_max_params(self): def Calc_valid_param_counts(self): # Return a set of numbers of parameters that are acceptable. This is defined by which are optional + # within the auto_validate structure. If you're not using the auto_validate structure then you also + # probably don't need this, but you'll need to do something to validate the correct number of parameters + # have been passed in the validation (VS_COUNT) + + # This routine does not return it, but setting the counts to [n, None] indicates to the parameter number + # validation routine that n or more parameters are acceptable -- this is great for comments etc. ret = None if self.auto_validate: @@ -167,84 +321,136 @@ def Calc_valid_param_counts(self): vn = len(self.auto_validate) for i in range(vn): i_val = self.auto_validate[i] - if (i_val[P_OPTIONAL] == True) or (i+1 == vn): + if (i_val[AV_OPTIONAL] == True) or (i+1 == vn): ret += [i+1] if ret: - return set(ret) + return ret return ret - def Validate_param_count(self, ret, idx, line, lines, split_line, symbols, pass_no): + def Validate_param_count(self, ret, idx, line, lines, split_line, symbols): + # Should only be called from pass 1 (actually within VS_COUNT that happens just prior to + # VS_PASS_1 + + # Whilst you can override this method, you're more likely to override the Validation_step_count() + # method which does no more than just call this. if not (ret == None or ret == True): return ret - if pass_no == 1: - return variables.Check_num_params(split_line, self.valid_num_params, idx, line, self.name) - - return ret + return variables.Check_num_params(split_line, self.valid_num_params, idx, line, self.name) - def Validate_params(self, ret, val_const, idx, line, lines, split_line, symbols, pass_no): + def Validate_params(self, ret, val_validation, idx, line, lines, split_line, symbols): + # This command is called from both pass 1 and 2 of validation It is really just a method to + # call the validation of the parameters one by one. If you haven't set up the maximum parameters + # (if you haven't used the auto_validate structure) then you can override this to validate each + # of your parameters. You will need to remember that this gets called for both pass 1 and 2. if not (ret == None or ret == True): return ret for i in range(self.valid_max_params): - ret = self.Validate_param_n(ret, i+1, val_const, idx, line, lines, split_line, symbols, pass_no) + ret = self.Validate_param_n(ret, i+1, val_validation, idx, line, lines, split_line, symbols) if ret != True: return ret return ret - def Validate_param_n(self, ret, n, val_const, idx, line, lines, split_line, symbols, pass_no): + def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, symbols): + # This method validates parameters. For custom parameters, you're best off defining new validation + # methods (like the current variables.Validate_gt_zero()) unless you need access to the symbol + # table. + + # Note that this function, because it runs during validation, accesses the split_line, not the + # symbol table. + + # Where a variable type is defined as having "special" validation, that validation is currently + # hard coded here. It would be better to register validation routines, but... later. if not (ret == None or ret == True): return ret - val = self.auto_validate[n-1] - - opt = self.valid_num_params == {} or (set(range(1,n)) & self.valid_num_params) != [] - - ret = variables.Check_generic_param(split_line, n, val[P_DESCRIPTION], idx, self.name, line, val[P_CONVERT], val[P_TYPE], val[val_const], val[P_OPTIONAL], val[P_VAR_OK]) - if ret == True or ret == None: - return True + if n <= len(self.auto_validate): + # the normal auto-validation + val = self.auto_validate[n-1] - return (ret, line) - - + opt = self.valid_num_params == [] or (set(range(1,n)) & set(self.valid_num_params)) != [] + + val_t = val[AV_TYPE] + ret = variables.Check_generic_param(split_line, n, val[AV_DESCRIPTION], idx, self.name, line, val_t, val[val_validation], val[AV_OPTIONAL], val[AV_VAR_OK]) + + # should we do special validation? + if ret == True or ret == None: + if not val_t[AVT_SPECIAL]: + return True + + if val_validation == AV_P1_VALIDATION: + if val_t == PT_TARGET: # targets (label definitions) have pass 1 validation only + # check for duplicate label + if split_line[n] in symbols[SYM_LABELS]: # Does the label already exist (that's bad)? + return ("Duplicate LABEL", line) + + # add label to symbol table # Add the new label to the labels in the symbol table + symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number + + elif val_validation == AV_P2_VALIDATION: + if val_t == PT_LABEL: # references (to a label) have pass 2 validation only + # check for existance of label + if split_line[n] not in symbols[SYM_LABELS]: + return ("Target not found", line) + + return True + + return (ret, line) + + def Run_params(self, ret, idx, split_line, symbols, coords, is_async, pass_no): + # This method gets the parameters. Oddly enough it has 2 passes too. The first pass simply gets the + # variables, while the second pass gets them and does validation. + + # This method actually just calls the Run_Param_n method that does all the hard work for each parameter if ret == None: ret = True if pass_no == 1: - self.param = [self.name] - self.param_cnt = len(split_line) + param_cnt = len(split_line) + symbols[SYM_PARAM_CNT] = param_cnt for i in range(self.valid_max_params): - if i < self.param_cnt: - self.param += [self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no)] - else: - self.param += [None] + if i < param_cnt: + symbols[SYM_PARAMS][i+1] = self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no) + elif pass_no == 2: + # for pass 2 we don't try to validate null variables for i in range(self.valid_max_params): - if self.param[i+1] != None: + if symbols[SYM_PARAMS][i+1] != None: ret = self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no) return ret def Run_param_n(self, ret, idx, n, split_line, symbols, coords, is_async, pass_no): + # This function gets called to firstly get the parameter (pass_no = 1) and then + # to validate it (with pass_no = 2) + + # Note that pass 1 returns the variable value, where pass 2 returns a value indicating + # if validation has passed. if pass_no == 1: - return variables.get_value(split_line[n], symbols) + v = split_line[n] + if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK]: + v = variables.get_value(split_line[n], symbols) + if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: + v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) + return v elif pass_no == 2: val = self.auto_validate[n-1] ok = ret - if val[P_P1_VALIDATION]: - ok = val[P_P1_VALIDATION](self.param[n], idx, self.name, val[P_DESCRIPTION], n, split_line[n]) + if val[AV_P1_VALIDATION]: + ok = val[AV_P1_VALIDATION](symbols[SYM_PARAMS][n], idx, self.name, val[AV_DESCRIPTION], n, split_line[n]) if ok != True: - print("[" + self.lib + "] " + coords[0] + " " + ok) + print("[" + self.lib + "] " + coords[BC_TEXT] + " " + ok) ret = -1 return ret @@ -259,14 +465,14 @@ class Command_Header(Command_Basic): def __init__( self, - Name: str, # The name of the command (what you put in the script) - Is_async: bool, # is this async? - Lib="LIB_UNSET", - Auto_validate=None + name: str, # The name of the command (what you put in the script) + is_async: bool, # is this async? + lib="LIB_UNSET", + auto_validate=None ): - super().__init__(Name, Lib, Auto_validate) - self.is_async = Is_async + super().__init__(name, lib, auto_validate) + self.is_async = is_async def Validate( self, diff --git a/commands_control.py b/commands_control.py index a50e344..ef8af85 100644 --- a/commands_control.py +++ b/commands_control.py @@ -1,6 +1,7 @@ -import command_base, lp_events, scripts +import command_base, lp_events, scripts, variables +from constants import * -lib = "cmds_ctrl" # name of this library (for logging) +LIB = "cmds_ctrl" # name of this library (for logging) # ################################################## # ### CLASS Control_Comment ### @@ -14,33 +15,18 @@ def __init__( self, ): - super().__init__("-") # the name of the command as you have to enter it in the code + super().__init__("-", # the name of the command as you have to enter it in the code + LIB, + (), + () ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - return True # return True if there is no error (a comment can't ever be an error) - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " comment: " + split_line[1:]) # coords[0] is the text "(x, y)" - - return idx+1 # Return the number of the next line to execute, -1 to exit + # this command does not have a standard list of fields, so we need to do some stuff manually + self.valid_max_params = 32767 # There is no maximum, but this is a reasonable limit! + self.valid_num_params = [0, None] # zero or more is OK + #self.run_states = [RS_INIT, RS_INFO, RS_FINAL] # No need to do anything at all for a comment, but let's display it + #self.validation_states = [] # And no validation either + scripts.add_command(Control_Comment()) # register the command @@ -55,44 +41,19 @@ def __init__( self, ): - super().__init__("LABEL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - # check number of split_line - if len(split_line) != 2: - return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.name, line) - - # check for duplicate label - if split_line[1] in symbols["labels"]: # Does the label already exist (that's bad)? - return ("duplicate LABEL", line) - - # add label to symbol table # Add the new label to the labels in the symbol table - symbols["labels"][split_line[1]] = idx # key is label, data is line number - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Label: " + split_line[1]) + super().__init__("LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, False, PT_TARGET, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Label {1}"), + ) ) - return idx+1 # Nothing to do when executing a label + #self.run_states = [RS_INIT, RS_INFO, RS_FINAL] # No need to do anything at all for a label, but let's display it + #self.validation_states = [VS_PASS_1] # We need to do pass 1 validation scripts.add_command(Control_Label()) # register the command @@ -108,48 +69,20 @@ def __init__( self, ): - super().__init__("GOTO_LABEL") # the name of the command as you have to enter it in the code + super().__init__("GOTO_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type conv p1_val p2_val + ("Label", False, False, PT_LABEL, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Goto label {1}"), + ) ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - # check number of split_line - if len(split_line) != 2: - return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.ame, line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Goto LABEL " + split_line[1]) - - # check for label - if not split_line[1] in symbols["labels"]: # The label should always exist - print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - return symbols["labels"][split_line[1]] # normally we return the line number the label is on - - return idx+1 # We'll never get here + def Process(self, idx, split_line, symbols, coords, is_async): + return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # we simply return the line number the label is on scripts.add_command(Control_Goto_Label()) # register the command @@ -165,50 +98,21 @@ def __init__( self, ): - super().__init__("IF_PRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 2: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes exactly 1 argument.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed goto LABEL " + split_line[1]) - if not split_line[1] in symbols["labels"]: # The label should always exist - print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if lp_events.pressed[coords[1]][coords[2]]: # coords[1] is x, and coords[2] is y - if split_line[1] in symbols["labels"]: # The label should always exist - return symbols["labels"][split_line[1]] # and we return the line number the label is on - else: - print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 + super().__init__("IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type conv p1_val p2_val + ("Label", False, False, PT_LABEL, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " If pressed goto label {1}"), + ) ) - return idx+1 + + def Process(self, idx, split_line, symbols, coords, is_async): + if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: # if key is pressed + return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # and we return the line number the label is on scripts.add_command(Control_If_Pressed_Goto_Label()) # register the command @@ -224,47 +128,21 @@ def __init__( self, ): - super().__init__("IF_UNPRESSED_GOTO_LABEL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 2: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' takes exactly 1 argument.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed goto LABEL " + split_line[1]) + super().__init__("IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type conv p1_val p2_val + ("Label", False, False, PT_LABEL, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " If unpressed goto label {1}"), + ) ) - if not split_line[1] in symbols["labels"]: # The label should always exist - print("Line:" + str(idx+1) + " - missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if not lp_events.pressed[coords[1]][coords[2]]: # if the key is pressed - return symbols["labels"][split_line[1]] # jump to the label - - return idx+1 + + def Process(self, idx, split_line, symbols, coords, is_async): + if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: # if key is pressed + return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # and we return the line number the label is on scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command @@ -280,63 +158,50 @@ def __init__( self, ): - super().__init__("REPEAT_LABEL") # the name of the command as you have to enter it in the code + super().__init__("REPEAT_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Repeat label {1}, {2} times max"), + ) ) + + + def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + ret = super().Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) + + if ret == None or ret == True: + symbols[SYM_REPEATS][idx] = int(split_line[2]) + symbols[SYM_ORIGINAL][idx] = int(split_line[2]) + ret = True + + return ret + + + def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + # Oddly enough, we want the original info message too. + ret = super().Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) + + if symbols[SYM_REPEATS][idx] > 0: + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + else: + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " No repeats left, not repeating.") - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): + return ret - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + " requires a minimum of 1 repeat.", line) - else: - symbols["repeats"][idx] = int(split_line[2]) - symbols["original"][idx] = int(split_line[2]) - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) + def Process(self, idx, split_line, symbols, coords, is_async): + + if symbols[SYM_REPEATS][idx] > 0: + symbols[SYM_REPEATS][idx] = symbols[SYM_REPEATS][idx] - 1 + return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] + return True - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ - split_line[2] + " times max") - - if not split_line[1] in symbols["labels"]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] - else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - - return idx+1 - scripts.add_command(Control_Repeat_Label()) # register the command @@ -374,16 +239,16 @@ def Validate( if temp < 1: return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) else: - symbols["repeats"][idx] = int(split_line[2])-1 - symbols["original"][idx] = int(split_line[2])-1 + symbols[SYM_REPEATS][idx] = int(split_line[2])-1 + symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 except: return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: + if split_line[1] not in symbols[SYM_LABELS]: return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - if symbols["labels"][split_line[1]] > idx: + if symbols[SYM_LABELS][split_line[1]] > idx: return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) return True @@ -397,16 +262,16 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ split_line[2] + " times max") - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] + if symbols[SYM_REPEATS][idx] > 0: + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + symbols[SYM_REPEATS][idx] -= 1 + return symbols[SYM_LABELS][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols["repeats"][idx] = symbols["original"][idx] # makes this behave like a normal loop + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") + symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # makes this behave like a normal loop return idx+1 @@ -444,13 +309,13 @@ def Validate( if temp < 1: return ("Line:" + str(idx+1) + " - '" + split_line[0] + " requires a minimum of 1 repeat.", line) else: - symbols["repeats"][idx] = int(split_line[2]) - symbols["original"][idx] = int(split_line[2]) + symbols[SYM_REPEATS][idx] = int(split_line[2]) + symbols[SYM_ORIGINAL][idx] = int(split_line[2]) except: return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: + if split_line[1] not in symbols[SYM_LABELS]: return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -464,19 +329,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - if not split_line[1] in symbols["labels"]: # The label should always exist + if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: - if lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] + if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: + if symbols[SYM_REPEATS][idx] > 0: + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + symbols[SYM_REPEATS][idx] -= 1 + return symbols[SYM_LABELS][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") return idx+1 @@ -517,16 +382,16 @@ def Validate( if temp < 1: return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) else: - symbols["repeats"][idx] = int(split_line[2])-1 - symbols["original"][idx] = int(split_line[2])-1 + symbols[SYM_REPEATS][idx] = int(split_line[2])-1 + symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 except: return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: + if split_line[1] not in symbols[SYM_LABELS]: return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - if symbols["labels"][split_line[1]] > idx: + if symbols[SYM_LABELS][split_line[1]] > idx: return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) @@ -541,20 +406,20 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") - if not split_line[1] in symbols["labels"]: # The label should always exist + if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: - if lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] + if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: + if symbols[SYM_REPEATS][idx] > 0: + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + symbols[SYM_REPEATS][idx] -= 1 + return symbols[SYM_LABELS][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols["repeats"][idx] = symbols["original"][idx] # for a normal repeat statement + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") + symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # for a normal repeat statement return idx+1 @@ -592,13 +457,13 @@ def Validate( temp = int(split_line[2]) if temp < 1: return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - symbols["repeats"][idx] = int(split_line[2]) - symbols["original"][idx] = int(split_line[2]) + symbols[SYM_REPEATS][idx] = int(split_line[2]) + symbols[SYM_ORIGINAL][idx] = int(split_line[2]) except: return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: + if split_line[1] not in symbols[SYM_LABELS]: return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) return True @@ -612,19 +477,19 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - if not split_line[1] in symbols["labels"]: # The label should always exist + if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist print(" Line:" + str(idx+1) + " Missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: - if not lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] + if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: + if symbols[SYM_REPEATS][idx] > 0: + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + symbols[SYM_REPEATS][idx] -= 1 + return symbols[SYM_LABELS][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") return idx+1 @@ -664,16 +529,16 @@ def Validate( temp = int(split_line[2]) if temp < 1: return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - symbols["repeats"][idx] = int(split_line[2])-1 - symbols["original"][idx] = int(split_line[2])-1 + symbols[SYM_REPEATS][idx] = int(split_line[2])-1 + symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 except: return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols["labels"]: + if split_line[1] not in symbols[SYM_LABELS]: return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - if symbols["labels"][split_line[1]] > idx: + if symbols[SYM_LABELS][split_line[1]] > idx: return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) @@ -688,20 +553,20 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") - if not split_line[1] in symbols["labels"]: # The label should always exist + if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist print("missing LABEL '" + split_line[1] + "'") # otherwise an error return -1 else: - if not lp_events.pressed[coords[1]][coords[2]]: - if symbols["repeats"][idx] > 0: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + str(symbols["repeats"][idx]) + " repeats left.") - symbols["repeats"][idx] -= 1 - return symbols["labels"][split_line[1]] + if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: + if symbols[SYM_REPEATS][idx] > 0: + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + symbols[SYM_REPEATS][idx] -= 1 + return symbols[SYM_LABELS][split_line[1]] else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols["repeats"][idx] = symbols["original"][idx] # to behave more normal + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") + symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # to behave more normal return idx+1 @@ -745,10 +610,10 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Reset all repeats") + print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Reset all repeats") - for i in symbols["repeats"]: - symbols["repeats"][i] = symbols["original"][i] + for i in symbols[SYM_REPEATS]: + symbols[SYM_REPEATS][i] = symbols[SYM_ORIGINAL][i] return idx+1 diff --git a/commands_external.py b/commands_external.py index c130a55..3b6ce23 100644 --- a/commands_external.py +++ b/commands_external.py @@ -46,7 +46,7 @@ def Run( if "http" not in link: link = "http://" + link - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open website " + link + " in default browser") + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open website " + link + " in default browser") webbrowser.open(link) @@ -100,7 +100,7 @@ def Run( if "http" not in link: link = "http://" + link - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open website " + link + " in default browser, try to make a new window") + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open website " + link + " in default browser, try to make a new window") webbrowser.open_new(link) @@ -155,7 +155,7 @@ def Run( path_name = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Open file or folder " + path_name) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open file or folder " + path_name) files.open_file_folder(path_name) @@ -206,11 +206,11 @@ def Run( ): if len(split_line) > 2: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ " at volume " + str(split_line[2])) sound.play(split_line[1], float(split_line[2])) else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) sound.play(split_line[1]) return idx+1 @@ -309,12 +309,12 @@ def Run( ): args = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Running code: " + args) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Running code: " + args) try: subprocess.run(args) except Exception as e: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Error with running code: " + str(e)) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Error with running code: " + str(e)) return idx+1 diff --git a/commands_keys.py b/commands_keys.py index 3326d6d..0a03e7e 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -41,7 +41,7 @@ def Run( print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Wait for script key to be unpressed") - while lp_events.pressed[coords[1]][coords[2]]: + while lp_events.pressed[coords[BC_X]][coords[BC_Y]]: sleep(DELAY_EXIT_CHECK) if check_kill(x, y, is_async): return idx + 1 @@ -100,29 +100,29 @@ def Run( releasefunc = lambda: kb.release(key) if len(split_line) <= 2: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1]) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1]) kb.tap(key) elif len(split_line) <= 3: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + " times") + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + " times") taps = int(split_line[2]) for tap in range(taps): - if check_kill(coords[1], coords[2], is_async, releasefunc): + if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): return idx + 1 kb.tap(key) else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + \ + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + \ " times for " + str(split_line[3]) + " seconds each") taps = int(split_line[2]) delay = float(split_line[3]) for tap in range(taps): - if check_kill(coords[1], coords[2], is_async, releasefunc): + if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): return -1 kb.press(key) - if not safe_sleep(delay, coords[1], coords[2], is_async, releasefunc): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async, releasefunc): return -1 return idx+1 @@ -174,7 +174,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Press key " + split_line[1]) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Press key " + split_line[1]) key = kb.sp(split_line[1]) kb.press(key) @@ -228,7 +228,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Release key " + split_line[1]) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Release key " + split_line[1]) key = kb.sp(split_line[1]) kb.release(key) @@ -276,7 +276,7 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Release all keys") + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Release all keys") kb.release_all() @@ -325,7 +325,7 @@ def Run( type_string = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Type out string " + type_string) + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Type out string " + type_string) kb.write(type_string) diff --git a/commands_mouse.py b/commands_mouse.py index 489c808..0fddc66 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -1,50 +1,34 @@ import command_base, ms, scripts, variables -from command_base import R_INIT, R_GET, R_INFO, R_VALIDATE, R_RUN, R_FINAL +from constants import * -lib = "cmds_mous" # name of this library (for logging) +LIB = "cmds_mous" # name of this library (for logging) # ################################################## # ### CLASS Mouse_Move ### # ################################################## -# class that defines the M_MOVE command (wait while a button is pressed?) +# class that defines the M_MOVE command (relative mouse movement) class Mouse_Move(command_base.Command_Basic): def __init__( self, ): - super().__init__("M_MOVE", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, None, None), \ - ("Y value", False, True, "integer", int, None, None) ) ) + super().__init__("M_MOVE", # the name of the command as you have to enter it in the code + LIB, # the name of this module + ( # description of parameters + # Desc Opt Var type p1_val p2_val (trailing comma is important) + ("X value", False, True, PT_INT, None, None), + ("Y value", False, True, PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (2, " Relative mouse movement ({1}, {2})"), + ) ) - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Relative mouse movement (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - ms.move_to_pos(float(self.param[1]), float(self.param[2])) - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) - + def Process(self, idx, split_line, symbols, coords, is_async): + ms.move_to_pos(float(symbols[SYM_PARAMS][1]), float(symbols[SYM_PARAMS][2])) + scripts.add_command(Mouse_Move()) # register the command @@ -59,38 +43,21 @@ def __init__( self, ): - super().__init__("M_SET", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None) ) ) + super().__init__("M_SET", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Y value", False, True, PT_INT, variables.Validate_ge_zero, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (2, " Set mouse position to ({1}, {2})"), + ) ) - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Set mouse position to (" + str(self.param[1]) + ", " + \ - str(self.param[2]) + ")") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - ms.set_pos(float(self.param[1]), float(self.param[2])) - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) + def Process(self): + ms.set_pos(float(symbols[SYM_PARAMS][1]), float(symbols[SYM_PARAMS][2])) scripts.add_command(Mouse_Set()) # register the command @@ -106,43 +73,25 @@ def __init__( self, ): - super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, None, None), \ - ("Scroll amount", False, True, "integer", int, None, None) ) ) + super().__init__("M_SCROLL", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Scroll amount", False, True, PT_INT, None, None), + ("X value", True, True, PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Scroll {1}"), + (2, " Scroll ({1}, {2})"), + ) ) - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 - - if self.param[2]: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - if self.param[2]: - ms.scroll(float(self.param[2]), float(self.param[1])) - else: - ms.scroll(0, float(self.param[1])) - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) + def Process(self, idx, split_line, symbols, coords, is_async): + if symbols[SYM_PARAMS][2]: + ms.scroll(float(symbols[SYM_PARAMS][2]), float(symbols[SYM_PARAMS][1])) + else: + ms.scroll(0, float(symbols[SYM_PARAMS][1])) scripts.add_command(Mouse_Scroll()) # register the command @@ -158,68 +107,46 @@ def __init__( self, ): - super().__init__("M_LINE", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X1 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Y1 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("X2 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Y2 value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + super().__init__("M_LINE", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X1 value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Y1 value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("X2 value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Y2 value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (4, " Mouse line from ({1}, {2}) to ({3}, {4})"), + (5, " Mouse line from ({1}, {2}) to ({3}, {4}) and wait {5}ms between steps"), + (6, " Mouse line from ({1}, {2}) to ({3}, {4}) by {6} pixels per step and wait {5}ms between steps"), + ) ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + delay = None + if symbols[SYM_PARAMS][5]: + delay = float(symbols[SYM_PARAMS][5]) / 1000.0 + + skip = 1 + if symbols[SYM_PARAMS][6]: + skip = int(symbols[SYM_PARAMS][6]) + + points = ms.line_coords(symbols[SYM_PARAMS][1], symbols[SYM_PARAMS][2], symbols[SYM_PARAMS][3], symbols[SYM_PARAMS][4]) + + for x_M, y_M in points[::skip]: + if check_kill(coords[BC_X], coords[BC_Y], is_async): return -1 - delay = None - if self.param[5]: - delay = float(self.param[5]) / 1000.0 - - skip = 1 - if self.param[6]: - skip = int(self.param[6]) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line from (" + \ - str(self.param[1]) + ", " + str(self.param[2]) + ") to (" + \ - str(self.param[3]) + ", " + str(self.param[4]) + ") by " + str(skip) + " pixels per step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line from (" + \ - str(self.param[1]) + ", " + str(self.param[2]) + ") to (" + \ - str(self.param[3]) + ", " + str(self,param[4]) + ") by " + \ - str(skip) + " pixels per step and wait " + str(self.param[5]) + " milliseconds between each step") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - points = ms.line_coords(self.param[1], self.param[2], self.param[3], self.param[4]) + ms.set_pos(x_M, y_M) - for x_M, y_M in points[::skip]: - if check_kill(x, y, is_async): + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): return -1 - ms.set_pos(x_M, y_M) - - if (delay != None) and (delay > 0): - if not safe_sleep(delay, x, y, is_async): - return -1 - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) - scripts.add_command(Mouse_Line()) # register the command @@ -234,65 +161,46 @@ def __init__( self, ): - super().__init__("M_LINE_MOVE", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): + super().__init__("M_LINE_MOVE", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X value", False, True, PT_INT, None, None), + ("Y value", False, True, PT_INT, None, None), + ("Wait value", True, True, PT_INT, variables.Validate_gt_zero, None), + ("Skip value", True, True, PT_INT, variables.Validate_ge_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Mouse line move relative ({1}, {2})"), + (3, " Mouse line move relative ({1}, {2}) and wait {3}ms between steps"), + (4, " Mouse line move relative ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), + ) ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + delay = None + if symbols[SYM_PARAMS][3]: + delay = float(symbols[SYM_PARAMS][3]) / 1000.0 + + skip = 1 + if symbols[SYM_PARAMS][4]: + skip = int(symbols[SYM_PARAMS][4]) + + x_C, y_C = ms.get_pos() + x_N, y_N = x_C + symbols[SYM_PARAMS][1], y_C + symbols[SYM_PARAMS][2] + points = ms.line_coords(x_C, y_C, x_N, y_N) - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + for x_M, y_M in points[::skip]: + if check_kill(coords[BC_X], coords[BC_Y], is_async): return -1 - delay = None - if self.param[3]: - delay = float(self.param[3]) / 1000.0 + ms.set_pos(x_M, y_M) - skip = 1 - if self.param[4]: - skip = int(self.param[4]) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - str(self.param[1]) + ", " + str(self.param[2]) + ") and wait " + str(self.param[3]) + " milliseconds between each step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Mouse line move relative (" + \ - str(self.param[1]) + ", " + str(self.param[2]) + ") by " + str(self.param[4]) + \ - " pixels per step and wait " + str(self.param[3]) + " milliseconds between each step") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - x_C, y_C = ms.get_pos() - x_N, y_N = x_C + self.param[1], y_C + self.param[2] - points = ms.line_coords(x_C, y_C, x_N, y_N) - - for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): return -1 - ms.set_pos(x_M, y_M) - - if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): - return -1 - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Line_Move()) # register the command @@ -308,63 +216,42 @@ def __init__( self, ): - super().__init__("M_LINE_SET", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Y value", False, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Wait value", True, True, "integer", int, variables.Validate_ge_zero, None), \ - ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: + super().__init__("M_LINE_SET", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Y value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Mouse set line ({1}, {2})"), + (3, " Mouse set line ({1}, {2}) and wait {3}ms between steps"), + (4, " Mouse set line ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), + ) ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + delay = None + if self.params[3]: + delay = float(self.params[3]) / 1000.0 + + skip = 1 + if self.params[4]: + skip = int(self.params[4]) + + x_C, y_C = ms.get_pos() + points = ms.line_coords(x_C, y_C, params[1], params[2]) + + for x_M, y_M in points[::skip]: + if check_kill(coords[BC_X], coords[BC_Y], is_async): return -1 - - delay = None - if self.params[3]: - delay = float(self.params[3]) / 1000.0 - - skip = 1 - if self.params[4]: - skip = int(self.params[4]) - - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ - self.params[1] + ", " + self.params[2] + ") by " + str(skip) + \ - " pixels per step") - else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Mouse line set (" + \ - self.params[1] + ", " + self.params[2] + ") by " + str(skip) + \ - " pixels per step and wait " + self.params[3] + " milliseconds between each step") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, x1, y1) - - for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): + ms.set_pos(x_M, y_M) + if (delay != None) and (delay > 0): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): - return -1 - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) scripts.add_command(Mouse_Line_Set()) # register the command @@ -380,65 +267,51 @@ def __init__( self, ): - super().__init__("M_RECALL_LINE", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("Wait value", True , True, "integer", int, variables.Validate_ge_zero, None), \ - ("Skip value", True, True, "integer", int, variables.Validate_gt_zero, None) ) ) - - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 - - x1, y1 = symbols['m_pos'] + super().__init__("M_RECALL_LINE", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Wait value", True , True, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (0, " Recall mouse position () in a line"), + (1, " Recall mouse position () in a line and wait {1} milliseconds between each step"), + (2, " Recall mouse position () in a line by {2} pixels per step and wait {1} milliseconds between each step"), + ) ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + # while this looks like validation, it is just a warning + if symbols[SYM_MOUSE] == tuple(): + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + else: + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols[SYM_MOUSE])) + + x1, y1 = symbols[SYM_MOUSE] delay = 0 - if self.params[3]: + if self.params[1]: delay = float(self.params[3]) / 1000.0 skip = 1 - if self.params[4]: + if self.params[2]: skip = int(self.params[4]) - if (delay == None) or (delay <= 0): - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ - " in a line by " + str(skip) + " pixels per step") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols["m_pos"]) + \ - " in a line by " + str(skip) + " pixels per step and wait " + \ - str(delay) + " milliseconds between each step") - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) for x_M, y_M in points[::skip]: - if check_kill(coords[1], coords[2], is_async): + if check_kill(coords[BC_X], coords[BC_Y], is_async): return -1 ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[1], coords[2], is_async): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): return -1 - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) - scripts.add_command(Mouse_Recall_Line()) # register the command @@ -453,33 +326,19 @@ def __init__( self, ): - super().__init__("M_STORE", lib, () ) # the name of the command as you have to enter it in the code - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 + super().__init__("M_STORE", # the name of the command as you have to enter it in the code + LIB, + ( + # no variables defined, so none are allowed + ), + ( + # num params, format string (trailing comma is important) + (0, " Store mouse position"), + ) ) - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Store mouse position") - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - symbols["m_pos"] = ms.get_pos() # Another example of modifying the symbol table during execution. - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) + def Process(self, idx, split_line, symbols, coords, is_async): + symbols[SYM_MOUSE] = ms.get_pos() # Another example of modifying the symbol table during execution. scripts.add_command(Mouse_Store()) # register the command @@ -495,38 +354,24 @@ def __init__( self, ): - super().__init__("M_RECALL", lib, () ) # the name of the command as you have to enter it in the code + super().__init__("M_RECALL", # the name of the command as you have to enter it in the code + ( + # no variables defined, so none are allowed + ), + ( + # no message format, so default will be used + ) ) + self.run_states = [RS_INIT, RS_GET, RS_VALIDATE, RS_RUN, RS_FINAL] # we won't do RS_INFO - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [R_INIT, R_GET]) == -1: - return -1 - - if symbols['m_pos'] == tuple(): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") - else: - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols['m_pos'])) - - if self.Partial_run(*params, [R_VALIDATE]) == -1: - return -1 - - if symbols['m_pos'] != tuple(): - ms.set_pos(symbols['m_pos'][0], symbols['m_pos'][1]) - - return idx+1 - - finally: - self.Partial_run(*params, [R_FINAL]) + def Process(self, idx, split_line, symbols, coords, is_async): + # while this looks like validation, it is really just the info. Putting it here is easy + if symbols[SYM_MOUSE] == tuple(): + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + else: + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols[SYM_MOUSE])) + ms.set_pos(symbols[SYM_MOUSE][0], symbols[SYM_MOUSE][1]) scripts.add_command(Mouse_Recall()) # register the command diff --git a/commands_pause.py b/commands_pause.py index 58eaa0a..d3cdf40 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -49,11 +49,11 @@ def Run( is_async # True if the script is running asynchronously ): - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " Delay for " + split_line[1] + " seconds") + print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Delay for " + split_line[1] + " seconds") delay = float(split_line[1]) - if not safe_sleep(delay, coords[1], coords[2], is_async): + if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): return -1 return idx+1 diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 932a054..d9a820a 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -1,6 +1,11 @@ import command_base, lp_events, scripts, variables, sys +from constants import * -lib = "cmds_rpnc" # name of this library (for logging) +LIB = "cmds_rpnc" # name of this library (for logging) + +# note that if you don't like RPN and prefer to write algebraic expressions +# all you need to do is create a command that converts algebraic commands to +# postfix (RPN) and you can use the RPN evaluator to process it. # ################################################## # ### CLASS RpnCalc_Rpn_Eval ### @@ -13,6 +18,151 @@ # in the symbol table. In this version The output is to the log, but it # is easily extended. class RpnCalc_Rpn_Eval(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RPN_EVAL", # the name of the command as you have to enter it in the code + LIB) + + # this command does not have a standard list of fields, so we need to do some stuff manually + self.valid_max_params = 255 # There is no maximum, but this is a reasonable limit! + self.valid_num_params = [1, None] # one or more is OK + + self.run_states = [RS_INIT, RS_RUN, RS_FINAL] # No need for anything other than running. + self.validation_states = [VS_PASS_1] # No need for anything other than the first pass + + # Create a register for the sub-commands + self.operators = dict() + + # Now register the operators + self.Register_operators() + + + # We can simply override the first pass validation + def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + # validate the number of parameters + ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols) + + if ret == True: + c_len = len(split_line) # Number of tokens + while i < c_len: # for each item of the line of tokens + cmd = split_line[i] # get the current one + + n = None + try: + n = float(cmd) # we'll be happy with a float (since an int is a subset) + except ValueError: + pass + else: + i += 1 # move along to the next token + continue + + if n == None: + opr = cmd.upper() # Convert to uppercase for searching + if opr in self.operators: # if it's valid + for p in range(self.operators[opr][1]): + if i + p + 1 >= c_len: + return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + else: + param = split_line[i+p+1] + if not variables.valid_var_name(param): + return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, line) + i = i + 1 + self.operators[opr][1] # pull of additional parameters if required + if i > c_len: + return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + else: # if invalid, report it + return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + + return ret + + + # define how to process. We could override something at a lower level, but + # this retains any initialisation and finalization and simplifies return + # requirements + def Process(self, idx, split_line, symbols, coords, is_async): + print("[" + self.lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # coords[BC_TEXT] is the text "(x, y)" + + i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params + + while i < len(split_line): # for each item of the line of tokens + cmd = split_line[i] # get the current one + + n = None # what we get if it's not a number + try: + n = int(cmd) # is it an integer? + except ValueError: + try: + n = float(cmd) # how about a float? + except ValueError: + pass + + if n != None: # if it was one of the above + symbols[SYM_STACK].append(n) # ...put on the stack + i += 1 # move along to the next token + continue + + opr = cmd.upper() # Convert to uppercase for searching + if opr in self.operators: # if it's valid + try: + i = i + self.operators[opr][0](symbols, opr, split_line[i:]) # run it + except: + print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") + break + else: # if invalid, report it + print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") + break + + return idx+1 # Normal default exit to the next line + + + # register all the operators for RPN_EVAL: + def Register_operators(self): + # operator function # params + self.operators["+"] = (self.add, 0) # + + self.operators["-"] = (self.subtract, 0) # - + self.operators["*"] = (self.multiply, 0) # * + self.operators["/"] = (self.divide, 0) # / + self.operators["//"] = (self.i_div, 0) # integer division + self.operators["MOD"] = (self.mod, 0) # modulus function + self.operators["VIEW"] = (self.view, 0) # View X + self.operators["VIEW_S"] = (self.view_s, 0) # View stack + self.operators["VIEW_L"] = (self.view_l, 0) # View local vars + self.operators["VIEW_G"] = (self.view_g, 0) # View global vars + self.operators["1/X"] = (self.one_on_x, 0) # 1/x + self.operators["INT"] = (self.int_x, 0) # integer portion of x + self.operators["FRAC"] = (self.frac_x, 0) # fractional part of x + self.operators["CHS"] = (self.chs, 0) # change sign of top of stack + self.operators["SQR"] = (self.sqr, 0) # **2 + self.operators["Y^X"] = (self.y_to_x, 0) # ** + self.operators["DUP"] = (self.dup, 0) # Duplicate top of stack + self.operators["POP"] = (self.pop, 0) # remove item from top of stack + self.operators["CLST"] = (self.clst, 0) # clear stack + self.operators["LASTX"] = (self.last_x, 0) # get the last value of x + self.operators["CL_L"] = (self.cl_l, 0) # clear local variables + self.operators["STACK"] = (self.stack_len, 0) # length of stack + self.operators["X<>Y"] = (self.swap_x_y, 0) # swap x and y + self.operators[">"] = (self.sto, 1) # store + self.operators[">L"] = (self.sto_l, 1) # store local + self.operators[">G"] = (self.sto_g, 1) # store global + self.operators["<"] = (self.rcl, 1) # recall + self.operators["Y?"] = (self.x_gt_y, 0) # is x > y? + self.operators["X>=Y?"] = (self.x_ge_y, 0) # is x >= y? + self.operators["XY"] = (self.swap_x_y, 0) # swap x and y - self.operators[">"] = (self.sto, 1) # store - self.operators[">L"] = (self.sto_l, 1) # store local - self.operators[">G"] = (self.sto_g, 1) # store global - self.operators["<"] = (self.rcl, 1) # recall - self.operators["Y?"] = (self.x_gt_y, 0) # is x > y? - self.operators["X>=Y?"] = (self.x_ge_y, 0) # is x >= y? - self.operators["X= c_len: - return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, line) - else: - param = split_line[i+p+1] - if not variables.valid_var_name(param): - return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, line) - i = i + 1 + self.operators[opr][1] # pull of additional parameters if required - if i > c_len: - return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) - else: # if invalid, report it - return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) - - return True - - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[0] + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # coords[0] is the text "(x, y)" - - i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params - - while i < len(split_line): # for each item of the line of tokens - cmd = split_line[i] # get the current one - - n = None # what we get if it's not a number - try: - n = int(cmd) # is it an integer? - except ValueError: - try: - n = float(cmd) # how about a float? - except ValueError: - pass - - if n != None: # if it was one of the above - symbols['stack'].append(n) # ...put on the stack - i += 1 # move along to the next token - else: - opr = cmd.upper() # Convert to uppercase for searching - if opr in self.operators: # if it's valid - try: - i = i + self.operators[opr][0](symbols, opr, split_line[i:]) # run it - except: - print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") - break - else: # if invalid, report it - print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") - break - - return idx+1 # Normal default exit to the next line - - scripts.add_command(RpnCalc_Rpn_Eval()) # register the command diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..91fbeb4 --- /dev/null +++ b/constants.py @@ -0,0 +1,62 @@ +# Constants used all over the place. An excuse to use "from constants import *" + +# Symbol table constants +SYM_REPEATS = "repeats" # current value for loops +SYM_ORIGINAL = "original" # initial values for loops +SYM_LABELS = "labels" # location of labels +SYM_MOUSE = "m_pos*" # save location of mouse +SYM_LOCAL = "l_vars" # local variables for script +SYM_GLOBAL = "g_vars" # global variables and lock +SYM_STACK = "stack" # stack for script +SYM_PARAMS = "params" # params for script +SYM_PARAM_CNT = "param_cnt" # count of params passed to command + +# constants for run state +RS_INIT = 0 +RS_GET = 1 +RS_INFO = 2 +RS_VALIDATE = 3 +RS_RUN = 4 +RS_FINAL = 5 + +# constants for validation state +VS_COUNT = 0 # Count is done prior to pass 1 +VS_PASS_1 = 1 +VS_PASS_2 = 2 + +# Constants for auto validation +AV_DESCRIPTION = 0 +AV_OPTIONAL = 1 +AV_VAR_OK = 2 +AV_TYPE = 3 # This is a tuple +AVT_DESC = 0 # and this is what is inside the tuple +AVT_CONV = 1 +AVT_SPECIAL = 2 +AV_P1_VALIDATION = 4 +AV_P2_VALIDATION = 5 + +# constants for parameter types +# desc conv special (special means additional auto-validation +PT_INT = ("int", int, False) +PT_FLOAT = ("float", float, False) +PT_TEXT = ("text", str, False) +PT_LABEL = ("label", str, True) # Note that this is for a reference to a label, not the definition of a label! +PT_TARGET = ("target", str, True) # Note that this is for the definition of a target (e.g. creating a label) + +# constants for auto_message +AM_COUNT = 0 +AM_FORMAT = 1 + +AM_PREFIX = "[{0}] {1} Line:{2}" +AM_DEFAULT = AM_PREFIX + " {3} parameters ({4})" + +# constants for button coords +BC_TEXT = 0 +BC_X = 1 +BC_Y = 2 + +# Misc constants +COLOR_PRIMED = 5 #red +COLOR_FUNC_KEYS_PRIMED = 9 #amber +EXIT_UPDATE_DELAY = 0.1 +DELAY_EXIT_CHECK = 0.025 diff --git a/parse.py b/parse.py index cc1cd0d..a7b39d9 100644 --- a/parse.py +++ b/parse.py @@ -15,6 +15,8 @@ def set_var(var_string, val): if isinstance(val, str): value = variables[val] variables[var_string] = value + else: + raise def get_var(var_string): try: diff --git a/scripts.py b/scripts.py index 7a3e877..921baae 100644 --- a/scripts.py +++ b/scripts.py @@ -2,6 +2,7 @@ from time import sleep from functools import partial import lp_events, lp_colors, kb, sound, ms, files, command_base +from constants import * # VALID_COMMAND is a dictionary of all commands available. @@ -21,10 +22,6 @@ GLOBAL_LOCK = threading.Lock() # a lock got the globals to prevent simultaneous access -COLOR_PRIMED = 5 #red -COLOR_FUNC_KEYS_PRIMED = 9 #amber -EXIT_UPDATE_DELAY = 0.1 -DELAY_EXIT_CHECK = 0.025 # Add a new command. This removes any existing command of the same name from the VALID_COMMANDS @@ -58,13 +55,13 @@ def new_symbol_table(): # returns a new (blank) symbol table # symbol table is dictionary of objects symbols = { - "repeats": dict(), - "original": dict(), - "labels": dict(), - "m_pos": tuple(), - "g_vars": [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) - "l_vars": dict(), # local (to the script) variables (with no lock) - "stack": [] } # script stack (for RPN_EVAL) + SYM_REPEATS: dict(), + SYM_ORIGINAL: dict(), + SYM_LABELS: dict(), + SYM_MOUSE: tuple(), + SYM_GLOBAL: [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) + SYM_LOCAL: dict(), # local (to the script) variables (with no lock) + SYM_STACK: [] } # script stack (for RPN_EVAL) return symbols @@ -95,8 +92,7 @@ def __init__( self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? - # Do what is required to parse the script. Parsing does not output any information unless it is an error - + # Do what is required to parse the script. Parsing does not output any information unless it is an error def parse_script(self): if self.validated: # we don't want to repeat validation over and over return True @@ -111,7 +107,7 @@ def parse_script(self): err = True errors = 0 # no errors found - for pass_no in (1,2): # pass 1, collect info & syntax check, + for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, # pass 2 symbol check & assocoated processing for idx,line in enumerate(self.script_lines): # gen line number and text if self.is_ignorable_line(line): @@ -236,7 +232,6 @@ def run_script_and_run_next(self): # run a script - def run_script(self): lp_colors.updateXY(self.x, self.y) @@ -293,7 +288,6 @@ def main_logic(idx): # validating a script consists of doing the checks that we do prior to running, but # we won't run it afterwards. - def validate_script(self): if self.validated or self.script_str == "": # If valid or there is no script... self.validated = True @@ -317,7 +311,6 @@ def validate_script(self): # bind a button - def bind(x, y, script_str, color): global to_run global buttons @@ -338,7 +331,6 @@ def bind(x, y, script_str, color): # unbind a button - def unbind(x, y): global to_run global buttons @@ -362,8 +354,7 @@ def unbind(x, y): files.layout_changed_since_load = True # Mark the layout as changed -# swap details for two buttons - +# swap details for two buttons def swap(x1, y1, x2, y2): global text @@ -387,7 +378,6 @@ def swap(x1, y1, x2, y2): # Duplicate a button - def copy(x1, y1, x2, y2): global buttons @@ -404,7 +394,6 @@ def copy(x1, y1, x2, y2): # move a button - def move(x1, y1, x2, y2): global buttons @@ -423,7 +412,6 @@ def move(x1, y1, x2, y2): # determine if a key is bound - def is_bound(x, y): global buttons @@ -434,7 +422,6 @@ def is_bound(x, y): # Unbind all keys. - def unbind_all(): global buttons global to_run @@ -454,5 +441,3 @@ def unbind_all(): files.layout_changed_since_load = False # So mark it as unchanged - - diff --git a/variables.py b/variables.py index 41eb37e..8459b89 100644 --- a/variables.py +++ b/variables.py @@ -1,3 +1,5 @@ +from constants import * + # operations needed to access variables # NOTE that any locking is the responsibility of the calling code! @@ -8,7 +10,7 @@ def pop(syms): # return the top valie from the stack (and remove it) try: - return syms['stack'].pop() # take the top value from the stack in the supplied symbol table + return syms[SYM_STACK].pop() # take the top value from the stack in the supplied symbol table except: return 0 # raise Exception("Stack empty") @@ -16,7 +18,7 @@ def pop(syms): def push(syms, val): # put val on to the top of the stack - syms['stack'].append(val) # Push a value onto the stack in the supplied symbol table + syms[SYM_STACK].append(val) # Push a value onto the stack in the supplied symbol table # the top of the stack will also return 0 for an empty stack. Alternatively it could @@ -24,7 +26,7 @@ def push(syms, val): def top(syms, i): # peek at the top value of the stack without removing it (for i=1, y:i=2, z:i=3...) try: - return syms['stack'][-i] + return syms[SYM_STACK][-i] except: return 0 #raise Exception("Stack empty") @@ -95,6 +97,13 @@ def error_msg(idx, name, desc, p, param, err): # check the number of parameters allowed def Check_num_params(split_line, lens, idx, line, name): + # lens is an array of valid numbers of parameters + # it will be None if you've taken control of handling the parameters yourself. + # if you set it to [n, None] that means any number of parameters from n to infinity! + + if lens == None: # if this is undefined + return True # anything is valid + n = len(split_line)-1 if n in lens: return True @@ -111,28 +120,30 @@ def Check_num_params(split_line, lens, idx, line, name): cnt = "" if len(lens) == 1: cnt += str(lens[0]) + elif len(lens) == 2 and lens[1] == None: + cnt += str(lens[0]) + " or more" else: - cnt += ", ".join([str(el) for el in lens[0:-1]]) + ", " + str(lens[-1]) + cnt += ", ".join([str(el) for el in lens[0:-1]]) + ", or " + str(lens[-1]) return (error_msg(idx, name, msg, None, str(n), "supplied, " + cnt + " are required"), line) # check a generic parameter -def Check_generic_param(split_line, p, desc, idx, name, line, conv, conv_name, validation=None, optional=False, var_ok=True): +def Check_generic_param(split_line, p, desc, idx, name, line, v_type, validation=None, optional=False, var_ok=True): temp = None if p >= len(split_line): if optional: return True else: - return (error_msg(idx, name, desc, p, None, 'required ' + conv_name + ' parameter not present'), line) + return (error_msg(idx, name, desc, p, None, 'required ' + v_type[AVT_TYPE] + ' parameter not present'), line) try: - temp = conv(split_line[p]) + temp = v_type[AVT_CONV](split_line[p]) except: if var_ok and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + conv_name), line) + return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + v_type[AVT_TYPE]), line) if validation: return validation(temp, idx, name, desc, p, split_line[p]) @@ -140,6 +151,7 @@ def Check_generic_param(split_line, p, desc, idx, name, line, conv, conv_name, v return True +# @@@ deprecated def Check_numeric_param(split_line, p, desc, idx, name, line, validation, optional=False, var_ok=True): temp = None @@ -165,9 +177,9 @@ def Check_numeric_param(split_line, p, desc, idx, name, line, validation, option # get the value of a parameter def get_value(v, symbols): if valid_var_name(v): - g_vars = symbols['g_vars'] + g_vars = symbols[SYM_GLOBAL] with g_vars[0]: # lock the globals while we do this - v = get(v, symbols['l_vars'], g_vars[1]) + v = get(v, symbols[SYM_LOCAL], g_vars[1]) return v @@ -200,3 +212,6 @@ def Validate_ge_zero(v, idx, name, desc, p, param): return error_msg(idx, name, desc, p, param, 'must not be less than zero') else: return error_msg(idx, name, desc, p, param, 'must be an integer') + + + From 95b6500afb25c7d6606a159810f6c2b74fdaff9e Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 16 Sep 2020 20:31:13 +0800 Subject: [PATCH 18/83] Completed refactoring of flow control commands including the creation of a generic flow control class that makes all the other commands really simple. * new class Control_Flow_Basic * fixed problems with boolean return values matching true/false for some values * Fixed problem with auto-validate code to continue to work without auto-validation supplied. * further change to support the [n,None] definition for parameters --- command_base.py | 29 +- commands_control.py | 668 +++++++++++++++++++------------------------- commands_rpncalc.py | 3 +- variables.py | 10 +- 4 files changed, 316 insertions(+), 394 deletions(-) diff --git a/command_base.py b/command_base.py index d4f0757..3880451 100644 --- a/command_base.py +++ b/command_base.py @@ -86,7 +86,7 @@ def Validate( finally: if type(ret) == tuple: return ret - elif ret == None or ret == True: + elif ret == None or ((type(ret) == bool) and ret): return True else: return ("", "") @@ -133,10 +133,10 @@ def Parse( ret = self.Validate(idx, line, lines, split_line, symbols, pass_no) - if ret == True: + if ((type(ret) == bool) and ret): return True - if ret == None or ret == False or len(ret) != 2: + if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), line) if ret[0]: @@ -170,27 +170,27 @@ def Run( if RS_INIT in self.run_states: # Do the initialisation if required (highly recommended) ret = self.Partial_run_step_init(ret, idx, split_line, symbols, coords, is_async) - if ret == -1 or ret == False: + if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_GET in self.run_states: # Get the parameters if required ret = self.Partial_run_step_get(ret, idx, split_line, symbols, coords, is_async) - if ret == -1 or ret == False: + if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_INFO in self.run_states: # Display info if required ret = self.Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) - if ret == -1 or ret == False: + if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_VALIDATE in self.run_states: # Validate the parameters if required ret = self.Partial_run_step_validate(ret, idx, split_line, symbols, coords, is_async) - if ret == -1 or ret == False: + if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_RUN in self.run_states: # Actualy do the command! (calls Perform() ret = self.Partial_run_step_run(ret, idx, split_line, symbols, coords, is_async) - if ret == -1 or ret == False: + if ret == -1 or ((type(ret) == bool) and not ret): return ret except: @@ -205,7 +205,7 @@ def Run( # Make sure the return values are tidied up. if type(ret) == int: return ret - elif ret == None or ret == True: + elif ret == None or ((type(ret) == bool) and ret): return idx+1 else: return -1 @@ -336,7 +336,7 @@ def Validate_param_count(self, ret, idx, line, lines, split_line, symbols): # Whilst you can override this method, you're more likely to override the Validation_step_count() # method which does no more than just call this. - if not (ret == None or ret == True): + if not (ret == None or ((type(ret) == bool) and ret)): return ret return variables.Check_num_params(split_line, self.valid_num_params, idx, line, self.name) @@ -347,7 +347,7 @@ def Validate_params(self, ret, val_validation, idx, line, lines, split_line, sym # call the validation of the parameters one by one. If you haven't set up the maximum parameters # (if you haven't used the auto_validate structure) then you can override this to validate each # of your parameters. You will need to remember that this gets called for both pass 1 and 2. - if not (ret == None or ret == True): + if not (ret == None or ((type(ret) == bool) and ret)): return ret for i in range(self.valid_max_params): @@ -368,9 +368,12 @@ def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, # Where a variable type is defined as having "special" validation, that validation is currently # hard coded here. It would be better to register validation routines, but... later. - if not (ret == None or ret == True): + if not (ret == None or ((type(ret) == bool) and ret)): return ret + if self.auto_validate == None: # no auto validation can be done + return ret + if n <= len(self.auto_validate): # the normal auto-validation val = self.auto_validate[n-1] @@ -381,7 +384,7 @@ def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, ret = variables.Check_generic_param(split_line, n, val[AV_DESCRIPTION], idx, self.name, line, val_t, val[val_validation], val[AV_OPTIONAL], val[AV_VAR_OK]) # should we do special validation? - if ret == True or ret == None: + if ret == None or ((type(ret) == bool) and ret): if not val_t[AVT_SPECIAL]: return True diff --git a/commands_control.py b/commands_control.py index ef8af85..b109ee2 100644 --- a/commands_control.py +++ b/commands_control.py @@ -37,11 +37,12 @@ def __init__( # class that defines the LABEL command (a target of GOTO's etc) class Control_Label(command_base.Command_Basic): - def __init__( - self, + def __init__( + self ): - super().__init__("LABEL", # the name of the command as you have to enter it in the code + super().__init__( + "LABEL", # the name of the command as you have to enter it in the code LIB, ( # Desc Opt Var type p1_val p2_val @@ -59,30 +60,153 @@ def __init__( scripts.add_command(Control_Label()) # register the command +# ################################################## +# ### CLASS Control_Flow_Basic ### +# ################################################## + +# class that defines an object that can handle flow control +# +# THIS IS NOT REGISTERED. IT IS AN ANCESTOR CLASS FOR OTHER MORE POWERFUL COMMANDS +class Control_Flow_Basic(command_base.Command_Basic): + def __init__( + self, + name: str, # The name of the command (what you put in the script) + lib=LIB, + auto_validate=None, # Definition of the input parameters + auto_message=None, # Definition of the message format + invalid_message=None, # Info message if invalid + valid_function=None, # Test to be performed to determine validity + label_preceeds=False, # must the label preceed this line + reset=False, # do we do reset at end of loop? + loop_val_init_function=None, # How to initialize the loop counter + next_function=None, # Passed the current value, return the next + test_function=None # Test to be performed before looping None = loop always + ): + + super().__init__(name, # the name of the command as you have to enter it in the code + lib, + auto_validate, + auto_message); + + # note that it is safe to have these extra variables in the class, as they are + # constant for a given child class. + self.invalid_message = invalid_message + self.valid_function = valid_function + self.label_preceeds = label_preceeds + self.reset = reset + self.loop_val_init_function = loop_val_init_function + self.next_function = next_function + self.test_function = test_function + + + def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + ret = super().Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) + + if ret == None or ((type(ret) == bool) and ret): + if self.loop_val_init_function: + self.loop_val_init_function(symbols, idx, split_line) + ret = True + + return ret + + + def Partial_validate_step_pass_2(self, ret, idx, line, lines, split_line, symbols): + ret = super().Partial_validate_step_pass_2(ret, idx, line, lines, split_line, symbols) + + if (ret == None or ((type(ret) == bool) and ret)): + if self.label_preceeds and symbols[SYM_LABELS][split_line[1]] < idx: + ret = ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) + + return ret + + + def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + ret = super().Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) + + if self.valid_function == None or self.valid_function(coords): # if no validation function, or it returns true, continue + if self.test_function and self.next_function: + if symbols[SYM_REPEATS][idx] > 0: + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + else: + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " No repeats left, not repeating.") + else: + print(self.invalid_message) + + return ret + + + def Process(self, idx, split_line, symbols, coords, is_async): + ret = idx+1 # if all else fails! + + if self.valid_function == None or self.valid_function(coords): # if no validation function, or it returns true, continue + + if self.next_function: # if we can calc the next value of the loop + val = symbols[SYM_REPEATS][idx] # get this value + symbols[SYM_REPEATS][idx] = self.next_function(val) # and calculate the next + + if not (self.test_function or self.next_function): # it's either both or none at the moment, if neither + ret = symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # this is unconditional + elif (self.test_function and self.next_function): # if both, then we can do the test + if self.test_function(val): + ret = symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # jump if test succeeds + else: + if self.reset: + self.Reset(symbols, idx) # potential reset if it doesn't + + return ret + + + def Valid_key_pressed(self, coords): + return lp_events.pressed[coords[BC_X]][coords[BC_Y]] + + + def Valid_key_unpressed(self, coords): + return not self.Valid_key_pressed(coords) + + + def Test_func_ge_zero(self, val): + return val >= 0 + + + def Next_decrement(self, val): + return val-1 + + + def Reset(self, symbols, idx): + symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] + + + def Init_n(self, symbols, idx, split_line): + symbols[SYM_ORIGINAL][idx] = int(split_line[2]) + self.Reset(symbols, idx) + + + def Init_n_minus_1(self, symbols, idx, split_line): + symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 + self.Reset(symbols, idx) + + # ################################################## # ### CLASS Control_Goto_Label ### # ################################################## # class that defines the GOTO_LABEL command -class Control_Goto_Label(command_base.Command_Basic): +class Control_Goto_Label(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("GOTO_LABEL", # the name of the command as you have to enter it in the code + super().__init__( + "GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type conv p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Label", False, False, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) (1, " Goto label {1}"), - ) ) - - - def Process(self, idx, split_line, symbols, coords, is_async): - return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # we simply return the line number the label is on + ) ) # don't even need the additional parameters! scripts.add_command(Control_Goto_Label()) # register the command @@ -93,26 +217,25 @@ def Process(self, idx, split_line, symbols, coords, is_async): # ################################################## # class that defines the IF_PRESSED_GOTO_LABEL command -class Control_If_Pressed_Goto_Label(command_base.Command_Basic): +class Control_If_Pressed_Goto_Label(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + super().__init__( + "IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type conv p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) - (1, " If pressed goto label {1}"), - ) ) - - - def Process(self, idx, split_line, symbols, coords, is_async): - if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: # if key is pressed - return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # and we return the line number the label is on + (1, " if pressed goto label {1}"), + ), + "the button is not pressed", + self.Valid_key_pressed + ) scripts.add_command(Control_If_Pressed_Goto_Label()) # register the command @@ -123,26 +246,25 @@ def Process(self, idx, split_line, symbols, coords, is_async): # ################################################## # class that defines the IF_UNPRESSED_GOTO_LABEL command -class Control_If_Unpressed_Goto_Label(command_base.Command_Basic): +class Control_If_Unpressed_Goto_Label(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + super().__init__( + "IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type conv p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) - (1, " If unpressed goto label {1}"), - ) ) - - - def Process(self, idx, split_line, symbols, coords, is_async): - if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: # if key is pressed - return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # and we return the line number the label is on + (1, " if unpressed goto label {1}"), + ), + "the button is pressed", + self.Valid_key_unpressed + ) scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command @@ -153,54 +275,30 @@ def Process(self, idx, split_line, symbols, coords, is_async): # ################################################## # class that defines the REPEAT_LABEL command -class Control_Repeat_Label(command_base.Command_Basic): +class Control_Repeat_Label(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("REPEAT_LABEL", # the name of the command as you have to enter it in the code + super().__init__( LIB, ( - # Desc Opt Var type p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) (2, " Repeat label {1}, {2} times max"), - ) ) - - - def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): - ret = super().Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) - - if ret == None or ret == True: - symbols[SYM_REPEATS][idx] = int(split_line[2]) - symbols[SYM_ORIGINAL][idx] = int(split_line[2]) - ret = True - - return ret - - - def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): - # Oddly enough, we want the original info message too. - ret = super().Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) - - if symbols[SYM_REPEATS][idx] > 0: - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - else: - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " No repeats left, not repeating.") - - return ret - - - def Process(self, idx, split_line, symbols, coords, is_async): - - if symbols[SYM_REPEATS][idx] > 0: - symbols[SYM_REPEATS][idx] = symbols[SYM_REPEATS][idx] - 1 - return symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] - - return True + ), + None, + None, + False, + False, + self.Init_n, + self.Next_decrement, + self.Test_func_ge_zero + ) scripts.add_command(Control_Repeat_Label()) # register the command @@ -213,66 +311,31 @@ def Process(self, idx, split_line, symbols, coords, is_async): # class that defines the REPEAT command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end -class Control_Repeat(command_base.Command_Basic): +class Control_Repeat(Control_Flow_Basic): def __init__( - self, - ): - - super().__init__("REPEAT") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - else: - symbols[SYM_REPEATS][idx] = int(split_line[2])-1 - symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols[SYM_LABELS]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - if symbols[SYM_LABELS][split_line[1]] > idx: - return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + self ): - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Repeat LABEL " + split_line[1] + " " + \ - split_line[2] + " times max") - - if symbols[SYM_REPEATS][idx] > 0: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - symbols[SYM_REPEATS][idx] -= 1 - return symbols[SYM_LABELS][split_line[1]] - else: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # makes this behave like a normal loop - return idx+1 + super().__init__( + "REPEAT", # the name of the command as you have to enter it in the code + LIB, + ( + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " repeat {1}, {2} times max"), + ), + None, + None, + True, + True, + self.Init_n_minus_1, + self.Next_decrement, + self.Test_func_ge_zero + ) scripts.add_command(Control_Repeat()) # register the command @@ -282,71 +345,35 @@ def Run( # ### CLASS Control_If_Pressed_Repeat_Label ### # ################################################## -# class that defines the IF_PRESSED_REPEAT_LABEL command -class Control_If_Pressed_Repeat_Label(command_base.Command_Basic): +# class that defines the IF_PRESSED_REPEAT_LABEL command. +class Control_If_Pressed_Repeat_Label(Control_Flow_Basic): def __init__( - self, - ): - - super().__init__("IF_PRESSED_REPEAT_LABEL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + " requires a minimum of 1 repeat.", line) - else: - symbols[SYM_REPEATS][idx] = int(split_line[2]) - symbols[SYM_ORIGINAL][idx] = int(split_line[2]) - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols[SYM_LABELS]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + self ): - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - - if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist - print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: - if symbols[SYM_REPEATS][idx] > 0: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - symbols[SYM_REPEATS][idx] -= 1 - return symbols[SYM_LABELS][split_line[1]] - else: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - - return idx+1 + super().__init__( + "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " If key is pressed repeat label {1}, {2} times max"), + ), + "the button is umpressed", + self.Valid_key_pressed, + False, + False, + self.Init_n, + self.Next_decrement, + self.Test_func_ge_zero + ) -scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command +# scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command # ################################################## @@ -356,72 +383,31 @@ def Run( # class that defines the IF_PRESSED command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end -class Control_If_Pressed_Repeat(command_base.Command_Basic): +class Control_If_Pressed_Repeat(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("IF_PRESSED_REPEAT") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - else: - symbols[SYM_REPEATS][idx] = int(split_line[2])-1 - symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols[SYM_LABELS]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - if symbols[SYM_LABELS][split_line[1]] > idx: - return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) - - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is pressed repeat " + split_line[1] + " " + split_line[2] + " times max") - - if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist - print("Line:" + str(idx+1) + " - Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if lp_events.pressed[coords[BC_X]][coords[BC_Y]]: - if symbols[SYM_REPEATS][idx] > 0: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - symbols[SYM_REPEATS][idx] -= 1 - return symbols[SYM_LABELS][split_line[1]] - else: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # for a normal repeat statement - - return idx+1 + super().__init__( + "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " If key is not pressed repeat label {1}, {2} times max"), + ), + "the button is not pressed", + self.Valid_key_pressed, + True, + True, + self.Init_n, + self.Next_decrement, + self.Test_func_ge_zero + ) scripts.add_command(Control_If_Pressed_Repeat()) # register the command @@ -432,66 +418,31 @@ def Run( # ################################################## # class that defines the IF_UNPRESSED_REPEAT_LABEL command. -class Control_If_Unpressed_Repeat_Label(command_base.Command_Basic): +class Control_If_Unpressed_Repeat_Label(Control_Flow_Basic): def __init__( - self, + self ): - super().__init__("IF_UNPRESSED_REPEAT_LABEL") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - symbols[SYM_REPEATS][idx] = int(split_line[2]) - symbols[SYM_ORIGINAL][idx] = int(split_line[2]) - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols[SYM_LABELS]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is not pressed repeat label " + split_line[1] + " " + split_line[2] + " times max") - - if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist - print(" Line:" + str(idx+1) + " Missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: - if symbols[SYM_REPEATS][idx] > 0: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - symbols[SYM_REPEATS][idx] -= 1 - return symbols[SYM_LABELS][split_line[1]] - else: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - - return idx+1 + super().__init__( + "IF_UNPRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + LIB, + ( + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " If key is not pressed repeat label {1}, {2} times max"), + ), + "the button is pressed", + self.Valid_key_unpressed, + False, + False, + self.Init_n, + self.Next_decrement, + self.Test_func_ge_zero + ) scripts.add_command(Control_If_Unpressed_Repeat_Label()) # register the command @@ -504,72 +455,32 @@ def Run( # class that defines the IF_UNPRESSED_REPEAT command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end -class Control_If_Unpressed_Repeat(command_base.Command_Basic): +class Control_If_Unpressed_Repeat(Control_Flow_Basic): def __init__( - self, - ): - - super().__init__("IF_UNPRESSED_REPEAT") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + self ): - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) != 3: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + "' needs both a label name and how many times to repeat.", line) - - try: - temp = int(split_line[2]) - if temp < 1: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " requires a minimum of 1 repeat.", line) - symbols[SYM_REPEATS][idx] = int(split_line[2])-1 - symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 - except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) - - if pass_no == 2: # in Pass 2 we can check to make sure referenced symbols exist - if split_line[1] not in symbols[SYM_LABELS]: - return ("Line:" + str(idx+1) + " - Target not found for " + self.name, line) - - if symbols[SYM_LABELS][split_line[1]] > idx: - return ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) - - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " If key is not pressed repeat " + split_line[1] + " " + split_line[2] + " times max") - - if not split_line[1] in symbols[SYM_LABELS]: # The label should always exist - print("missing LABEL '" + split_line[1] + "'") # otherwise an error - return -1 - else: - if not lp_events.pressed[coords[BC_X]][coords[BC_Y]]: - if symbols[SYM_REPEATS][idx] > 0: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") - symbols[SYM_REPEATS][idx] -= 1 - return symbols[SYM_LABELS][split_line[1]] - else: - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No repeats left, not repeating.") - symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] # to behave more normal - - return idx+1 - + super().__init__( + "IF_UNPRESSED_REPEAT", # the name of the command as you have to enter it in the code + LIB, + ( + # desc opt var type p1_val p2_val + ("label", False, False, PT_LABEL, None, None), + ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " If key is not pressed repeat {1}, {2} times max"), + ), + "the button is pressed", + self.Valid_key_unpressed, + True, + True, + self.Init_n_minus_1, + self.Next_decrement, + self.Test_func_ge_zero + ) + scripts.add_command(Control_If_Unpressed_Repeat()) # register the command @@ -579,6 +490,9 @@ def Run( # ################################################## # class that defines the RESET_REPEATS command +# +# Here's a command that could just be defined into action, but the +# basic implementation using the low level interface is so simple. class Control_Reset_Repeats(command_base.Command_Basic): def __init__( self, @@ -586,6 +500,7 @@ def __init__( super().__init__("RESET_REPEATS") # the name of the command as you have to enter it in the code + def Validate( self, idx: int, # The current line number @@ -601,6 +516,7 @@ def Validate( return True + def Run( self, idx: int, # The current line number @@ -619,5 +535,3 @@ def Run( scripts.add_command(Control_Reset_Repeats()) # register the command - - diff --git a/commands_rpncalc.py b/commands_rpncalc.py index d9a820a..89949ba 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -44,8 +44,9 @@ def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbol # validate the number of parameters ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols) - if ret == True: + if ((type(ret) == bool) and ret): c_len = len(split_line) # Number of tokens + i = 1 while i < c_len: # for each item of the line of tokens cmd = split_line[i] # get the current one diff --git a/variables.py b/variables.py index 8459b89..946ef68 100644 --- a/variables.py +++ b/variables.py @@ -104,8 +104,12 @@ def Check_num_params(split_line, lens, idx, line, name): if lens == None: # if this is undefined return True # anything is valid + ln = len(lens) n = len(split_line)-1 - if n in lens: + if ln == 2 and lens[1] == None: + if n >= lens[0]: + return True + elif n in lens: return True # create a properly formatted error message @@ -136,14 +140,14 @@ def Check_generic_param(split_line, p, desc, idx, name, line, v_type, validation if optional: return True else: - return (error_msg(idx, name, desc, p, None, 'required ' + v_type[AVT_TYPE] + ' parameter not present'), line) + return (error_msg(idx, name, desc, p, None, 'required ' + v_type[AVT_DESC] + ' parameter not present'), line) try: temp = v_type[AVT_CONV](split_line[p]) except: if var_ok and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + v_type[AVT_TYPE]), line) + return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + v_type[AVT_DESC]), line) if validation: return validation(temp, idx, name, desc, p, split_line[p]) From 1340b52da490ce4dbed73eab11c5f07ea710eaf7 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 20 Sep 2020 00:03:29 +0800 Subject: [PATCH 19/83] Continues bug fixes/conversion of commands to latest structure * definition pf PT_KEY to do auto-validation of "keys" parameters * addition of Command_Text_Basic class as a descendent of Command_Basic that handles commands with "text" parameters. * conversion of comment command to use Command_Text_Basic * addition of 2 new commands END and ABORT that do the same thing (terminate the script) * conversion of keys commands to use latest command model (as yet untested) * documentation of new commands * bug fix for handling of comments in scripts.py * noting the position in scripts.py where the problem with "second button presses" occurs. I think that the use of PyPI would help here because *reasons* --- README.md | 4 + command_base.py | 33 +++++ commands_control.py | 64 +++++++-- commands_keys.py | 313 ++++++++++++++------------------------------ constants.py | 1 + scripts.py | 13 +- 6 files changed, 197 insertions(+), 231 deletions(-) diff --git a/README.md b/README.md index 02e8f88..96f1a65 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,12 @@ If this is used, all other lines in the file must either be blank lines or comme Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text file with newlines separating commands. #### Utility [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +* `ABORT` + * Terminates the script immediately, logging any message after the command. This has the same functionality as END, however it carries with it the notion that the termination was abnormal. * `DELAY` * Delays the script for (argument 1) seconds. +* `END` + * Terminates the script immediately, logging any message after the command. This has the same functionality as ABORT, however it indicates a normal termination. * `GOTO_LABEL` * Goto label (argument 1). * `IF_PRESSED_GOTO_LABEL` diff --git a/command_base.py b/command_base.py index 3880451..2562d09 100644 --- a/command_base.py +++ b/command_base.py @@ -396,6 +396,10 @@ def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, # add label to symbol table # Add the new label to the labels in the symbol table symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number + elif val_t == PT_KEY: # targets (label definitions) have pass 1 validation only + # check for valid key + if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? + return ("Unknown key", line) elif val_validation == AV_P2_VALIDATION: if val_t == PT_LABEL: # references (to a label) have pass 2 validation only @@ -459,6 +463,35 @@ def Run_param_n(self, ret, idx, n, split_line, symbols, coords, is_async, pass_n return ret +# ################################################## +# ### CLASS Command_Text_Basic ### +# ################################################## + +# class that defines an object that can handle just text after the command +class Command_Text_Basic(Command_Basic): + def __init__( + self, + name: str, # The name of the command (what you put in the script) + lib, + info_msg): # what we display before the text + + super().__init__(name, # the name of the command as you have to enter it in the code + lib, + (), + () ) + + # this command does not have a standard list of fields, so we need to do some stuff manually + self.valid_max_params = 32767 # There is no maximum, but this is a reasonable limit! + self.valid_num_params = [0, None] # zero or more is OK + + self.info_msg = info_msg # customised message text before parameter text + + + def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + self.info_msg + " " + " ".join(split_line[1:]).strip()) + + + # ################################################## # ### CLASS Command_Header ### # ################################################## diff --git a/commands_control.py b/commands_control.py index b109ee2..6795c2c 100644 --- a/commands_control.py +++ b/commands_control.py @@ -10,23 +10,15 @@ # class that defines the comment command (single quote at beginning of line) # this is special because it has some different handling in the main code # to allow it to work without a space following it -class Control_Comment(command_base.Command_Basic): +class Control_Comment(command_base.Command_Text_Basic): def __init__( self, ): super().__init__("-", # the name of the command as you have to enter it in the code LIB, - (), - () ) + "-" ) - # this command does not have a standard list of fields, so we need to do some stuff manually - self.valid_max_params = 32767 # There is no maximum, but this is a reasonable limit! - self.valid_num_params = [0, None] # zero or more is OK - - #self.run_states = [RS_INIT, RS_INFO, RS_FINAL] # No need to do anything at all for a comment, but let's display it - #self.validation_states = [] # And no validation either - scripts.add_command(Control_Comment()) # register the command @@ -53,9 +45,6 @@ def __init__( (1, " Label {1}"), ) ) - #self.run_states = [RS_INIT, RS_INFO, RS_FINAL] # No need to do anything at all for a label, but let's display it - #self.validation_states = [VS_PASS_1] # We need to do pass 1 validation - scripts.add_command(Control_Label()) # register the command @@ -373,7 +362,7 @@ def __init__( ) -# scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command +scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command # ################################################## @@ -535,3 +524,50 @@ def Run( scripts.add_command(Control_Reset_Repeats()) # register the command + + +# ################################################## +# ### CLASS Control_End ### +# ################################################## + +# class that defines the END command +# +# This command simply ends the current script. I'm going to be working on subroutines, so this is a good +# start. The parameters to this command are simply the message it will print. +# This is really like a comment that returns the next line as -1 +class Control_End(command_base.Command_Text_Basic): + def __init__( + self, + ): + + super().__init__("END", # the name of the command as you have to enter it in the code + LIB, + "SCRIPT ENDED" ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + return -1 + + +scripts.add_command(Control_End()) # register the command + + +# ################################################## +# ### CLASS Control_Abort ### +# ################################################## + +# class that defines the ABORT command +# +# This is effectively the same as END, but the message (and the implication) is different +class Control_Abort(Control_End): + def __init__( + self, + ): + + super().__init__() + + self.name = "ABORT" + self.info_msg = "SCRIPT ABORTED" + + +scripts.add_command(Control_Abort()) # register the command diff --git a/commands_keys.py b/commands_keys.py index 0a03e7e..a7996d8 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -1,6 +1,7 @@ -import command_base, kb, lp_events, scripts +import command_base, kb, lp_events, scripts, variables +from constants import * -lib = "cmds_ctrl" # name of this library (for logging) +LIB = "cmds_keys" # name of this library (for logging) # ################################################## # ### CLASS Keys_Wait_Pressed ### @@ -12,41 +13,24 @@ def __init__( self, ): - super().__init__("WAIT_PRESSED") # the name of the command as you have to enter it in the code + super().__init__( + "WAIT_PRESSED", # the name of the command as you have to enter it in the code + LIB, + (), + () ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): + def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " Wait for script key to be unpressed") - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Wait for script key to be unpressed") + def Process(self, idx, split_line, symbols, coords, is_async): while lp_events.pressed[coords[BC_X]][coords[BC_Y]]: sleep(DELAY_EXIT_CHECK) if check_kill(x, y, is_async): return idx + 1 - return idx+1 + return idx + 1 scripts.add_command(Keys_Wait_Pressed()) # register the command @@ -62,71 +46,57 @@ def __init__( self, ): - super().__init__("TAP") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - if len(split_line) > 4: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - if kb.sp(split_line[1]) == None: - return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - key = kb.sp(split_line[1]) - - releasefunc = lambda: kb.release(key) - - if len(split_line) <= 2: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1]) - kb.tap(key) - elif len(split_line) <= 3: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + " times") - taps = int(split_line[2]) - - for tap in range(taps): - if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): - return idx + 1 + super().__init__( + "TAP", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Key", False, False, PT_KEY, None, None), + ("Times", True, True, PT_INT, variables.Validate_gt_zero, None), + ("Duration", True, True, PT_FLOAT, variables.Validate_ge_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Tap key {1}"), + (2, " Tap key {1}, {2} times"), + (3, " Tap key {1}, {2} times for {3} seconds each"), + ) ) + + + def Process(self, idx, split_line, symbols, coords, is_async): + cnt = symbols[SYM_PARAM_CNT] + key = kb.sp(symbols[SYM_PARAMS][1]) + releasefunc = lambda: None + + times = 1 + if cnt >= 2: + times = int(symbols[SYM_PARAMS][2]) + + delay = 0 + if cnt == 3: + delay = float(symbols[SYM_PARAMS][3]) + releasefunc = lambda: kb.release(key) + + precheck = delay == 0 and times > 1 + + for tap in range(taps): + if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): + return idx+1 + + if delay == 0: kb.tap(key) - else: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Tap key " + split_line[1] + " " + split_line[2] + \ - " times for " + str(split_line[3]) + " seconds each") - - taps = int(split_line[2]) - delay = float(split_line[3]) - - for tap in range(taps): - if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): - return -1 - + else: kb.press(key) + + if precheck and check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): + return -1 + + if delay > 0: if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async, releasefunc): return -1 - - return idx+1 - + + releasefunc() + scripts.add_command(Keys_Tap()) # register the command @@ -141,46 +111,23 @@ def __init__( self, ): - super().__init__("PRESS") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): + super().__init__( + "PRESS", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Key", False, False, PT_KEY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Press key {1}"), + ) ) - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - if kb.sp(split_line[1]) == None: - return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Press key " + split_line[1]) - - key = kb.sp(split_line[1]) + def Process(self, idx, split_line, symbols, coords, is_async): + key = kb.sp(symbols[SYM_PARAMS][1]) kb.press(key) - return idx+1 - scripts.add_command(Keys_Press()) # register the command @@ -195,46 +142,23 @@ def __init__( self, ): - super().__init__("RELEASE") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + super().__init__( + "RELEASE", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Key", False, False, PT_KEY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Release key {1}"), + ) ) - if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - if kb.sp(split_line[1]) == None: - return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Release key " + split_line[1]) - - key = kb.sp(split_line[1]) + def Process(self, idx, split_line, symbols, coords, is_async): + key = kb.sp(symbols[SYM_PARAMS][1]) kb.release(key) - return idx+1 - scripts.add_command(Keys_Release()) # register the command @@ -249,39 +173,19 @@ def __init__( self, ): - super().__init__("RELEASE_ALL") # the name of the command as you have to enter it in the code + super().__init__( + "RELEASE_ALL", # the name of the command as you have to enter it in the code + LIB, + (), + ( + # num params, format string (trailing comma is important) + (0, " Release all keys"), + ) ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Release all keys") + def Process(self, idx, split_line, symbols, coords, is_async): kb.release_all() - return idx+1 - scripts.add_command(Keys_Release_All()) # register the command @@ -291,28 +195,16 @@ def Run( # ################################################## # class that defines the STRING command (type a string) -class Keys_String(command_base.Command_Basic): +class Keys_String(command_base.Command_Text_Basic): def __init__( - self, - ): - - super().__init__("STRING") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): + self ): + + super().__init__("STRING", # the name of the command as you have to enter it in the code + LIB, + "Type out string" ) - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + self.valid_num_params = [1, None] # There is a minimum - return True def Run( self, @@ -324,9 +216,6 @@ def Run( ): type_string = " ".join(split_line[1:]) - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Type out string " + type_string) - kb.write(type_string) return idx+1 diff --git a/constants.py b/constants.py index 91fbeb4..6395d13 100644 --- a/constants.py +++ b/constants.py @@ -42,6 +42,7 @@ PT_TEXT = ("text", str, False) PT_LABEL = ("label", str, True) # Note that this is for a reference to a label, not the definition of a label! PT_TARGET = ("target", str, True) # Note that this is for the definition of a target (e.g. creating a label) +PT_KEY = ("key", str, True) # This is a key literal # constants for auto_message AM_COUNT = 0 diff --git a/scripts.py b/scripts.py index 921baae..7a5d069 100644 --- a/scripts.py +++ b/scripts.py @@ -185,6 +185,9 @@ def schedule_script(self): if self.thread != None: if self.thread.is_alive(): + # @@@ The following code creates a problem if a script is looking for a second keypress + # @@@ Maybe we need an option to make a script un-interruptable, or alternately require + # @@@ *something* else (maybe ctrl-alt) to be pressed to allow the kill to take place. print("[scripts] " + self.coords + " Script already running, killing script....") self.thread.kill.set() return @@ -263,11 +266,11 @@ def main_logic(idx): else: split_line = line.split(" ") - if split_line[0] in VALID_COMMANDS: # if first element is a command - command = VALID_COMMANDS[split_line[0]] # get the command - return command.Run(idx, split_line, self.symbols, (self.coords, self.x, self.y), self.is_async) - else: - print("[scripts] " + self.coords + " Invalid command: " + split_line[0] + ", skipping...") + if split_line[0] in VALID_COMMANDS: # if first element is a command + command = VALID_COMMANDS[split_line[0]] # get the command + return command.Run(idx, split_line, self.symbols, (self.coords, self.x, self.y), self.is_async) + else: + print("[scripts] " + self.coords + " Invalid command: " + split_line[0] + ", skipping...") return idx + 1 From 55818fa128a7cdfd2baac81ffeea153df08e90ec Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 20 Sep 2020 19:03:16 +0800 Subject: [PATCH 20/83] Refactoring to pass the button object rather than information about the button. This will (in future) allow the buttons to be associated with something other than the 8x8 grid of the LaunchPad. One reason for this is to allow scripts to be called as subroutines without tying up a button. * line, lines, and symbols passed to routines replaced by btn object. * removal of constants associated with coords * created a new class command_text_basic that leaves all the parameters unchanged in split_line[1]. This makes it easier to deal with things like double spaces. * changed the parsing of commands that have no upper limit on their parameters to scan only the number that are present * corrected a very silly error with REPEAR_LABEL * Added an 'abort' command to RPN_EVAL to allow it to cause the script to exit. * Refactored *some* of the external commands. Commands that ideally require multiple strings, or strings with values interspersed are somewhat problematic. to handle transparently. * this version is probably suitable for initial testing. * NewCommands.md is getting very out of date... --- LPHK.py | 2 +- NewCommands.md | 66 ++++++------ README.md | 4 +- command_base.py | 196 +++++++++++++++++----------------- commands_control.py | 127 +++++++++++----------- commands_external.py | 247 +++++++++++++------------------------------ commands_header.py | 48 ++++----- commands_keys.py | 61 +++++------ commands_mouse.py | 100 +++++++++--------- commands_pause.py | 22 ++-- commands_rpncalc.py | 42 ++++++-- constants.py | 5 - files.py | 6 +- scripts.py | 158 ++++++++++++++++----------- variables.py | 54 +++++----- window.py | 16 +-- 16 files changed, 547 insertions(+), 607 deletions(-) diff --git a/LPHK.py b/LPHK.py index 214fc64..d0fa263 100755 --- a/LPHK.py +++ b/LPHK.py @@ -118,7 +118,7 @@ def shutdown(): if scripts.buttons[x][y].thread != None: scripts.buttons[x][y].thread.kill.set() if window.lp_connected: - scripts.unbind_all() + scripts.Unbind_all() lp_events.timer.cancel() launchpad_connector.disconnect(lp) window.lp_connected = False diff --git a/NewCommands.md b/NewCommands.md index 9181c0e..44628e7 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -125,7 +125,7 @@ The `M_SCROLL` command looks like this: self.Partial_run(*params, [RS_FINAL]) -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` We will examine the 6 parts you need to consider @@ -253,7 +253,7 @@ No matter how we return, the FINALLY block will ensure that the finalization is The final step is to include code to incorporate this command into the set of commands available for scripts. ```python -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` This line creates a command object, and passes it to the routine which adds it to the list of available commands. @@ -311,7 +311,7 @@ class Mouse_Scroll(command_base.Command_Basic): ): if not self.auto_validate or len(self.auto_validate) != 2: - return ("Invalid command setup", line) + return ("Invalid command setup", btn.line[idx]) ret = True @@ -322,11 +322,11 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.check_numeric_param(split_line, 1, self.auto_validate[1][0], idx, self.name, line, variables.validate_ge_zero, self.auto_validate[1][1], self.auto_validate[1][2]) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) ret = variables.check_generic_param(split_line, 2, self.auto_validate[1][0], idx, self.name, line, self.auto_validate[1][4], self.auto_validate[1][3], None, False, True) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) return ret @@ -360,13 +360,13 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if not ok: @@ -380,7 +380,7 @@ class Mouse_Scroll(command_base.Command_Basic): return idx+1 -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` We will examine the 6 parts you need to consider @@ -439,7 +439,7 @@ Note that commands are case sensitive, so the name should be in all uppercase to ): if not self.auto_validate or len(self.auto_validate) != 2: - return ("Invalid command setup", line) + return ("Invalid command setup", btn.line[idx]) ret = True @@ -450,11 +450,11 @@ Note that commands are case sensitive, so the name should be in all uppercase to ret = variables.check_numeric_param(split_line, 1, self.auto_validate[1][0], idx, self.name, line, variables.validate_ge_zero, self.auto_validate[1][1], self.auto_validate[1][2]) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) ret = variables.check_generic_param(split_line, 2, self.auto_validate[1][0], idx, self.name, line, self.auto_validate[1][4], self.auto_validate[1][3], None, False, True) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) return ret ``` @@ -492,13 +492,13 @@ Note that commands are case sensitive, so the name should be in all uppercase to ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if not ok: @@ -512,7 +512,7 @@ Note that commands are case sensitive, so the name should be in all uppercase to return idx+1 -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` ##### Part 6 - Command Integration @@ -579,11 +579,11 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) return True @@ -617,13 +617,13 @@ class Mouse_Scroll(command_base.Command_Basic): ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if not ok: @@ -637,7 +637,7 @@ class Mouse_Scroll(command_base.Command_Basic): return idx+1 -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` We will examine the 6 parts you need to consider @@ -700,16 +700,16 @@ Every command requires a validation. If you do not provide validation code, the if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions # check number of split_line if len(split_line) != 2: - return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.ame, line) + return ("Line:" + str(idx+1) + " - Wrong number of parameters in " + self.ame, btn.line[idx]) try: temp = int(split_line[1]) if valid_var_name(temp): if temp < 1: - return ("Line:" + str(idx+1) + " - '" + split_line[0] + " parameter 1 must be a positive number.", line) + return ("Line:" + str(idx+1) + " - '" + split_line[0] + " parameter 1 must be a positive number.", btn.line[idx]) except: - return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - " + split_line[0] + " number of repeats '" + split_line[2] + "' not valid.", btn.line[idx]) if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions ret = variables.check_num(split_line, [1, 2], idx, line, self.name) @@ -718,11 +718,11 @@ Every command requires a validation. If you do not provide validation code, the ret = variables.check_int_param(split_line, 1, "X value", idx, self.name, line, variables.validate_int_ge_zero) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) ret = variables.check_int_param(split_line, 2, "Scroll amount", idx, self.name, line, variables.validate_int_ge_zero, True) if ret != True: - return (ret, line) + return (ret, btn.line[idx]) return True ``` @@ -776,13 +776,13 @@ Every command that does something (e.g. not labels - that have their effect duri ret = variables.validate_int_ge_zero(v1, idx, self.name, "X amount", 1, split_line[1]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if v2: ret = variables.validate_int_ge_zero(v2, idx, self.name, "Scroll amount", 2, split_line[2]) if ret != True: - print("[" + lib + "] " + coords[BC_TEXT] + " " + ret) + print("[" + lib + "] " + btn.coords + " " + ret) ok = False if not ok: @@ -817,7 +817,7 @@ idx + 1 is returned. This points to the nect line to execute, which in this ins The final step is to include code to incorporate this command into the set of commands available for scripts. ```python -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command ``` This line creates a command object, and passes it to the routine which adds it to the list of available commands. @@ -907,7 +907,7 @@ class Header_Async(command_base.Command_Header): return idx+1 -scripts.add_command(Header_Async()) # register the header +scripts.Add_command(Header_Async()) # register the header ``` This consists of almost exactly the same 5 parts, but with important differences. @@ -969,7 +969,7 @@ Every command requires a validation. If you do not provide validation code, the return (self.name + " must appear on the first line.", lines[0]) if len(split_line) > 1: - return (self.name + " takes no arguments.", line) + return (self.name + " takes no arguments.", btn.line[idx]) return True ``` @@ -1004,7 +1004,7 @@ If the header's function is performed during validation (`@ASYNC` is) then there The final step has the same form, behavior, and cautions as for commands. ```python -scripts.add_command(Header_Async()) # register the header +scripts.Add_command(Header_Async()) # register the header ``` ## Registration @@ -1097,9 +1097,9 @@ This structure contains the list of values that make up the stack for the curren The coords are the current x,y values passed to the command (or header). I believe these are the button coordinates. This array contains 3 elements: - * coords[BC_TEXT] - a string describing the location - * coords[BC_X] - the X value - * coords[BC_Y] - the Y value + * btn.coords - a string describing the location + * btn.x - the X value + * btn.y - the Y value ## self.Name diff --git a/README.md b/README.md index 96f1a65..771f0e3 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,8 @@ All Mouse movement commands can now use variables in place of constants. Variab * !?L {x} - Does a local variable {x} not exist * ?G {x} - Does a global variable {x} exist * !?G {x} - Does a global variable {x} not exist + * One command allows you to affect script execution + * abort - causes the script to terminate * The stack is local to the current script, however it is maintained between executions! * The global variables are global to all scripts. * Local variables are local to the current script (and are maintained across executions) @@ -489,7 +491,7 @@ In order of priority: * Move `@SIMPLE` to keyboard module. * Allow F['COMMAND']['macro'] = True to disallow other non-comment lines in the script. Default is False. * Macros will automatically have `_` added to the beginning (`@` will only be for headers) - * `validate_script()` will take care of making sure macros are alone (after comment/nl stripping) + * `Validate_script()` will take care of making sure macros are alone (after comment/nl stripping) * Allow F['COMMAND']['macro_async'] = True to enable async on a macro. Default is False, ignored if not a macro. * When importing functions on startup, make a dict to keep track of what macros are async * `scripts.py` will have a `run_async` dict to keep track of if a script is async diff --git a/command_base.py b/command_base.py index 2562d09..28a89d0 100644 --- a/command_base.py +++ b/command_base.py @@ -1,4 +1,4 @@ -import variables +import variables, kb from constants import * # ################################################## @@ -47,11 +47,9 @@ def Validate( # This method should return True if the validation was successful, otherwise it should # return a tuple of the error message and the line causing the error. self, + btn, idx: int, - line, - lines, split_line, - symbols, pass_no # pass_no 1 is a symbol gathering pass, pass_no 2 is a pass that requires # symbols. Any processing that does not set up labels should be done on # pass 2. For example, goto can be checked on pass 2 to ensure the label @@ -66,17 +64,17 @@ def Validate( if pass_no == VS_PASS_1: # validate the count if required if VS_COUNT in self.validation_states: - ret = self.Partial_validate_step_count(ret, idx, line, lines, split_line, symbols) + ret = self.Partial_validate_step_count(ret, btn, idx, split_line) # do pass 1 validation if required if VS_PASS_1 in self.validation_states: - ret = self.Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) + ret = self.Partial_validate_step_pass_1(ret, btn, idx, split_line) # if it's pass 2 elif pass_no == VS_PASS_2: # call Pass 2 if required if VS_PASS_1 in self.validation_states: - ret = self.Partial_validate_step_pass_2(ret, idx, line, lines, split_line, symbols) + ret = self.Partial_validate_step_pass_2(ret, btn, idx, split_line) except: import traceback @@ -92,37 +90,35 @@ def Validate( return ("", "") - def Partial_validate_step_count(self, ret, idx, line, lines, split_line, symbols): + def Partial_validate_step_count(self, ret, btn, idx, split_line): # Validation of the count is separated from the pass 1 validation because sometinmes # you want to override one but not the other. You would override this if you have some # odd way of counting parameters, or the count depends on something complex. - ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols) + ret = self.Validate_param_count(ret, btn, idx, split_line) - def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): # Pass 1 Validation is typically defined by the auto_validate structure set up in the # command initialisation. You would override this if you haven't defined this, or the # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. - ret = self.Validate_params(ret, AV_P1_VALIDATION, idx, line, lines, split_line, symbols) + ret = self.Validate_params(ret, btn, idx, split_line, AV_P1_VALIDATION) - def Partial_validate_step_pass_2(self, ret, idx, line, lines, split_line, symbols): + def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): # Pass 2 Validation is typically defined by the auto_validate structure set up in the # command initialisation. You would override this if you haven't defined this, or the # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. - ret = self.Validate_params(ret, AV_P2_VALIDATION, idx, line, lines, split_line, symbols) + ret = self.Validate_params(ret, btn, idx, split_line, AV_P2_VALIDATION) def Parse( # Parse is pretty much a call to Validate, except the output of the validation is immediately printed. self, + btn, idx: int, - line, - lines, split_line, - symbols, pass_no # pass_no 1 is a symbol gathering pass, pass_no 2 is a pass that requires # symbols. Any processing that does not set up labels should be done on # pass 2. Fatal errors can be generated on pass 1 or 2 for invalid syntax. @@ -131,13 +127,13 @@ def Parse( # past pass 1). ): - ret = self.Validate(idx, line, lines, split_line, symbols, pass_no) + ret = self.Validate(btn, idx, split_line, pass_no) if ((type(ret) == bool) and ret): return True if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: - ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), line) + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.line[idx]) if ret[0]: print(ret[0]) @@ -158,38 +154,36 @@ def Run( # To cause the script to jump to a different line, simply return the line number you wish to go to. self, + btn, idx: int, - split_line, - symbols, - coords, - is_async + split_line ): try: ret = None # this is an invalid return value, but it indicates nothing has happened yet if RS_INIT in self.run_states: # Do the initialisation if required (highly recommended) - ret = self.Partial_run_step_init(ret, idx, split_line, symbols, coords, is_async) + ret = self.Partial_run_step_init(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_GET in self.run_states: # Get the parameters if required - ret = self.Partial_run_step_get(ret, idx, split_line, symbols, coords, is_async) + ret = self.Partial_run_step_get(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_INFO in self.run_states: # Display info if required - ret = self.Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) + ret = self.Partial_run_step_info(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_VALIDATE in self.run_states: # Validate the parameters if required - ret = self.Partial_run_step_validate(ret, idx, split_line, symbols, coords, is_async) + ret = self.Partial_run_step_validate(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret if RS_RUN in self.run_states: # Actualy do the command! (calls Perform() - ret = self.Partial_run_step_run(ret, idx, split_line, symbols, coords, is_async) + ret = self.Partial_run_step_run(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret @@ -200,7 +194,7 @@ def Run( finally: if RS_FINAL in self.run_states: # Do the finalisation if required (highly recommended) - self.Partial_run_step_final(ret, idx, split_line, symbols, coords, is_async) + self.Partial_run_step_final(ret, btn, idx, split_line) # Make sure the return values are tidied up. if type(ret) == int: @@ -211,7 +205,7 @@ def Run( return -1 - def Partial_run_step_init(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_init(self, ret, btn, idx, split_line): # information about *this* run of the command MUST be in the symbol table # You might be tempted to not run the init if the variables below aren't needed, @@ -219,21 +213,21 @@ def Partial_run_step_init(self, ret, idx, split_line, symbols, coords, is_async) # If you need more temporary data, you can override this, call the ancestor, and # create what you need. - symbols[SYM_PARAMS] = [self.name] + [None] * self.valid_max_params - symbols[SYM_PARAM_CNT] = 0 + btn.symbols[SYM_PARAMS] = [self.name] + [None] * self.Param_validation_count(len(split_line)-1) + btn.symbols[SYM_PARAM_CNT] = 0 return ret - def Partial_run_step_get(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_get(self, ret, btn, idx, split_line): # This gets the values from the command, including fetching variable values. # After this is run, parameters will be in the symbol table, but those coming # from variables will not have been validated. - ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 1) + ret = self.Run_params(ret, btn, idx, split_line, VS_PASS_1) return ret - def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_info(self, ret, btn, idx, split_line): # This step matches the number of parameters passed with the definitions for messages, # printing the matching message, or a default message if no matching message can be found. @@ -242,52 +236,52 @@ def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async) # If you're overriding the method, you will rarely want to call the ancestor method. msg = False if self.auto_message: - params = symbols[SYM_PARAMS] - param_cnt = symbols[SYM_PARAM_CNT] - 1 + params = btn.symbols[SYM_PARAMS] + param_cnt = btn.symbols[SYM_PARAM_CNT] for msg_def in self.auto_message: if msg_def[AM_COUNT] == param_cnt: - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + msg_def[AM_FORMAT].format(*params)) + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + msg_def[AM_FORMAT].format(*params)) msg = True break if not msg: - print(AM_DEFAULT.format(self.lib, coords[BC_TEXT], str(idx+1), self.name, str(params))) + print(AM_DEFAULT.format(self.lib, btn.coords, str(idx+1), self.name, str(params))) return ret - def Partial_run_step_validate(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_validate(self, ret, btn, idx, split_line): # This step performs run-time validation of values passed from variables. # Those that come from variables are checked using the validation method passed. - ret = self.Run_params(ret, idx, split_line, symbols, coords, is_async, 2) + ret = self.Run_params(ret, btn, idx, split_line, VS_PASS_2) return ret - def Partial_run_step_run(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_run(self, ret, btn, idx, split_line): # This performs the running of the command. Because this does nothing it is pretty much # ALWAYS overridden. However, to make life easier, this function calls the Process() # method. That's simpler to override. - ret = self.Process(idx, split_line, symbols, coords, is_async) + ret = self.Process(btn, idx, split_line) if ret == None: ret = idx + 1 return ret - def Partial_run_step_final(self, ret, idx, split_line, symbols, coords, is_async): + def Partial_run_step_final(self, ret, btn, idx, split_line): # This removes stuff from the symbol table that isn't needed any more. Whilst it may be fairly # superfluous at present, if you start to put more stuff in the symbol table during the execution # of a command this might start to get more important. # If you override this, it is conventional to call the ancestor function last, but there's no reason # at present that you must. - del symbols[SYM_PARAMS] - del symbols[SYM_PARAM_CNT] + del btn.symbols[SYM_PARAMS] + del btn.symbols[SYM_PARAM_CNT] return ret - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): # This is the default process called to run a command. Override it to do something other than # nothing at runtime. @@ -330,7 +324,7 @@ def Calc_valid_param_counts(self): return ret - def Validate_param_count(self, ret, idx, line, lines, split_line, symbols): + def Validate_param_count(self, ret, btn, idx, split_line): # Should only be called from pass 1 (actually within VS_COUNT that happens just prior to # VS_PASS_1 @@ -339,10 +333,20 @@ def Validate_param_count(self, ret, idx, line, lines, split_line, symbols): if not (ret == None or ((type(ret) == bool) and ret)): return ret - return variables.Check_num_params(split_line, self.valid_num_params, idx, line, self.name) - + return variables.Check_num_params(btn, self, idx, split_line) + + + def Param_validation_count(self, n_passed): + # This routine determines how many parameters to check. In cases where there are unlimited parameters, + # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. + # This function improves efficiency. + if self.valid_max_params < n_passed or (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): + return n_passed + else: + return self.valid_max_params + - def Validate_params(self, ret, val_validation, idx, line, lines, split_line, symbols): + def Validate_params(self, ret, btn, idx, split_line, val_validation): # This command is called from both pass 1 and 2 of validation It is really just a method to # call the validation of the parameters one by one. If you haven't set up the maximum parameters # (if you haven't used the auto_validate structure) then you can override this to validate each @@ -350,15 +354,15 @@ def Validate_params(self, ret, val_validation, idx, line, lines, split_line, sym if not (ret == None or ((type(ret) == bool) and ret)): return ret - for i in range(self.valid_max_params): - ret = self.Validate_param_n(ret, i+1, val_validation, idx, line, lines, split_line, symbols) + for i in range(self.Param_validation_count(len(split_line)-1)): + ret = self.Validate_param_n(ret, btn, idx, split_line, val_validation, i+1) if ret != True: return ret return ret - def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, symbols): + def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # This method validates parameters. For custom parameters, you're best off defining new validation # methods (like the current variables.Validate_gt_zero()) unless you need access to the symbol # table. @@ -380,39 +384,38 @@ def Validate_param_n(self, ret, n, val_validation, idx, line, lines, split_line, opt = self.valid_num_params == [] or (set(range(1,n)) & set(self.valid_num_params)) != [] - val_t = val[AV_TYPE] - ret = variables.Check_generic_param(split_line, n, val[AV_DESCRIPTION], idx, self.name, line, val_t, val[val_validation], val[AV_OPTIONAL], val[AV_VAR_OK]) + ret = variables.Check_generic_param(btn, self, idx, split_line, n, val, val_validation) # should we do special validation? if ret == None or ((type(ret) == bool) and ret): - if not val_t[AVT_SPECIAL]: + if not val[AV_TYPE][AVT_SPECIAL]: return True if val_validation == AV_P1_VALIDATION: - if val_t == PT_TARGET: # targets (label definitions) have pass 1 validation only + if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only # check for duplicate label - if split_line[n] in symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", line) + if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? + return ("Duplicate LABEL", btn.line[idx]) - # add label to symbol table # Add the new label to the labels in the symbol table - symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number - elif val_t == PT_KEY: # targets (label definitions) have pass 1 validation only + # add label to symbol table # Add the new label to the labels in the symbol table + btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number + elif val[AV_TYPE] == PT_KEY: # targets (label definitions) have pass 1 validation only # check for valid key - if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? - return ("Unknown key", line) + if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? + return ("Unknown key", btn.line[idx]) elif val_validation == AV_P2_VALIDATION: - if val_t == PT_LABEL: # references (to a label) have pass 2 validation only + if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only # check for existance of label - if split_line[n] not in symbols[SYM_LABELS]: - return ("Target not found", line) + if split_line[n] not in btn.symbols[SYM_LABELS]: + return ("Target not found", btn.Line(idx)) return True - return (ret, line) + return (ret, btn.line[idx]) - def Run_params(self, ret, idx, split_line, symbols, coords, is_async, pass_no): + def Run_params(self, ret, btn, idx, split_line, pass_no): # This method gets the parameters. Oddly enough it has 2 passes too. The first pass simply gets the # variables, while the second pass gets them and does validation. @@ -421,23 +424,25 @@ def Run_params(self, ret, idx, split_line, symbols, coords, is_async, pass_no): ret = True if pass_no == 1: - param_cnt = len(split_line) - symbols[SYM_PARAM_CNT] = param_cnt + param_cnt = len(split_line) - 1 + btn.symbols[SYM_PARAM_CNT] = param_cnt + btn.symbols[SYM_PARAMS][0] = split_line[0] - for i in range(self.valid_max_params): + for i in range(self.Param_validation_count(param_cnt)): if i < param_cnt: - symbols[SYM_PARAMS][i+1] = self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no) + btn.symbols[SYM_PARAMS][i+1] = self.Run_param_n(ret, btn, idx, split_line, pass_no, i+1) elif pass_no == 2: # for pass 2 we don't try to validate null variables - for i in range(self.valid_max_params): - if symbols[SYM_PARAMS][i+1] != None: - ret = self.Run_param_n(ret, idx, i+1, split_line, symbols, coords, is_async, pass_no) + param_cnt = len(split_line) - 1 + for i in range(self.Param_validation_count(param_cnt)): + if btn.symbols[SYM_PARAMS][i+1] != None: + ret = self.Run_param_n(ret, btn, idx, split_line, pass_no, i+1) return ret - def Run_param_n(self, ret, idx, n, split_line, symbols, coords, is_async, pass_no): + def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): # This function gets called to firstly get the parameter (pass_no = 1) and then # to validate it (with pass_no = 2) @@ -446,20 +451,21 @@ def Run_param_n(self, ret, idx, n, split_line, symbols, coords, is_async, pass_n if pass_no == 1: v = split_line[n] if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK]: - v = variables.get_value(split_line[n], symbols) + v = variables.get_value(split_line[n], btn.symbols) if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) return v elif pass_no == 2: - val = self.auto_validate[n-1] - ok = ret - - if val[AV_P1_VALIDATION]: - ok = val[AV_P1_VALIDATION](symbols[SYM_PARAMS][n], idx, self.name, val[AV_DESCRIPTION], n, split_line[n]) - if ok != True: - print("[" + self.lib + "] " + coords[BC_TEXT] + " " + ok) - ret = -1 - + if len(self.auto_validate) != 0: + val = self.auto_validate[n-1] + ok = ret + + if val[AV_P1_VALIDATION]: + ok = val[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, val[AV_DESCRIPTION], n, split_line[n]) + if ok != True: + print("[" + self.lib + "] " + btn.coords + " " + ok) + ret = -1 + return ret @@ -484,12 +490,14 @@ def __init__( self.valid_max_params = 32767 # There is no maximum, but this is a reasonable limit! self.valid_num_params = [0, None] # zero or more is OK - self.info_msg = info_msg # customised message text before parameter text - + if "{1}" in info_msg: + self.info_msg = info_msg # customised message text before parameter text + else: + self.info_msg = info_msg + " {1}" - def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + self.info_msg + " " + " ".join(split_line[1:]).strip()) + def Partial_run_step_info(self, ret, btn, idx, split_line): + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " " + self.info_msg.format("", btn.symbols[SYM_PARAMS][1])) # ################################################## @@ -512,16 +520,14 @@ def __init__( def Validate( self, + btn, idx: int, - line, - lines, split_line, - symbols, pass_no ): if idx != 0: - return ("ERROR on line " + line + ". " + self.name + " must only appear on line 1.", -1) + return ("ERROR on line " + btn.Line(idx) + ". " + self.name + " must only appear on line 1.", -1) return (None, 0) diff --git a/commands_control.py b/commands_control.py index 6795c2c..ce7cccd 100644 --- a/commands_control.py +++ b/commands_control.py @@ -20,7 +20,7 @@ def __init__( "-" ) -scripts.add_command(Control_Comment()) # register the command +scripts.Add_command(Control_Comment()) # register the command # ################################################## @@ -46,7 +46,7 @@ def __init__( ) ) -scripts.add_command(Control_Label()) # register the command +scripts.Add_command(Control_Label()) # register the command # ################################################## @@ -88,69 +88,69 @@ def __init__( self.test_function = test_function - def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): - ret = super().Partial_validate_step_pass_1(ret, idx, line, lines, split_line, symbols) + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) if ret == None or ((type(ret) == bool) and ret): if self.loop_val_init_function: - self.loop_val_init_function(symbols, idx, split_line) + self.loop_val_init_function(btn, idx, split_line) ret = True return ret - def Partial_validate_step_pass_2(self, ret, idx, line, lines, split_line, symbols): - ret = super().Partial_validate_step_pass_2(ret, idx, line, lines, split_line, symbols) + def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_2(ret, btn, idx, split_line) if (ret == None or ((type(ret) == bool) and ret)): - if self.label_preceeds and symbols[SYM_LABELS][split_line[1]] < idx: - ret = ("Line:" + str(idx+1) + " - Target for " + self.name + " must preceed the command.", line) + if self.label_preceeds and btn.symbols[SYM_LABELS][split_line[1]] > idx: + ret = ("Line:" + str(idx+1) + " - Target for " + self.name + " (" + split_line[1] + ") must preceed the command.", btn.Line(idx)) return ret - def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): - ret = super().Partial_run_step_info(ret, idx, split_line, symbols, coords, is_async) + def Partial_run_step_info(self, ret, btn, idx, split_line): + ret = super().Partial_run_step_info(ret, btn, idx, split_line) - if self.valid_function == None or self.valid_function(coords): # if no validation function, or it returns true, continue + if self.valid_function == None or self.valid_function(btn): # if no validation function, or it returns true, continue if self.test_function and self.next_function: - if symbols[SYM_REPEATS][idx] > 0: - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " " + str(symbols[SYM_REPEATS][idx]) + " repeats left.") + if btn.symbols[SYM_REPEATS][idx] > 0: + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " " + str(btn.symbols[SYM_REPEATS][idx]) + " repeats left.") else: - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " No repeats left, not repeating.") + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " No repeats left, not repeating.") else: print(self.invalid_message) return ret - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): ret = idx+1 # if all else fails! - if self.valid_function == None or self.valid_function(coords): # if no validation function, or it returns true, continue + if self.valid_function == None or self.valid_function(btn): # if no validation function, or it returns true, continue - if self.next_function: # if we can calc the next value of the loop - val = symbols[SYM_REPEATS][idx] # get this value - symbols[SYM_REPEATS][idx] = self.next_function(val) # and calculate the next + if self.next_function: # if we can calc the next value of the loop + val = btn.symbols[SYM_REPEATS][idx] # get this value + btn.symbols[SYM_REPEATS][idx] = self.next_function(val) # and calculate the next - if not (self.test_function or self.next_function): # it's either both or none at the moment, if neither - ret = symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # this is unconditional - elif (self.test_function and self.next_function): # if both, then we can do the test - if self.test_function(val): - ret = symbols[SYM_LABELS][symbols[SYM_PARAMS][1]] # jump if test succeeds + if not (self.test_function or self.next_function): # it's either both or none at the moment, if neither + ret = btn.symbols[SYM_LABELS][btn.symbols[SYM_PARAMS][1]] # this is unconditional + elif (self.test_function and self.next_function): # if both, then we can do the test + if self.test_function(val-1): + ret = btn.symbols[SYM_LABELS][btn.symbols[SYM_PARAMS][1]] # jump if test succeeds else: if self.reset: - self.Reset(symbols, idx) # potential reset if it doesn't + self.Reset(btn, idx) # potential reset if it doesn't return ret - def Valid_key_pressed(self, coords): - return lp_events.pressed[coords[BC_X]][coords[BC_Y]] + def Valid_key_pressed(self, btn): + return lp_events.pressed[btn.x][btn.y] - def Valid_key_unpressed(self, coords): - return not self.Valid_key_pressed(coords) + def Valid_key_unpressed(self, btn): + return not self.Valid_key_pressed(btn) def Test_func_ge_zero(self, val): @@ -161,18 +161,18 @@ def Next_decrement(self, val): return val-1 - def Reset(self, symbols, idx): - symbols[SYM_REPEATS][idx] = symbols[SYM_ORIGINAL][idx] + def Reset(self, btn, idx): + btn.symbols[SYM_REPEATS][idx] = btn.symbols[SYM_ORIGINAL][idx] - def Init_n(self, symbols, idx, split_line): - symbols[SYM_ORIGINAL][idx] = int(split_line[2]) - self.Reset(symbols, idx) + def Init_n(self, btn, idx, split_line): + btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2]) + self.Reset(btn, idx) - def Init_n_minus_1(self, symbols, idx, split_line): - symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 - self.Reset(symbols, idx) + def Init_n_minus_1(self, btn, idx, split_line): + btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 + self.Reset(btn, idx) # ################################################## @@ -198,7 +198,7 @@ def __init__( ) ) # don't even need the additional parameters! -scripts.add_command(Control_Goto_Label()) # register the command +scripts.Add_command(Control_Goto_Label()) # register the command # ################################################## @@ -227,7 +227,7 @@ def __init__( ) -scripts.add_command(Control_If_Pressed_Goto_Label()) # register the command +scripts.Add_command(Control_If_Pressed_Goto_Label()) # register the command # ################################################## @@ -256,7 +256,7 @@ def __init__( ) -scripts.add_command(Control_If_Unpressed_Goto_Label()) # register the command +scripts.Add_command(Control_If_Unpressed_Goto_Label()) # register the command # ################################################## @@ -270,10 +270,11 @@ def __init__( ): super().__init__( + "REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), + ("Label", False, False, PT_LABEL, None, None), ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), ), ( @@ -290,7 +291,7 @@ def __init__( ) -scripts.add_command(Control_Repeat_Label()) # register the command +scripts.Add_command(Control_Repeat_Label()) # register the command # ################################################## @@ -310,12 +311,12 @@ def __init__( LIB, ( # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), + ("Label", False, False, PT_LABEL, None, None), ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " repeat {1}, {2} times max"), + (2, " Repeat {1}, {2} times max"), ), None, None, @@ -327,7 +328,7 @@ def __init__( ) -scripts.add_command(Control_Repeat()) # register the command +scripts.Add_command(Control_Repeat()) # register the command # ################################################## @@ -362,7 +363,7 @@ def __init__( ) -scripts.add_command(Control_If_Pressed_Repeat_Label()) # register the command +scripts.Add_command(Control_If_Pressed_Repeat_Label()) # register the command # ################################################## @@ -399,7 +400,7 @@ def __init__( ) -scripts.add_command(Control_If_Pressed_Repeat()) # register the command +scripts.Add_command(Control_If_Pressed_Repeat()) # register the command # ################################################## @@ -434,7 +435,7 @@ def __init__( ) -scripts.add_command(Control_If_Unpressed_Repeat_Label()) # register the command +scripts.Add_command(Control_If_Unpressed_Repeat_Label()) # register the command # ################################################## @@ -471,7 +472,7 @@ def __init__( ) -scripts.add_command(Control_If_Unpressed_Repeat()) # register the command +scripts.Add_command(Control_If_Unpressed_Repeat()) # register the command # ################################################## @@ -492,38 +493,34 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.Line(idx)) return True def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): - print("[" + LIB + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Reset all repeats") + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Reset all repeats") - for i in symbols[SYM_REPEATS]: - symbols[SYM_REPEATS][i] = symbols[SYM_ORIGINAL][i] + for i in btn.symbols[SYM_REPEATS]: + btn.symbols[SYM_REPEATS][i] = btn.symbols[SYM_ORIGINAL][i] return idx+1 -scripts.add_command(Control_Reset_Repeats()) # register the command +scripts.Add_command(Control_Reset_Repeats()) # register the command # ################################################## @@ -545,11 +542,11 @@ def __init__( "SCRIPT ENDED" ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): return -1 -scripts.add_command(Control_End()) # register the command +scripts.Add_command(Control_End()) # register the command # ################################################## @@ -570,4 +567,4 @@ def __init__( self.info_msg = "SCRIPT ABORTED" -scripts.add_command(Control_Abort()) # register the command +scripts.Add_command(Control_Abort()) # register the command diff --git a/commands_external.py b/commands_external.py index 3b6ce23..25cebed 100644 --- a/commands_external.py +++ b/commands_external.py @@ -1,59 +1,43 @@ import command_base, webbrowser, sound, subprocess, os, scripts +from constants import * -lib = "cmds_extn" # name of this library (for logging) +LIB = "cmds_extn" # name of this library (for logging) # ################################################## # ### CLASS External_Web ### # ################################################## # class that defines the WEB command -class External_Web(command_base.Command_Basic): +class External_Web(command_base.Command_Text_Basic): def __init__( self, ): - super().__init__("WEB") # the name of the command as you have to enter it in the code + super().__init__( + "WEB", # the name of the command as you have to enter it in the code + LIB, + " Open website '{1}' in default browser" + ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - return True + self.valid_num_params = [1] # one or more is OK - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): + def Partial_run_step_get(self, ret, btn, idx, split_line): + # This gets the values as normal, then modifies them as required + ret = super().Partial_run_step_get(ret, btn, idx, split_line) + link = split_line[1] if "http" not in link: - link = "http://" + link - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open website " + link + " in default browser") + split_line[1] = "http://" + link + + return ret - webbrowser.open(link) - return idx+1 + def Process(self, btn, idx, split_line): + webbrowser.open(btn.symbols[SYM_PARAMS][1]) -scripts.add_command(External_Web()) # register the command +scripts.Add_command(External_Web()) # register the command # ################################################## @@ -61,53 +45,22 @@ def Run( # ################################################## # class that defines the WEB_NEW command -class External_Web_New(command_base.Command_Basic): +class External_Web_New(External_Web): def __init__( self, ): - super().__init__("WEB_NEW") # the name of the command as you have to enter it in the code + super().__init__() - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + self.name = "WEB_NEW" # the name of the command as you have to enter it in the code + self.info_msg = " Open website '{1}' in a new browser" - if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - return True + def Process(self, btn, idx, split_line): + webbrowser.open_new(btn.symbols[SYM_PARAMS][1]) - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - link = split_line[1] - if "http" not in link: - link = "http://" + link - - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open website " + link + " in default browser, try to make a new window") - - webbrowser.open_new(link) - - return idx+1 - - -scripts.add_command(External_Web_New()) # register the command +scripts.Add_command(External_Web_New()) # register the command # ################################################## @@ -115,54 +68,25 @@ def Run( # ################################################## # class that defines the OPEN command -class External_Open(command_base.Command_Basic): +class External_Open(command_base.Command_Text_Basic): def __init__( self, ): - super().__init__("OPEN") # the name of the command as you have to enter it in the code + super().__init__( + "OPEN", # the name of the command as you have to enter it in the code + LIB, + " Open file or location '{1}'" + ) - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) - - path_name = " ".join(split_line[1:]) - - if (not os.path.isfile(path_name)) and (not os.path.isdir(path_name)): - return ("Line:" + str(idx+1) + " - " + split_line[0] + " folder or file location '" + path_name + \ - "' does not exist.", line) + self.valid_num_params = [1] # one or more is OK - return True - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): + def Process(self, btn, idx, split_line): + files.open_file_folder(btn.symbols[SYM_PARAMS][1]) - path_name = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Open file or folder " + path_name) - - files.open_file_folder(path_name) - - return idx+1 - - -scripts.add_command(External_Open()) # register the command +scripts.Add_command(External_Open()) # register the command # ################################################## @@ -179,44 +103,39 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) if len(split_line) > 3: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.line[idx]) return True def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): if len(split_line) > 2: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ " at volume " + str(split_line[2])) sound.play(split_line[1], float(split_line[2])) else: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) sound.play(split_line[1]) return idx+1 -scripts.add_command(External_Sound()) # register the command +scripts.Add_command(External_Sound()) # register the command # ################################################## @@ -229,46 +148,30 @@ def __init__( self, ): - super().__init__("SOUND_STOP") # the name of the command as you have to enter it in the code - - def Validate( - self, - idx: int, # The current line number - line, # The current line - lines, # The current script - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions - if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) - - return True - - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - if len(split_line) > 1: - delay = split_line[1] - print("[scripts] " + coords + - " Line:" + str(idx+1) + " Stopping sounds with " + delay + " milliseconds fadeout time") - sound.fadeout(int(delay)) - else: - print("[scripts] " + coords + " Line:" + str(idx+1) + " Stopping sounds") + super().__init__( + "SOUND_STOP", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Fade value", True , True, PT_INT, variables.Validate_gt_zero, None), + ), + ( + # num params, format string (trailing comma is important) + (0, " Stopping sounds immediately"), + (1, " Stopping sounds with {1} milliseconds fadeout time"), + ) ) + + + def Process(self, btn, idx, split_line): + delay = btn.symbols[SYM_PARAMS][1] + + if delay == None or delay <= 0: sound.stop() - - return idx+1 + else: + sound.fadeout(int(delay)) -scripts.add_command(External_Sound()) # register the command +scripts.Add_command(External_Sound()) # register the command # ################################################## @@ -283,42 +186,40 @@ def __init__( super().__init__("CODE") # the name of the command as you have to enter it in the code + def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) return True + def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): args = " ".join(split_line[1:]) - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Running code: " + args) + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Running code: " + args) try: subprocess.run(args) except Exception as e: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Error with running code: " + str(e)) + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) return idx+1 -scripts.add_command(External_Code()) # register the command +scripts.Add_command(External_Code()) # register the command diff --git a/commands_header.py b/commands_header.py index 5edf48d..4de7dff 100644 --- a/commands_header.py +++ b/commands_header.py @@ -15,11 +15,9 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): @@ -28,24 +26,22 @@ def Validate( return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - " + self.name + " takes no arguments.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " takes no arguments.", btn.line[idx]) return True def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): return idx+1 -scripts.add_command(Header_Async()) # register the header +scripts.Add_command(Header_Async()) # register the header # ################################################## @@ -62,11 +58,9 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): @@ -75,13 +69,13 @@ def Validate( return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - " + self.name + " requires a key to bind.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a key to bind.", btn.line[idx]) if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - " + self.name + " only take one argument", line) + return ("Line:" + str(idx+1) + " - " + self.name + " only take one argument", btn.line[idx]) if kb.sp(split_line[1]) == None: - return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", line) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", btn.line[idx]) for lin in lines[1:]: if lin != "" and lin[0] != "-": @@ -92,11 +86,9 @@ def Validate( def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): print("[cmds_head] " + coords + " Line:" + str(idx+1) + " Simple keybind: " + split_line[1]) @@ -109,7 +101,7 @@ def Run( #WAIT_UNPRESSED while lp_events.pressed[x][y]: sleep(DELAY_EXIT_CHECK) - if check_kill(x, y, is_async, releasefunc): + if btn.Check_kill(releasefunc): return idx + 1 #RELEASE @@ -118,7 +110,7 @@ def Run( return idx+1 -scripts.add_command(Header_Simple()) # register the header +scripts.Add_command(Header_Simple()) # register the header # ################################################## @@ -136,11 +128,9 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): @@ -149,18 +139,16 @@ def Validate( return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - " + self.name + " requires a filename as a parameter.", line) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a filename as a parameter.", btn.line[idx]) return True def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): layout_name = " ".join(split_line[1:]) @@ -186,6 +174,6 @@ def Run( return idx+1 -scripts.add_command(Header_Load_Layout()) # register the header +scripts.Add_command(Header_Load_Layout()) # register the header diff --git a/commands_keys.py b/commands_keys.py index a7996d8..94ae5c0 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -20,20 +20,20 @@ def __init__( () ) - def Partial_run_step_info(self, ret, idx, split_line, symbols, coords, is_async): - print(AM_PREFIX.format(self.lib, coords[BC_TEXT], str(idx+1)) + " Wait for script key to be unpressed") + def Partial_run_step_info(self, ret, btn, idx, split_line): + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " Wait for script key to be unpressed") - def Process(self, idx, split_line, symbols, coords, is_async): - while lp_events.pressed[coords[BC_X]][coords[BC_Y]]: + def Process(self, btn, idx, split_line): + while lp_events.pressed[btn.x][btn.y]: sleep(DELAY_EXIT_CHECK) - if check_kill(x, y, is_async): + if btn.Check_kill(): return idx + 1 return idx + 1 -scripts.add_command(Keys_Wait_Pressed()) # register the command +scripts.Add_command(Keys_Wait_Pressed()) # register the command # ################################################## @@ -63,24 +63,24 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - cnt = symbols[SYM_PARAM_CNT] - key = kb.sp(symbols[SYM_PARAMS][1]) + def Process(self, btn, idx, split_line): + cnt = btn.symbols[SYM_PARAM_CNT] + key = kb.sp(btn.symbols[SYM_PARAMS][1]) releasefunc = lambda: None - times = 1 + taps = 1 if cnt >= 2: - times = int(symbols[SYM_PARAMS][2]) + taps = int(btn.symbols[SYM_PARAMS][2]) delay = 0 if cnt == 3: - delay = float(symbols[SYM_PARAMS][3]) + delay = float(btn.symbols[SYM_PARAMS][3]) releasefunc = lambda: kb.release(key) - precheck = delay == 0 and times > 1 + precheck = delay == 0 and taps > 1 for tap in range(taps): - if check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): + if btn.Check_kill(releasefunc): return idx+1 if delay == 0: @@ -88,17 +88,17 @@ def Process(self, idx, split_line, symbols, coords, is_async): else: kb.press(key) - if precheck and check_kill(coords[BC_X], coords[BC_Y], is_async, releasefunc): + if precheck and btn.Check_kill(releasefunc): return -1 if delay > 0: - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async, releasefunc): + if not btn.Safe_sleep(delay, releasefunc): return -1 releasefunc() -scripts.add_command(Keys_Tap()) # register the command +scripts.Add_command(Keys_Tap()) # register the command # ################################################## @@ -124,12 +124,12 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - key = kb.sp(symbols[SYM_PARAMS][1]) + def Process(self, btn, idx, split_line): + key = kb.sp(btn.symbols[SYM_PARAMS][1]) kb.press(key) -scripts.add_command(Keys_Press()) # register the command +scripts.Add_command(Keys_Press()) # register the command # ################################################## @@ -155,12 +155,12 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - key = kb.sp(symbols[SYM_PARAMS][1]) + def Process(self, btn, idx, split_line): + key = kb.sp(btn.symbols[SYM_PARAMS][1]) kb.release(key) -scripts.add_command(Keys_Release()) # register the command +scripts.Add_command(Keys_Release()) # register the command # ################################################## @@ -183,11 +183,11 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): kb.release_all() -scripts.add_command(Keys_Release_All()) # register the command +scripts.Add_command(Keys_Release_All()) # register the command # ################################################## @@ -208,17 +208,14 @@ def __init__( def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): - type_string = " ".join(split_line[1:]) - kb.write(type_string) + kb.write(split_line[1]) return idx+1 -scripts.add_command(Keys_String()) # register the command +scripts.Add_command(Keys_String()) # register the command diff --git a/commands_mouse.py b/commands_mouse.py index 0fddc66..246b06e 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -26,11 +26,11 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - ms.move_to_pos(float(symbols[SYM_PARAMS][1]), float(symbols[SYM_PARAMS][2])) + def Process(self, btn, idx, split_line): + ms.move_to_pos(float(btn.symbols[SYM_PARAMS][1]), float(btn.symbols[SYM_PARAMS][2])) -scripts.add_command(Mouse_Move()) # register the command +scripts.Add_command(Mouse_Move()) # register the command # ################################################## @@ -56,11 +56,11 @@ def __init__( ) ) - def Process(self): - ms.set_pos(float(symbols[SYM_PARAMS][1]), float(symbols[SYM_PARAMS][2])) + def Process(self, btn, idx, split_line): + ms.set_pos(float(btn.symbols[SYM_PARAMS][1]), float(btn.symbols[SYM_PARAMS][2])) -scripts.add_command(Mouse_Set()) # register the command +scripts.Add_command(Mouse_Set()) # register the command # ################################################## @@ -87,14 +87,14 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - if symbols[SYM_PARAMS][2]: - ms.scroll(float(symbols[SYM_PARAMS][2]), float(symbols[SYM_PARAMS][1])) + def Process(self, btn, idx, split_line): + if btn.symbols[SYM_PARAMS][2]: + ms.scroll(float(btn.symbols[SYM_PARAMS][2]), float(btn.symbols[SYM_PARAMS][1])) else: - ms.scroll(0, float(symbols[SYM_PARAMS][1])) + ms.scroll(0, float(btn.symbols[SYM_PARAMS][1])) -scripts.add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Mouse_Scroll()) # register the command # ################################################## @@ -126,29 +126,29 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): delay = None - if symbols[SYM_PARAMS][5]: - delay = float(symbols[SYM_PARAMS][5]) / 1000.0 + if btn.symbols[SYM_PARAMS][5]: + delay = float(btn.symbols[SYM_PARAMS][5]) / 1000.0 skip = 1 - if symbols[SYM_PARAMS][6]: - skip = int(symbols[SYM_PARAMS][6]) + if btn.symbols[SYM_PARAMS][6]: + skip = int(btn.symbols[SYM_PARAMS][6]) - points = ms.line_coords(symbols[SYM_PARAMS][1], symbols[SYM_PARAMS][2], symbols[SYM_PARAMS][3], symbols[SYM_PARAMS][4]) + points = ms.line_coords(btn.symbols[SYM_PARAMS][1], btn.symbols[SYM_PARAMS][2], btn.symbols[SYM_PARAMS][3], btn.symbols[SYM_PARAMS][4]) for x_M, y_M in points[::skip]: - if check_kill(coords[BC_X], coords[BC_Y], is_async): + if btn.Check_kill(): return -1 ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): + if not btn.Safe_sleep(delay): return -1 -scripts.add_command(Mouse_Line()) # register the command +scripts.Add_command(Mouse_Line()) # register the command # ################################################## @@ -178,32 +178,32 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): delay = None - if symbols[SYM_PARAMS][3]: - delay = float(symbols[SYM_PARAMS][3]) / 1000.0 + if btn.symbols[SYM_PARAMS][3]: + delay = float(btn.symbols[SYM_PARAMS][3]) / 1000.0 skip = 1 - if symbols[SYM_PARAMS][4]: - skip = int(symbols[SYM_PARAMS][4]) + if btn.symbols[SYM_PARAMS][4]: + skip = int(btn.symbols[SYM_PARAMS][4]) x_C, y_C = ms.get_pos() - x_N, y_N = x_C + symbols[SYM_PARAMS][1], y_C + symbols[SYM_PARAMS][2] + x_N, y_N = x_C + btn.symbols[SYM_PARAMS][1], y_C + btn.symbols[SYM_PARAMS][2] points = ms.line_coords(x_C, y_C, x_N, y_N) for x_M, y_M in points[::skip]: - if check_kill(coords[BC_X], coords[BC_Y], is_async): + if btn.Check_kill(): return -1 ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): + if not btn.Safe_sleep(delay): return -1 -scripts.add_command(Mouse_Line_Move()) # register the command +scripts.Add_command(Mouse_Line_Move()) # register the command # ################################################## @@ -233,7 +233,7 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): delay = None if self.params[3]: delay = float(self.params[3]) / 1000.0 @@ -246,15 +246,15 @@ def Process(self, idx, split_line, symbols, coords, is_async): points = ms.line_coords(x_C, y_C, params[1], params[2]) for x_M, y_M in points[::skip]: - if check_kill(coords[BC_X], coords[BC_Y], is_async): + if btn.Check_kill(): return -1 ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): + if not btn.Safe_sleep(delay): return -1 -scripts.add_command(Mouse_Line_Set()) # register the command +scripts.Add_command(Mouse_Line_Set()) # register the command # ################################################## @@ -282,14 +282,14 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): # while this looks like validation, it is just a warning - if symbols[SYM_MOUSE] == tuple(): - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + if btn.symbols[SYM_MOUSE] == tuple(): + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols[SYM_MOUSE])) + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) - x1, y1 = symbols[SYM_MOUSE] + x1, y1 = btn.symbols[SYM_MOUSE] delay = 0 if self.params[1]: @@ -303,17 +303,17 @@ def Process(self, idx, split_line, symbols, coords, is_async): points = ms.line_coords(x_C, y_C, x1, y1) for x_M, y_M in points[::skip]: - if check_kill(coords[BC_X], coords[BC_Y], is_async): + if btn.Check_kill(): return -1 ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): + if not btn.Safe_sleep(delay): return -1 -scripts.add_command(Mouse_Recall_Line()) # register the command +scripts.Add_command(Mouse_Recall_Line()) # register the command # ################################################## @@ -337,11 +337,11 @@ def __init__( ) ) - def Process(self, idx, split_line, symbols, coords, is_async): - symbols[SYM_MOUSE] = ms.get_pos() # Another example of modifying the symbol table during execution. + def Process(self, btn, idx, split_line): + btn.symbols[SYM_MOUSE] = ms.get_pos() # Another example of modifying the symbol table during execution. -scripts.add_command(Mouse_Store()) # register the command +scripts.Add_command(Mouse_Store()) # register the command # ################################################## @@ -365,13 +365,13 @@ def __init__( self.run_states = [RS_INIT, RS_GET, RS_VALIDATE, RS_RUN, RS_FINAL] # we won't do RS_INFO - def Process(self, idx, split_line, symbols, coords, is_async): + def Process(self, btn, idx, split_line): # while this looks like validation, it is really just the info. Putting it here is easy - if symbols[SYM_MOUSE] == tuple(): - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + if btn.symbols[SYM_MOUSE] == tuple(): + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Recall mouse position " + str(symbols[SYM_MOUSE])) - ms.set_pos(symbols[SYM_MOUSE][0], symbols[SYM_MOUSE][1]) + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) + ms.set_pos(btn.symbols[SYM_MOUSE][0], btn.symbols[SYM_MOUSE][1]) -scripts.add_command(Mouse_Recall()) # register the command +scripts.Add_command(Mouse_Recall()) # register the command diff --git a/commands_pause.py b/commands_pause.py index d3cdf40..83229d7 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -16,50 +16,46 @@ def __init__( def Validate( self, + btn, idx: int, # The current line number - line, # The current line - lines, # The current script split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): if pass_no == 1: # check number of split_line if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", line) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.line[idx]) try: temp = float(split_line[1]) except: - return ("Line:" + str(idx+1) + " - Delay time '" + split_line[1] + "' not valid.", line) + return ("Line:" + str(idx+1) + " - Delay time '" + split_line[1] + "' not valid.", btn.line[idx]) return True def Run( self, + btn, idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously + split_line # The current line, split ): - print("[" + lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " Delay for " + split_line[1] + " seconds") + print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Delay for " + split_line[1] + " seconds") delay = float(split_line[1]) - if not safe_sleep(delay, coords[BC_X], coords[BC_Y], is_async): + if not btn.Safe_sleep(delay): return -1 return idx+1 -scripts.add_command(Pause_Delay()) # register the command +scripts.Add_command(Pause_Delay()) # register the command diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 89949ba..5f3d131 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -40,9 +40,9 @@ def __init__( # We can simply override the first pass validation - def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbols): + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): # validate the number of parameters - ret = self.Validate_param_count(ret, idx, line, lines, split_line, symbols) + ret = self.Validate_param_count(ret, btn, idx, split_line) if ((type(ret) == bool) and ret): c_len = len(split_line) # Number of tokens @@ -64,16 +64,16 @@ def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbol if opr in self.operators: # if it's valid for p in range(self.operators[opr][1]): if i + p + 1 >= c_len: - return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) else: param = split_line[i+p+1] if not variables.valid_var_name(param): - return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, line) + return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, btn.line[idx]) i = i + 1 + self.operators[opr][1] # pull of additional parameters if required if i > c_len: - return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) else: # if invalid, report it - return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, line) + return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) return ret @@ -81,8 +81,8 @@ def Partial_validate_step_pass_1(self, ret, idx, line, lines, split_line, symbol # define how to process. We could override something at a lower level, but # this retains any initialisation and finalization and simplifies return # requirements - def Process(self, idx, split_line, symbols, coords, is_async): - print("[" + self.lib + "] " + coords[BC_TEXT] + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # coords[BC_TEXT] is the text "(x, y)" + def Process(self, btn, idx, split_line): + print("[" + self.lib + "] " + btn.coords + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # btn.coords is the text "(x, y)" i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params @@ -99,14 +99,28 @@ def Process(self, idx, split_line, symbols, coords, is_async): pass if n != None: # if it was one of the above - symbols[SYM_STACK].append(n) # ...put on the stack + btn.symbols[SYM_STACK].append(n) # ...put on the stack i += 1 # move along to the next token continue opr = cmd.upper() # Convert to uppercase for searching if opr in self.operators: # if it's valid try: - i = i + self.operators[opr][0](symbols, opr, split_line[i:]) # run it + # capture the return value from the operator + o_ret = self.operators[opr][0](btn.symbols, opr, split_line[i:]) # run it + + # boolean returns are special + if type(o_ret) == bool: + if o_ret: + # True just does a normal "go to next" + i = i + self.operators[opr][1] + 1 + else: + # but False aborts the execution of the RPN calc AND terminates the script + print("Line:" + str(idx+1) + " - RPN terminates script") + return -1 + else: + # non-boolean returns are simply indications to skip ahead the appropriate amount + i = i + o_ret except: print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") break @@ -163,6 +177,7 @@ def Register_operators(self): self.operators["!?L"] = (self.is_local_not_def, 1) # is local var not defined self.operators["?G"] = (self.is_global_def, 1) # is global var defined self.operators["!?G"] = (self.is_global_not_def, 1) # is global var not defined + self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc def add(self, @@ -649,5 +664,10 @@ def is_global_not_def(self, symbols, cmd, cmds): return len(cmds)+1 -scripts.add_command(RpnCalc_Rpn_Eval()) # register the command + def abort_script(self, symbols, cmd, cmds): + # cause the script to be aborted + return False + + +scripts.Add_command(RpnCalc_Rpn_Eval()) # register the command diff --git a/constants.py b/constants.py index 6395d13..1fc21fd 100644 --- a/constants.py +++ b/constants.py @@ -51,11 +51,6 @@ AM_PREFIX = "[{0}] {1} Line:{2}" AM_DEFAULT = AM_PREFIX + " {3} parameters ({4})" -# constants for button coords -BC_TEXT = 0 -BC_X = 1 -BC_Y = 2 - # Misc constants COLOR_PRIMED = 5 #red COLOR_FUNC_KEYS_PRIMED = 9 #amber diff --git a/files.py b/files.py index b7c95b0..15f61e7 100644 --- a/files.py +++ b/files.py @@ -122,7 +122,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): converted_to_rg = False - scripts.unbind_all() + scripts.Unbind_all() window.app.draw_canvas() if preload == None: @@ -144,7 +144,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): if script_text != "": script_validation = None try: - script_validation = scripts.validate_script(script_text) + script_validation = scripts.Validate_script(script_text) except: new_layout_func = lambda: window.app.unbind_lp(prompt_save = False) if popups: @@ -159,7 +159,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): window.app.save_script(window.app, x, y, script_text, open_editor = True, color = color) in_error = False else: - scripts.bind(x, y, script_text, color) + scripts.Bind(x, y, script_text, color) else: lp_colors.setXY(x, y, color) lp_colors.update_all() diff --git a/scripts.py b/scripts.py index 7a5d069..d3246ca 100644 --- a/scripts.py +++ b/scripts.py @@ -27,7 +27,7 @@ # Add a new command. This removes any existing command of the same name from the VALID_COMMANDS # and returns it as the result -def add_command( +def Add_command( a_command: command_base.Command_Basic # the command to add ): @@ -51,7 +51,7 @@ def add_command( # it includes the locations of labels, loop counters, etc. If we implement variables # this is where we would place them -def new_symbol_table(): +def New_symbol_table(): # returns a new (blank) symbol table # symbol table is dictionary of objects symbols = { @@ -70,7 +70,7 @@ def new_symbol_table(): # ### CLASS Button ### # ################################################## -# class that defines a button command. +# class that defines a button. # A button is a class containing all that's essential for a button. class Button(): def __init__( @@ -93,7 +93,7 @@ def __init__( # Do what is required to parse the script. Parsing does not output any information unless it is an error - def parse_script(self): + def Parse_script(self): if self.validated: # we don't want to repeat validation over and over return True @@ -101,7 +101,7 @@ def parse_script(self): self.script_lines = self.script_str.split('\n') # Create the lines self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks - self.symbols = new_symbol_table() # Create a shiny new symbol table + self.symbols = New_symbol_table() # Create a shiny new symbol table self.is_async = False # default is NOT async err = True @@ -110,11 +110,14 @@ def parse_script(self): for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, # pass 2 symbol check & assocoated processing for idx,line in enumerate(self.script_lines): # gen line number and text - if self.is_ignorable_line(line): + if self.Is_ignorable_line(line): continue # don't process ignorable lines - split_line = line.split(" ") # split line on spaces - if split_line[0] in VALID_COMMANDS: # if first element is a command - res = VALID_COMMANDS[split_line[0]].Parse(idx, line, self.script_lines, split_line, self.symbols, pass_no); + + cmd_txt = self.Split_cmd_text(line) # get the name of the command + if cmd_txt in VALID_COMMANDS: # if first element is a command + command = VALID_COMMANDS[cmd_txt]# get the command itself + split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately + res = command.Parse(self, idx, split_line, pass_no); if res != True: if err == True: err = res # note the error @@ -122,7 +125,7 @@ def parse_script(self): else: msg = "Invalid command '" + split_line[0] + "' on line " + str(idx+1) + "." if err == True: - err = (msg, line) # note the error + err = (msg, btn.line[idx]) # note the error print (msg) errors += 1 # and 1 more error @@ -133,7 +136,7 @@ def parse_script(self): return err # success or failure - def check_kill(self, killfunc=None): + def Check_kill(self, killfunc=None): if not self.thread: print ("expecting a thread in ", self.coords) return False @@ -153,11 +156,11 @@ def check_kill(self, killfunc=None): # a sleep method that works with the multiple threads - def safe_sleep(self, time, endfunc=None): + def Safe_sleep(self, time, endfunc=None): while time > DELAY_EXIT_CHECK: sleep(DELAY_EXIT_CHECK) time -= DELAY_EXIT_CHECK - if check_kill(self.x, self.y, self.is_async, endfunc): + if self.Check_kill(endfunc): return False if time > 0: sleep(time) @@ -169,7 +172,7 @@ def safe_sleep(self, time, endfunc=None): # some lines can be ignored. These include blank lines and comments. It's faster to identify them # before trying to process them than treat them as an exception afterwards. - def is_ignorable_line(self, line): + def Is_ignorable_line(self, line): line = line.strip() # remove leading and trailing spaces if line != "": if line[0] == "-": @@ -180,7 +183,7 @@ def is_ignorable_line(self, line): return True # blank lines can be igmored - def schedule_script(self): + def Schedule_script(self): global to_run if self.thread != None: @@ -206,7 +209,7 @@ def schedule_script(self): self.thread.start() elif not self.running: print("[scripts] " + self.coords + " No script running, starting script in background...") - self.thread = threading.Thread(target=self.run_script_and_run_next, args=()) + self.thread = threading.Thread(target=self.Run_script_and_run_next, args=()) self.thread.kill = threading.Event() self.thread.start() else: @@ -216,7 +219,7 @@ def schedule_script(self): lp_colors.updateXY(self.x, self.y) - def run_next(self): + def Run_next(self): global to_run global buttons @@ -226,58 +229,93 @@ def run_next(self): y = tup[1] btn = buttons[x][y] - btn.schedule_script() + btn.Schedule_script() + + + def Run_script_and_run_next(self): + self.Run_script() + self.Run_next() + + + def Line(self, idx): + if self.script_lines and idx >=0 and idx < len(self.script_lines): + return self.Fix_comment(self.script_lines[idx]) + else: + return "" + + + def Fix_comment(self, line): + # Ensure there's a space after the comment character + if len(line) > 1 and line[0] == "-" and line[1] != " ": + return line[0] + " " + line[1:] + else: + return line + + def Split_cmd_text(self, line): + # Get the command text + return line[0:line.find(" ")] - def run_script_and_run_next(self): - self.run_script() - self.run_next() + + def Split_text(self, command, cmd_txt, line): + # Split line differently for "text" type commands + if isinstance(command, command_base.Command_Text_Basic): + # just split the command from the rest of the text + return [cmd_txt, line[len(cmd_txt)+1:]] + else: + # for all other commands, split on spaces + return line.split(" ") # run a script - def run_script(self): + def Run_script(self): lp_colors.updateXY(self.x, self.y) - if self.validate_script() != True: + if self.Validate_script() != True: return print("[scripts] " + self.coords + " Now running script...") self.running = not self.is_async - cmd = "RESET_REPEATS" # before we run, we want to rest loop counters - if cmd in VALID_COMMANDS: - command = VALID_COMMANDS[cmd] - command.Run(-1, [cmd], self.symbols, (self.coords, self.x, self.y), self.is_async) + cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters + if cmd_txt in VALID_COMMANDS: + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) if len(self.script_lines) > 0: self.running = True - def main_logic(idx): - if self.check_kill(): + def Main_logic(idx): + if self.Check_kill(): return idx + 1 - line = self.script_lines[idx] + line = self.Line(idx) + + # Handle completely blank lines if line == "": return idx + 1 - if line[0] == "-": - split_line = ["-", line[1:]] # comments are special -- not tokenised - else: - split_line = line.split(" ") + # Get the command text + cmd_txt = self.Split_cmd_text(line) - if split_line[0] in VALID_COMMANDS: # if first element is a command - command = VALID_COMMANDS[split_line[0]] # get the command - return command.Run(idx, split_line, self.symbols, (self.coords, self.x, self.y), self.is_async) + # Now get the command object + if cmd_txt in VALID_COMMANDS: + command = VALID_COMMANDS[cmd_txt] + + split_line = self.Split_text(command, cmd_txt, line) + + # now run the command + return command.Run(self, idx, split_line) else: - print("[scripts] " + self.coords + " Invalid command: " + split_line[0] + ", skipping...") + print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") return idx + 1 run = True idx = 0 while run: - idx = main_logic(idx) + idx = Main_logic(idx) if (idx < 0) or (idx >= len(self.script_lines)): run = False @@ -291,17 +329,17 @@ def main_logic(idx): # validating a script consists of doing the checks that we do prior to running, but # we won't run it afterwards. - def validate_script(self): + def Validate_script(self): if self.validated or self.script_str == "": # If valid or there is no script... self.validated = True return True # ...validation succeeds! - if self.parse_script(): # If parsing is OK + if self.Parse_script(): # If parsing is OK self.validated = True # Script is valid if len(self.script_lines) > 0: # look for async header and set flag - token = self.script_lines[0].split(" ")[0] - self.is_async = token in HEADERS and HEADERS[token].is_async + cmd_txt = self.Split_cmd_text(self.script_lines[0]) + self.is_async = cmd_txt in HEADERS and HEADERS[cmd_txt].is_async else: self.symbols = None # otherwise destroy symbol table @@ -314,7 +352,7 @@ def validate_script(self): # bind a button -def bind(x, y, script_str, color): +def Bind(x, y, script_str, color): global to_run global buttons @@ -327,14 +365,14 @@ def bind(x, y, script_str, color): temp = to_run.pop(index) # Remove them from the list return # Why do we return here? - schedule_script_bindable = lambda a, b: btn.schedule_script() + schedule_script_bindable = lambda a, b: btn.Schedule_script() lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) files.layout_changed_since_load = True # Mark the layout as changed # unbind a button -def unbind(x, y): +def Unbind(x, y): global to_run global buttons @@ -358,7 +396,7 @@ def unbind(x, y): # swap details for two buttons -def swap(x1, y1, x2, y2): +def Swap(x1, y1, x2, y2): global text color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 @@ -367,55 +405,55 @@ def swap(x1, y1, x2, y2): script_1 = buttons[x1, y1].script_str # Script for btn #1 script_2 = buttons[x2, y2].script_str # Script for btn #2 - unbind(x1, y1) # Unbind #1 + Unbind(x1, y1) # Unbind #1 if script_2 != "": # If there is a script #2... - bind(x1, y1, script_2, color_2) # ...bind it to #1 + Bind(x1, y1, script_2, color_2) # ...bind it to #1 lp_colors.updateXY(x1, y1) # Update the colours for btn #1 - unbind(x2, y2) # Do the reverse for #2 + Unbind(x2, y2) # Do the reverse for #2 if script_1 != "": - bind(x2, y2, script_1, color_1) + Bind(x2, y2, script_1, color_1) lp_colors.updateXY(x2, y2) files.layout_changed_since_load = True # Flag that the layout has changed # Duplicate a button -def copy(x1, y1, x2, y2): +def Copy(x1, y1, x2, y2): global buttons color_1 = lp_colors.curr_colors[x1][y1] # Get colour of btn to be copied script_1 = buttons[x1, y1].script_str # Get script to be copied - unbind(x2, y2) # Unbind the destination + Unbind(x2, y2) # Unbind the destination if script_1 != "": # If we're copying a button with a script... - bind(x2, y2, script_1, color_1) # ...bind the details to the destination + Bind(x2, y2, script_1, color_1) # ...bind the details to the destination lp_colors.updateXY(x2, y2) # Update the colours files.layout_changed_since_load = True # Flag the layout as changed # move a button -def move(x1, y1, x2, y2): +def Move(x1, y1, x2, y2): global buttons color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour script_1 = buttons[x1, y1].script_str # Get source button script - unbind(x1, y1) # Unbind *both* buttons - unbind(x2, y2) + Unbind(x1, y1) # Unbind *both* buttons + Unbind(x2, y2) if script_1 != "": # If the source had a script... - bind(x2, y2, script_1, color_1) # ...bind it to the destination + Bind(x2, y2, script_1, color_1) # ...bind it to the destination lp_colors.updateXY(x2, y2) # Update the destination colours files.layout_changed_since_load = True # And flag the layout as changed # determine if a key is bound -def is_bound(x, y): +def Is_bound(x, y): global buttons if buttons[x][y].script_str == "": # If there is no script... @@ -425,7 +463,7 @@ def is_bound(x, y): # Unbind all keys. -def unbind_all(): +def Unbind_all(): global buttons global to_run diff --git a/variables.py b/variables.py index 946ef68..9a7d7a3 100644 --- a/variables.py +++ b/variables.py @@ -96,61 +96,61 @@ def error_msg(idx, name, desc, p, param, err): # check the number of parameters allowed -def Check_num_params(split_line, lens, idx, line, name): - # lens is an array of valid numbers of parameters +def Check_num_params(btn, cmd, idx, split_line): + # cmd.valid_num_params is an array of valid numbers of parameters # it will be None if you've taken control of handling the parameters yourself. # if you set it to [n, None] that means any number of parameters from n to infinity! - if lens == None: # if this is undefined + if cmd.valid_num_params == None: # if this is undefined return True # anything is valid - ln = len(lens) + ln = len(cmd.valid_num_params) n = len(split_line)-1 - if ln == 2 and lens[1] == None: - if n >= lens[0]: + if ln == 2 and cmd.valid_num_params[1] == None: + if n >= cmd.valid_num_params[0]: return True - elif n in lens: + elif n in cmd.valid_num_params: return True # create a properly formatted error message - if len(lens) == 0: + if len(cmd.valid_num_params) == 0: msg = "Has no valid number of parameters described. " - return (error_msg(idx, name, msg, None, None, "Please correct the definition"), line) + return (error_msg(idx, cmd.name, msg, None, None, "Please correct the definition"), btn.Line(idx)) msg = "Incorrect number of parameters" - if lens == [0]: - return (error_msg(idx, name, msg, str(n), "supplied. None are permitted"), line) + if cmd.valid_num_params == [0]: + return (error_msg(idx, cmd.name, msg, str(n), "supplied. None are permitted"), btn.Line(idx)) else: cnt = "" - if len(lens) == 1: - cnt += str(lens[0]) - elif len(lens) == 2 and lens[1] == None: - cnt += str(lens[0]) + " or more" + if len(cmd.valid_num_params) == 1: + cnt += str(cmd.valid_num_params[0]) + elif len(cmd.valid_num_params) == 2 and cmd.valid_num_params[1] == None: + cnt += str(cmd.valid_num_params[0]) + " or more" else: - cnt += ", ".join([str(el) for el in lens[0:-1]]) + ", or " + str(lens[-1]) + cnt += ", ".join([str(el) for el in cmd.valid_num_params[0:-1]]) + ", or " + str(cmd.valid_num_params[-1]) - return (error_msg(idx, name, msg, None, str(n), "supplied, " + cnt + " are required"), line) + return (error_msg(idx, cmd.name, msg, None, str(n), "supplied, " + cnt + " are required"), btn.Line(idx)) # check a generic parameter -def Check_generic_param(split_line, p, desc, idx, name, line, v_type, validation=None, optional=False, var_ok=True): +def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): temp = None if p >= len(split_line): - if optional: + if val[AV_OPTIONAL]: return True else: - return (error_msg(idx, name, desc, p, None, 'required ' + v_type[AVT_DESC] + ' parameter not present'), line) + return (error_msg(idx, name, desc, p, None, 'required ' + val[AV_TYPE][AVT_DESC] + ' parameter not present'), btn.line[idx]) try: - temp = v_type[AVT_CONV](split_line[p]) + temp = val[AV_TYPE][AVT_CONV](split_line[p]) except: - if var_ok and valid_var_name(split_line[p]): # a variable is OK here + if val[AV_VAR_OK] and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, name, desc, p, split_line[p], 'not a valid ' + v_type[AVT_DESC]), line) + return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), btn.line[idx]) - if validation: - return validation(temp, idx, name, desc, p, split_line[p]) + if val[val_validation]: + return val[val_validation](temp, idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p]) return True @@ -163,14 +163,14 @@ def Check_numeric_param(split_line, p, desc, idx, name, line, validation, option if optional: return True else: - return (error_msg(idx, name, desc, p, None, 'required parameter not present'), line) + return (error_msg(idx, name, desc, p, None, 'required parameter not present'), btn.line[idx]) try: temp = conv(split_line[p]) except: if var_ok and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, name, desc, p, split_line[p], 'not valid'), line) + return (error_msg(idx, name, desc, p, split_line[p], 'not valid'), btn.line[idx]) if validation: return validation(temp, idx, name, desc, p, split_line[p]) diff --git a/window.py b/window.py index 666439f..8c51c46 100644 --- a/window.py +++ b/window.py @@ -210,7 +210,7 @@ def connect_lp(self): def disconnect_lp(self): global lp_connected try: - scripts.unbind_all() + scripts.Unbind_all() lp_events.timer.cancel() lpcon.disconnect(lp_object) except: @@ -232,7 +232,7 @@ def redetect_lp(self): def unbind_lp(self, prompt_save=True): if prompt_save: self.modified_layout_save_prompt() - scripts.unbind_all() + scripts.Unbind_all() files.curr_layout = None self.draw_canvas() @@ -298,12 +298,12 @@ def click(self, event): copy_func = partial(scripts.copy, self.last_clicked[0], self.last_clicked[1], column, row) if self.button_mode == "move": - if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): + if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to move a button to an already\nbound button. What would you like to do?", [["Overwrite", move_func], ["Swap", swap_func], ["Cancel", None]]) else: move_func() elif self.button_mode == "copy": - if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): + if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to copy a button to an already\nbound button. What would you like to do?", [["Overwrite", copy_func], ["Swap", swap_func], ["Cancel", None]]) else: copy_func() @@ -405,7 +405,7 @@ def validate_func(): text_string = t.get(1.0, tk.END) try: btn = scripts.Button(x, y, text_string) - script_validate = btn.parse_script() + script_validate = btn.Parse_script() except: #self.save_script(w, x, y, text_string) # This will fail and throw a popup error self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") @@ -580,7 +580,7 @@ def select_all(self, event): return "break" def unbind_destroy(self, x, y, window): - scripts.unbind(x, y) + scripts.Unbind(x, y) self.draw_canvas() window.destroy() @@ -596,14 +596,14 @@ def open_editor_func(): try: btn = scripts.Button(x, y, script_text) - script_validate = btn.parse_script() + script_validate = btn.Parse_script() except: self.popup(window, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = open_editor_func) raise if script_validate == True: if script_text != "": script_text = files.strip_lines(script_text) - scripts.bind(x, y, script_text, colors_to_set[x][y]) + scripts.Bind(x, y, script_text, colors_to_set[x][y]) self.draw_canvas() lp_colors.updateXY(x, y) window.destroy() From eb994e5c7536ca87bac1522193dccaf7fc36edec Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 11 Feb 2021 19:19:15 +0800 Subject: [PATCH 21/83] * Complete conversion of btn.Line[x] to btn.Line(x) * Remember to return values from validation steps * Fix an edge case for enumerating valid numbers of parameters * Correct handling of None vs Boolean vs tuple returns from validation * Fix validation for loaded layouts (it always failed) * Revert an instance of line being changed to btn.Line * Correct use of split_line[0] to cmd_txt in error message * corrections to CHeck_generic_param. It is getting both split_line and btn as parameters (more to do?) --- command_base.py | 28 +++++++++++++++++----------- commands_external.py | 6 +++--- commands_header.py | 14 +++++++------- commands_pause.py | 6 +++--- files.py | 3 ++- scripts.py | 8 +++++--- variables.py | 8 ++++---- 7 files changed, 41 insertions(+), 32 deletions(-) diff --git a/command_base.py b/command_base.py index 28a89d0..c50b379 100644 --- a/command_base.py +++ b/command_base.py @@ -80,7 +80,7 @@ def Validate( import traceback traceback.print_exc() ret = ("", "") - + finally: if type(ret) == tuple: return ret @@ -94,7 +94,8 @@ def Partial_validate_step_count(self, ret, btn, idx, split_line): # Validation of the count is separated from the pass 1 validation because sometinmes # you want to override one but not the other. You would override this if you have some # odd way of counting parameters, or the count depends on something complex. - ret = self.Validate_param_count(ret, btn, idx, split_line) + ret = self.Validate_param_count(ret, btn, idx, split_line) + return ret def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): @@ -103,6 +104,7 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. ret = self.Validate_params(ret, btn, idx, split_line, AV_P1_VALIDATION) + return ret def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): @@ -111,6 +113,7 @@ def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. ret = self.Validate_params(ret, btn, idx, split_line, AV_P2_VALIDATION) + return ret def Parse( @@ -133,7 +136,7 @@ def Parse( return True if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: - ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.line[idx]) + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.Line(idx)) if ret[0]: print(ret[0]) @@ -314,9 +317,12 @@ def Calc_valid_param_counts(self): ret = [] vn = len(self.auto_validate) for i in range(vn): - i_val = self.auto_validate[i] - if (i_val[AV_OPTIONAL] == True) or (i+1 == vn): - ret += [i+1] + if (i+1 == vn): # if this is the last argument + ret += [i+1] # obviously it's valid to have this many + else: + i_val = self.auto_validate[i+1] # get the *next* parameter + if (i_val[AV_OPTIONAL] == True): # if it's optional + ret += [i+1] # then it's valid to have this many too if ret: return ret @@ -340,7 +346,7 @@ def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. # This function improves efficiency. - if self.valid_max_params < n_passed or (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): + if ((self.valid_max_params == None and n_passed == 0) or (self.valid_max_params < n_passed)) or (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): return n_passed else: return self.valid_max_params @@ -356,7 +362,7 @@ def Validate_params(self, ret, btn, idx, split_line, val_validation): for i in range(self.Param_validation_count(len(split_line)-1)): ret = self.Validate_param_n(ret, btn, idx, split_line, val_validation, i+1) - if ret != True: + if not ((type(ret) == bool) and ret): return ret return ret @@ -395,14 +401,14 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only # check for duplicate label if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", btn.line[idx]) + return ("Duplicate LABEL", btn.Line(idx)) # add label to symbol table # Add the new label to the labels in the symbol table btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number elif val[AV_TYPE] == PT_KEY: # targets (label definitions) have pass 1 validation only # check for valid key if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? - return ("Unknown key", btn.line[idx]) + return ("Unknown key", btn.Line(idx)) elif val_validation == AV_P2_VALIDATION: if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only @@ -412,7 +418,7 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): return True - return (ret, btn.line[idx]) + return (ret, btn.Line(idx)) def Run_params(self, ret, btn, idx, split_line, pass_no): diff --git a/commands_external.py b/commands_external.py index 25cebed..5d6fe6d 100644 --- a/commands_external.py +++ b/commands_external.py @@ -110,10 +110,10 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.Line(idx)) if len(split_line) > 3: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.Line(idx)) return True @@ -197,7 +197,7 @@ def Validate( if pass_no == 1: # in Pass 1 we can do general syntax check and gather symbol definitions if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.Line(idx)) return True diff --git a/commands_header.py b/commands_header.py index 4de7dff..cf9e2e5 100644 --- a/commands_header.py +++ b/commands_header.py @@ -23,10 +23,10 @@ def Validate( if pass_no == 1: if idx > 0: # headers normally have to check the line number - return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) + return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", btn.Line(0)) if len(split_line) > 1: - return ("Line:" + str(idx+1) + " - " + self.name + " takes no arguments.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - " + self.name + " takes no arguments.", btn.Line(idx)) return True @@ -66,16 +66,16 @@ def Validate( if pass_no == 1: if idx > 0: # headers normally have to check the line number - return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) + return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", btn.Line(0)) if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - " + self.name + " requires a key to bind.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a key to bind.", btn.Line(idx)) if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - " + self.name + " only take one argument", btn.line[idx]) + return ("Line:" + str(idx+1) + " - " + self.name + " only take one argument", btn.Line(idx)) if kb.sp(split_line[1]) == None: - return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - No key named '" + split_line[1] + "'.", btn.Line(idx)) for lin in lines[1:]: if lin != "" and lin[0] != "-": @@ -139,7 +139,7 @@ def Validate( return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", lines[0]) if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - " + self.name + " requires a filename as a parameter.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - " + self.name + " requires a filename as a parameter.", btn.Line(idx)) return True diff --git a/commands_pause.py b/commands_pause.py index 83229d7..a387fb3 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -25,15 +25,15 @@ def Validate( if pass_no == 1: # check number of split_line if len(split_line) < 2: - return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Too few arguments for command '" + split_line[0] + "'.", btn.Line(idx)) if len(split_line) > 2: - return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Too many arguments for command '" + split_line[0] + "'.", btn.Line(idx)) try: temp = float(split_line[1]) except: - return ("Line:" + str(idx+1) + " - Delay time '" + split_line[1] + "' not valid.", btn.line[idx]) + return ("Line:" + str(idx+1) + " - Delay time '" + split_line[1] + "' not valid.", btn.Line(idx)) return True diff --git a/files.py b/files.py index 15f61e7..68f0ec6 100644 --- a/files.py +++ b/files.py @@ -144,7 +144,8 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): if script_text != "": script_validation = None try: - script_validation = scripts.Validate_script(script_text) + btn = scripts.Button(x, y, script_text) + script_validation = btn.Validate_script() except: new_layout_func = lambda: window.app.unbind_lp(prompt_save = False) if popups: diff --git a/scripts.py b/scripts.py index d3246ca..b12ab66 100644 --- a/scripts.py +++ b/scripts.py @@ -112,8 +112,9 @@ def Parse_script(self): for idx,line in enumerate(self.script_lines): # gen line number and text if self.Is_ignorable_line(line): continue # don't process ignorable lines - + cmd_txt = self.Split_cmd_text(line) # get the name of the command + if cmd_txt in VALID_COMMANDS: # if first element is a command command = VALID_COMMANDS[cmd_txt]# get the command itself split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately @@ -123,9 +124,9 @@ def Parse_script(self): err = res # note the error errors += 1 # and 1 more error else: - msg = "Invalid command '" + split_line[0] + "' on line " + str(idx+1) + "." + msg = " Invalid command '" + cmd_txt + "' on line " + str(idx+1) + "." if err == True: - err = (msg, btn.line[idx]) # note the error + err = (msg, line) # note the error print (msg) errors += 1 # and 1 more error @@ -254,6 +255,7 @@ def Fix_comment(self, line): def Split_cmd_text(self, line): # Get the command text + line += ' ' return line[0:line.find(" ")] diff --git a/variables.py b/variables.py index 9a7d7a3..738a27a 100644 --- a/variables.py +++ b/variables.py @@ -140,14 +140,14 @@ def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): if val[AV_OPTIONAL]: return True else: - return (error_msg(idx, name, desc, p, None, 'required ' + val[AV_TYPE][AVT_DESC] + ' parameter not present'), btn.line[idx]) + return (error_msg(idx, name, desc, p, None, 'required ' + val[AV_TYPE][AVT_DESC] + ' parameter not present'), split_line[p]) try: temp = val[AV_TYPE][AVT_CONV](split_line[p]) except: if val[AV_VAR_OK] and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), btn.line[idx]) + return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), split_line[p]) if val[val_validation]: return val[val_validation](temp, idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p]) @@ -163,14 +163,14 @@ def Check_numeric_param(split_line, p, desc, idx, name, line, validation, option if optional: return True else: - return (error_msg(idx, name, desc, p, None, 'required parameter not present'), btn.line[idx]) + return (error_msg(idx, name, desc, p, None, 'required parameter not present'), btn.Line(idx)) try: temp = conv(split_line[p]) except: if var_ok and valid_var_name(split_line[p]): # a variable is OK here return True - return (error_msg(idx, name, desc, p, split_line[p], 'not valid'), btn.line[idx]) + return (error_msg(idx, name, desc, p, split_line[p], 'not valid'), btn.Line(idx)) if validation: return validation(temp, idx, name, desc, p, split_line[p]) From 0b0d705898a216e6276e27408516a57b5ffbba0f Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 12 Feb 2021 16:49:57 +0800 Subject: [PATCH 22/83] * Created new parameter type (PT_VAR) to allow variable names to be passed to commands that return values * Ensure we don't try to get the value of variables passed to functions that are supposed to return values into them * Modified M_STORE command to accept (optionally) a pair of variable names in which to place the current mouse coords * Initial work for a new parameter type that returns a boolean value * Re-Corrected the code to count valid param counts for yet another edge case. * Updated the Validate_param_n routine to ensure you don't try to check more parameters than are passed. * made the function Validate_var_name work if incorrect types passed to it. * moved Auto_Store from rpn_calc to Variables to allow any routine to store values in variables. * modified importing of additional commands to allow some commands to be made optional (e.g. win32 specific functions on a linux platform) * corrected referenceing of parameters from self.param to btn.symbols in a few places. * a few more locations found where btn.Line(x) should replace btn.line[x] --- command_base.py | 34 +++++++++++++++++++++------------- command_list.py | 24 ++++++++++++++++++++++-- commands_mouse.py | 29 +++++++++++++++++++---------- constants.py | 9 ++++++++- variables.py | 13 ++++++++++++- 5 files changed, 82 insertions(+), 27 deletions(-) diff --git a/command_base.py b/command_base.py index c50b379..82c9d97 100644 --- a/command_base.py +++ b/command_base.py @@ -312,20 +312,16 @@ def Calc_valid_param_counts(self): # This routine does not return it, but setting the counts to [n, None] indicates to the parameter number # validation routine that n or more parameters are acceptable -- this is great for comments etc. ret = None - + if self.auto_validate: ret = [] vn = len(self.auto_validate) for i in range(vn): - if (i+1 == vn): # if this is the last argument - ret += [i+1] # obviously it's valid to have this many - else: - i_val = self.auto_validate[i+1] # get the *next* parameter - if (i_val[AV_OPTIONAL] == True): # if it's optional - ret += [i+1] # then it's valid to have this many too - - if ret: - return ret + i_val = self.auto_validate[i] # get the parameter + if (i_val[AV_OPTIONAL] == True): # if this one is optional + ret += [i] # then 1 fewer is OK + + ret += [vn] # it's always valid to pass *all* the parameters return ret @@ -359,7 +355,7 @@ def Validate_params(self, ret, btn, idx, split_line, val_validation): # of your parameters. You will need to remember that this gets called for both pass 1 and 2. if not (ret == None or ((type(ret) == bool) and ret)): return ret - + for i in range(self.Param_validation_count(len(split_line)-1)): ret = self.Validate_param_n(ret, btn, idx, split_line, val_validation, i+1) if not ((type(ret) == bool) and ret): @@ -380,6 +376,9 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # hard coded here. It would be better to register validation routines, but... later. if not (ret == None or ((type(ret) == bool) and ret)): return ret + + if n > len(split_line): + return ret if self.auto_validate == None: # no auto validation can be done return ret @@ -405,10 +404,18 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # add label to symbol table # Add the new label to the labels in the symbol table btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number - elif val[AV_TYPE] == PT_KEY: # targets (label definitions) have pass 1 validation only + elif val[AV_TYPE] == PT_KEY: # Keys have pass 1 validation only # check for valid key if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? return ("Unknown key", btn.Line(idx)) + elif val[AV_TYPE] == PT_BOOL: # booleans have pass 1 validation only + # check for valid boolean value + if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? + return ("Invalid boolean value", btn.Line(idx)) + elif val[AV_TYPE] == PT_VAR: # mandatory variables have pass 1 validation only + # check for valid variable name + if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? + return ("Invalid variable name", btn.Line(idx)) elif val_validation == AV_P2_VALIDATION: if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only @@ -456,7 +463,8 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): # if validation has passed. if pass_no == 1: v = split_line[n] - if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK]: + + if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK] and not (self.auto_validate[n-1][AV_TYPE] == PT_VAR) : v = variables.get_value(split_line[n], btn.symbols) if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) diff --git a/command_list.py b/command_list.py index 9ad0905..a879122 100644 --- a/command_list.py +++ b/command_list.py @@ -1,10 +1,30 @@ # This module exists simply to list the command modules imported into LPHK +IMPORT_FATAL = False + +import sys + import \ commands_header, \ commands_control, \ commands_keys, \ commands_mouse, \ commands_pause, \ - commands_external, \ - commands_rpncalc + commands_external + +# This library could be considered optional +try: + import commands_rpncalc +except ImportError: + print("[LPHK] WARNING: RPN_EVAL command is not available") + +# This library could be considered optional +try: + import commands_win32 +except ImportError: + print("[LPHK] WARNING: Windows specific commands are not available") + +# Any that were not optional should set the error flag so we can exit +if IMPORT_FATAL: + sys.exit("[LPHK] ERROR: Required command modules are absent") + diff --git a/commands_mouse.py b/commands_mouse.py index 246b06e..7d4f06d 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -235,12 +235,12 @@ def __init__( def Process(self, btn, idx, split_line): delay = None - if self.params[3]: - delay = float(self.params[3]) / 1000.0 + if btn.symbols[SYM_PARAMS][3]: + delay = float(btn.symbols[SYM_PARAMS][3]) / 1000.0 skip = 1 - if self.params[4]: - skip = int(self.params[4]) + if btn.symbols[SYM_PARAMS][4]: + skip = int(btn.symbols[SYM_PARAMS][4]) x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, params[1], params[2]) @@ -292,12 +292,12 @@ def Process(self, btn, idx, split_line): x1, y1 = btn.symbols[SYM_MOUSE] delay = 0 - if self.params[1]: - delay = float(self.params[3]) / 1000.0 + if btn.symbols[SYM_PARAMS][1]: + delay = float(btn.symbols[SYM_PARAMS][1]) / 1000.0 skip = 1 - if self.params[2]: - skip = int(self.params[4]) + if btn.symbols[SYM_PARAMS][2]: + skip = int(btn.symbols[SYM_PARAMS][2]) x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -329,16 +329,25 @@ def __init__( super().__init__("M_STORE", # the name of the command as you have to enter it in the code LIB, ( - # no variables defined, so none are allowed + # Desc Opt Var type p1_val p2_val + ("X value", True , True, PT_VAR, None, None), + ("Y value", False, True, PT_VAR, None, None), ), ( # num params, format string (trailing comma is important) (0, " Store mouse position"), + (2, " Store mouse position in variables ({1}, {2})"), ) ) def Process(self, btn, idx, split_line): - btn.symbols[SYM_MOUSE] = ms.get_pos() # Another example of modifying the symbol table during execution. + mpos = ms.get_pos() + + if not btn.symbols[SYM_PARAMS][1]: + btn.symbols[SYM_MOUSE] = mpos # Another example of modifying the symbol table during execution. + else: + variables.Auto_store(btn.symbols[SYM_PARAMS][1], mpos[0], btn.symbols) + variables.Auto_store(btn.symbols[SYM_PARAMS][2], mpos[1], btn.symbols) scripts.Add_command(Mouse_Store()) # register the command diff --git a/constants.py b/constants.py index 1fc21fd..2a28709 100644 --- a/constants.py +++ b/constants.py @@ -36,13 +36,15 @@ AV_P2_VALIDATION = 5 # constants for parameter types -# desc conv special (special means additional auto-validation +# desc conv special (special means additional auto-validation) PT_INT = ("int", int, False) PT_FLOAT = ("float", float, False) PT_TEXT = ("text", str, False) PT_LABEL = ("label", str, True) # Note that this is for a reference to a label, not the definition of a label! PT_TARGET = ("target", str, True) # Note that this is for the definition of a target (e.g. creating a label) PT_KEY = ("key", str, True) # This is a key literal +PT_BOOL = ("bool", str, True) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables +PT_VAR = ("var", str, True) # Variable is *required*, typically for storage of results # constants for auto_message AM_COUNT = 0 @@ -51,6 +53,11 @@ AM_PREFIX = "[{0}] {1} Line:{2}" AM_DEFAULT = AM_PREFIX + " {3} parameters ({4})" +# constants for boolean values +VALID_BOOL_TRUE = ["TRUE", "YES"] +VALID_BOOL_FALSE = ["FALSE", "NO"] +VALID_BOOL = VALID_BOOL_TRUE + VALID_BOOL_FALSE + # Misc constants COLOR_PRIMED = 5 #red COLOR_FUNC_KEYS_PRIMED = 9 #amber diff --git a/variables.py b/variables.py index 738a27a..1249604 100644 --- a/variables.py +++ b/variables.py @@ -69,7 +69,7 @@ def next_cmd(ret, cmds): # variable names should start with an alpha character def valid_var_name(v): - return len(v) > 0 and ord(v[0].upper()) in range(ord('A'), ord('Z')+1) + return isinstance(v, str) and len(v) > 0 and ord(v[0].upper()) in range(ord('A'), ord('Z')+1) # return a properly formatted error message @@ -218,4 +218,15 @@ def Validate_ge_zero(v, idx, name, desc, p, param): return error_msg(idx, name, desc, p, param, 'must be an integer') +def Auto_store(v, a, symbols): + # automatically stores the variable in the "right" place + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + if is_defined(v, symbols[SYM_LOCAL]): # Is it local... + put(v, a, symbols[SYM_LOCAL]) # ...then store it locally + elif is_defined(v, symbols[SYM_GLOBAL][1]): # Is it global... + put(v, a, symbols[SYM_GLOBAL][1]) # ...store it globally + else: + put(v, a, symbols[SYM_LOCAL]) # default is to create new in locals + + From 47116b8a6a7fd27f368788bbb120566c726006c3 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 12 Feb 2021 17:05:51 +0800 Subject: [PATCH 23/83] Oops, also some changes to the rpncalc module (described in the last commit) --- commands_rpncalc.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 5f3d131..30cd66d 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -64,16 +64,16 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): if opr in self.operators: # if it's valid for p in range(self.operators[opr][1]): if i + p + 1 >= c_len: - return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) + return ("Line:" + str(idx+1) + " - Insufficient tokens for parameter#" + str(p+1) + " of operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) else: param = split_line[i+p+1] if not variables.valid_var_name(param): - return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, btn.line[idx]) + return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, btn.Line(idx)) i = i + 1 + self.operators[opr][1] # pull of additional parameters if required if i > c_len: - return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) + return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) else: # if invalid, report it - return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.line[idx]) + return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) return ret @@ -473,13 +473,7 @@ def sto(self, symbols, cmd, cmds): ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? a = variables.top(symbols, 1) # will be stored from the top of the stack - with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - if variables.is_defined(v, symbols[SYM_LOCAL]): # Is it local... - variables.put(v, a, symbols[SYM_LOCAL]) # ...then store it locally - elif variables.is_defined(v, symbols[SYM_GLOBAL][1]): # Is it global... - variables.put(v, a, symbols[SYM_GLOBAL][1]) # ...store it globally - else: - variables.put(v, a, symbols[SYM_LOCAL]) # default is to create new in locals + variables.AutoStore(v, a, symbols) # "auto store" the value return ret From cf6b4f0893e706381ecfc3ae46980825b96c2200 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 14 Feb 2021 18:28:40 +0800 Subject: [PATCH 24/83] Remove some validations for the mouse commands because negative coordinates are valid for multiple screen setups. --- commands_mouse.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/commands_mouse.py b/commands_mouse.py index 7d4f06d..555fd8d 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -47,8 +47,8 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_INT, variables.Validate_ge_zero, None), - ("Y value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("X value", False, True, PT_INT, None, None), + ("Y value", False, True, PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -111,10 +111,10 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("X1 value", False, True, PT_INT, variables.Validate_ge_zero, None), - ("Y1 value", False, True, PT_INT, variables.Validate_ge_zero, None), - ("X2 value", False, True, PT_INT, variables.Validate_ge_zero, None), - ("Y2 value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("X1 value", False, True, PT_INT, None, None), + ("Y1 value", False, True, PT_INT, None, None), + ("X2 value", False, True, PT_INT, None, None), + ("Y2 value", False, True, PT_INT, None, None), ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), ), @@ -220,8 +220,8 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_INT, variables.Validate_ge_zero, None), - ("Y value", False, True, PT_INT, variables.Validate_ge_zero, None), + ("X value", False, True, PT_INT, None, None), + ("Y value", False, True, PT_INT, None, None), ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), ), From b3ec50efccd397dcdf5bc46633a5e3bcf4cccedd Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 15 Feb 2021 06:21:48 +0800 Subject: [PATCH 25/83] * allow a simpler method of counting parameters, getting and setting (yes, setting!) their values * fix a dumb error that tried to look at the first non-existent parameter while validating * update the mouse commands to use the newer syntax --- command_base.py | 39 ++++++++++++++++++++-- commands_mouse.py | 84 ++++++++++++++++++++++++----------------------- scripts.py | 2 +- 3 files changed, 81 insertions(+), 44 deletions(-) diff --git a/command_base.py b/command_base.py index 82c9d97..5fbe20a 100644 --- a/command_base.py +++ b/command_base.py @@ -342,7 +342,8 @@ def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. # This function improves efficiency. - if ((self.valid_max_params == None and n_passed == 0) or (self.valid_max_params < n_passed)) or (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): + if ((self.valid_max_params == None and n_passed == 0) or (self.valid_max_params < n_passed)) or \ + (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): return n_passed else: return self.valid_max_params @@ -377,7 +378,7 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): if not (ret == None or ((type(ret) == bool) and ret)): return ret - if n > len(split_line): + if n >= len(split_line): return ret if self.auto_validate == None: # no auto validation can be done @@ -483,6 +484,40 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): return ret + # Is there a parameter n? + def Has_param(self, btn, n): + val = btn.symbols[SYM_PARAMS][n] + return not (val is None) + + + # How many parameters do we have? + def Param_count(self, btn): + return btn.symbols[SYM_PARAM_CNT] + + + # gets the value of the nth parameter (button is required for context). Other is default value if param does not exist + def Get_param(self, btn, n, other=None): + val = self.auto_validate[n-1] + param = btn.symbols[SYM_PARAMS][n] + if param == None: + ret = other + else: + if val[AV_TYPE] == PT_VAR: + ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) + else: + ret = param + + return ret + + + # sets the value of the nth parameter (if it is a variable) + def Set_param(self, btn, n, val): + param = btn.symbols[SYM_PARAMS][n] + av = self.auto_validate[n-1] + if av[AV_TYPE] == PT_VAR: + variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable + + # ################################################## # ### CLASS Command_Text_Basic ### # ################################################## diff --git a/commands_mouse.py b/commands_mouse.py index 555fd8d..07dd3ef 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -27,7 +27,10 @@ def __init__( def Process(self, btn, idx, split_line): - ms.move_to_pos(float(btn.symbols[SYM_PARAMS][1]), float(btn.symbols[SYM_PARAMS][2])) + x = self.Get_param(btn, 1) + y = self.Get_param(btn, 1) + + ms.move_to_pos(x, y) scripts.Add_command(Mouse_Move()) # register the command @@ -57,7 +60,10 @@ def __init__( def Process(self, btn, idx, split_line): - ms.set_pos(float(btn.symbols[SYM_PARAMS][1]), float(btn.symbols[SYM_PARAMS][2])) + x = self.Get_param(btn, 1) + y = self.Get_param(btn, 2) + + ms.set_pos(x, y) scripts.Add_command(Mouse_Set()) # register the command @@ -88,10 +94,10 @@ def __init__( def Process(self, btn, idx, split_line): - if btn.symbols[SYM_PARAMS][2]: - ms.scroll(float(btn.symbols[SYM_PARAMS][2]), float(btn.symbols[SYM_PARAMS][1])) - else: - ms.scroll(0, float(btn.symbols[SYM_PARAMS][1])) + s = self.Get_Param(btn, 1) + x = self.Get_param(btn, 2, 0) + + ms.scroll(x, s) scripts.Add_command(Mouse_Scroll()) # register the command @@ -128,14 +134,16 @@ def __init__( def Process(self, btn, idx, split_line): delay = None - if btn.symbols[SYM_PARAMS][5]: - delay = float(btn.symbols[SYM_PARAMS][5]) / 1000.0 + if self.Has_param(btn, 5): + delay = float(self.Get_param(btn, 5)) / 1000.0 - skip = 1 - if btn.symbols[SYM_PARAMS][6]: - skip = int(btn.symbols[SYM_PARAMS][6]) + skip = self.Get_param(btn, 6, 1) - points = ms.line_coords(btn.symbols[SYM_PARAMS][1], btn.symbols[SYM_PARAMS][2], btn.symbols[SYM_PARAMS][3], btn.symbols[SYM_PARAMS][4]) + x1 = self.Get_param(btn, 1) + y1 = self.Get_param(btn, 2) + x2 = self.Get_param(btn, 3) + y2 = self.Get_param(btn, 4) + points = ms.line_coords(x1, y1, x2, y2) for x_M, y_M in points[::skip]: if btn.Check_kill(): @@ -180,15 +188,13 @@ def __init__( def Process(self, btn, idx, split_line): delay = None - if btn.symbols[SYM_PARAMS][3]: - delay = float(btn.symbols[SYM_PARAMS][3]) / 1000.0 + if self.Has_param(btn, 3): + delay = float(self.Get_param(btn, 3)) / 1000.0 - skip = 1 - if btn.symbols[SYM_PARAMS][4]: - skip = int(btn.symbols[SYM_PARAMS][4]) + skip = int(self.Get_param(btn, 4, 1)) x_C, y_C = ms.get_pos() - x_N, y_N = x_C + btn.symbols[SYM_PARAMS][1], y_C + btn.symbols[SYM_PARAMS][2] + x_N, y_N = x_C + self.Get_param(btn, 1), y_C + self.Get_param(btn, 2) points = ms.line_coords(x_C, y_C, x_N, y_N) for x_M, y_M in points[::skip]: @@ -234,23 +240,21 @@ def __init__( def Process(self, btn, idx, split_line): - delay = None - if btn.symbols[SYM_PARAMS][3]: - delay = float(btn.symbols[SYM_PARAMS][3]) / 1000.0 + delay = None # default value of parameter + if self.Has_param(btn, 3): # can't use the default param because if we have one + delay = float(self.Get_param(btn, 3)) / 1000.0 # we need to do math on it - skip = 1 - if btn.symbols[SYM_PARAMS][4]: - skip = int(btn.symbols[SYM_PARAMS][4]) + skip = self.Get_param(btn, 4, 1) # skip parameter has a default value of 1 - x_C, y_C = ms.get_pos() - points = ms.line_coords(x_C, y_C, params[1], params[2]) + x_C, y_C = ms.get_pos() # where are we now? + points = ms.line_coords(x_C, y_C, self.Get_param(btn, 1), self.Get_param(btn, 2)) # how do we get to where we want to be - for x_M, y_M in points[::skip]: - if btn.Check_kill(): + for x_M, y_M in points[::skip]: # For each point we're going to use + if btn.Check_kill(): # Just make sure we should still be running return -1 - ms.set_pos(x_M, y_M) - if (delay != None) and (delay > 0): - if not btn.Safe_sleep(delay): + ms.set_pos(x_M, y_M) # set the position + if (delay != None) and (delay > 0): # if we have a delay + if not btn.Safe_sleep(delay): # delay "safely" return -1 @@ -291,13 +295,11 @@ def Process(self, btn, idx, split_line): x1, y1 = btn.symbols[SYM_MOUSE] - delay = 0 - if btn.symbols[SYM_PARAMS][1]: - delay = float(btn.symbols[SYM_PARAMS][1]) / 1000.0 + delay = None + if self.Has_param(btn, 1): + delay = float(self.Get_param(btn, 1)) / 1000.0 - skip = 1 - if btn.symbols[SYM_PARAMS][2]: - skip = int(btn.symbols[SYM_PARAMS][2]) + skip = self.Get_param(btn, 1, 1) x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) @@ -343,11 +345,11 @@ def __init__( def Process(self, btn, idx, split_line): mpos = ms.get_pos() - if not btn.symbols[SYM_PARAMS][1]: - btn.symbols[SYM_MOUSE] = mpos # Another example of modifying the symbol table during execution. + if self.Has_param(btn, 1): # do we have a parameter 1? + self.Set_param(btn, 1, mpos[0]) # store into first and second patrameters + self.Set_param(btn, 2, mpos[1]) else: - variables.Auto_store(btn.symbols[SYM_PARAMS][1], mpos[0], btn.symbols) - variables.Auto_store(btn.symbols[SYM_PARAMS][2], mpos[1], btn.symbols) + btn.symbols[SYM_MOUSE] = mpos # Another example of modifying the symbol table during execution. scripts.Add_command(Mouse_Store()) # register the command diff --git a/scripts.py b/scripts.py index b12ab66..ffb49e2 100644 --- a/scripts.py +++ b/scripts.py @@ -346,7 +346,7 @@ def Validate_script(self): self.symbols = None # otherwise destroy symbol table return self.validated # and tell us the result - + buttons = [[Button(x, y, "") for y in range(9)] for x in range(9)] From c943251d2c8fbf98b58ac9b62dc6c08d5c8805e0 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 15 Feb 2021 06:23:46 +0800 Subject: [PATCH 26/83] * basic update for keys functions, but more to do * First win32 specific code. I'd like to make this linux/mac compatible, but that's a future effort. For now it's an optional module in the interpreted form of the program (I'll have to explore the compiled form) --- commands_keys.py | 12 +-- commands_win32.py | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 commands_win32.py diff --git a/commands_keys.py b/commands_keys.py index 94ae5c0..8fa669c 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -64,17 +64,17 @@ def __init__( def Process(self, btn, idx, split_line): - cnt = btn.symbols[SYM_PARAM_CNT] - key = kb.sp(btn.symbols[SYM_PARAMS][1]) + cnt = self.Param_count(btn) + key = kb.sp(self.Get_param(btn, 1)) releasefunc = lambda: None taps = 1 if cnt >= 2: - taps = int(btn.symbols[SYM_PARAMS][2]) + taps = self.Get_param(btn, 2) delay = 0 if cnt == 3: - delay = float(btn.symbols[SYM_PARAMS][3]) + delay = self.Get_param(btn, 3) releasefunc = lambda: kb.release(key) precheck = delay == 0 and taps > 1 @@ -125,7 +125,7 @@ def __init__( def Process(self, btn, idx, split_line): - key = kb.sp(btn.symbols[SYM_PARAMS][1]) + key = kb.sp(self.Get_param(btn, 1)) kb.press(key) @@ -156,7 +156,7 @@ def __init__( def Process(self, btn, idx, split_line): - key = kb.sp(btn.symbols[SYM_PARAMS][1]) + key = kb.sp(self.Get_param(btn, 1)) kb.release(key) diff --git a/commands_win32.py b/commands_win32.py new file mode 100644 index 0000000..4399469 --- /dev/null +++ b/commands_win32.py @@ -0,0 +1,207 @@ +# This module is VERY specific to Win32 +import command_base, ms, scripts, variables, win32gui, win32process, win32api +from constants import * + +LIB = "cmds_wn32" # name of this library (for logging) + +# ################################################## +# ### CLASS WIN32_GET_CURSOR ### +# ################################################## + +# class that defines the W_GET_CURSOR command -- gets the location of the cursor on the current form +class Win32_Get_Cursor(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("W_GET_CURSOR", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X value", False, True, PT_VAR, None, None), + ("Y value", False, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Store screen absolute cursor position in variables ({1}, {2})"), + ) ) + + + def get_cursor(self): + # get current cursor position within window + + res = (-1, -1) # failure value + + fg_win = win32gui.GetForegroundWindow() # find the current foreground window + fg_thread, fg_process = win32process.GetWindowThreadProcessId(fg_win) # get thread and process information + current_thread = win32api.GetCurrentThreadId() # find the current thread + win32process.AttachThreadInput(current_thread, fg_thread, True) # attach to the current thread + try: + res = win32gui.GetCaretPos() # find the caret + finally: + win32process.AttachThreadInput(current_thread, fg_thread, False) # and always detatch from the other thread + + return res # and this is where the text cursor is + + def Process(self, btn, idx, split_line): + x, y = self.get_cursor() # get x,y position of text cursor on fg window + + self.Set_param(btn, 1, x) # store the result + self.Set_param(btn, 2, y) + + +scripts.Add_command(Win32_Get_Cursor()) # register the command + + +# ################################################## +# ### CLASS W_GET_FG_HWND ### +# ################################################## + +# class that defines the W_GET_FG_HWND command - gets the handle of the current foreground window +class Win32_Get_Fg_Hwnd(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("W_GET_FG_HWND", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("HWND", False, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Return the handle of the current foreground window into {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + hwnd = win32gui.GetForegroundWindow() # get the current window + + #variables.Auto_store(btn.symbols[SYM_PARAMS][1], hwnd, btn.symbols) # Return the current window + self.Set_param(btn, 1, hwnd) + + +scripts.Add_command(Win32_Get_Fg_Hwnd()) # register the command + + +# ################################################## +# ### CLASS W_SET_FG_HWND ### +# ################################################## + +# class that defines the W_SET_FG_HWND command - makes the window pointed to by hwnd the current window +class Win32_Set_Fg_Hwnd(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("W_SET_FG_HWND", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("HWND", False, True, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Make window {1} the current window"), + ) ) + + + def Process(self, btn, idx, split_line): + hwnd = self.Get_param(btn, 1) # get the window handle from the passed variable (or constant) + win32gui.SetForegroundWindow(hwnd) # make it the foreground window + + +scripts.Add_command(Win32_Set_Fg_Hwnd()) # register the command + + +# ################################################## +# ### CLASS W_CLIENT_TO_SCREEN ### +# ################################################## + +# class that defines the W_CLIENT_TO_SCREEN command - converts a form relative coordinate to a screen (absolute) coord +class Win32_Client_To_Screen(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("W_CLIENT_TO_SCREEN", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("HWND", False, True, PT_INT, None, None), + ("X value", False, True, PT_VAR, None, None), + ("Y value", False, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " Convert form relative coord in ({1}, {2}) to screen (abs)"), + ) ) + + + def Process(self, btn, idx, split_line): + x = self.Get_param(btn, 2) # get x,y value + y = self.Get_param(btn, 3) + + x, y = win32gui.ClientToScreen(btn.symbols[SYM_PARAMS][1], (x, y)) # convert client coords to screen coords + + self.Set_param(btn, 2, x) # set new x, y values + self.Set_param(btn, 3, y) + + +scripts.Add_command(Win32_Client_To_Screen()) # register the command + + +# ################################################## +# ### CLASS W_FIND_HWND ### +# ################################################## + +# class that defines the W_FIND_HWND command - returns the nth matching window handle +class Win32_Find_Hwnd(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("W_FIND_HWND", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, False, PT_TEXT, None, None), # name to search for (use ~ for space + ("HWND", False, True, PT_VAR, None, None), # variable to contain HWND + ("M", False, True, PT_VAR, None, None), # number of matches found (if M Date: Wed, 17 Feb 2021 16:27:56 +0800 Subject: [PATCH 27/83] * Fixed error preventing layouts from being saved * Fix error in storing variables in RPN_EVAL --- commands_rpncalc.py | 2 +- files.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 30cd66d..167a6e1 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -473,7 +473,7 @@ def sto(self, symbols, cmd, cmds): ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? a = variables.top(symbols, 1) # will be stored from the top of the stack - variables.AutoStore(v, a, symbols) # "auto store" the value + variables.Auto_store(v, a, symbols) # "auto store" the value return ret diff --git a/files.py b/files.py index 68f0ec6..fb83e4e 100644 --- a/files.py +++ b/files.py @@ -109,7 +109,7 @@ def save_lp_to_layout(name): layout["buttons"].append([]) for y in range(9): color = lp_colors.curr_colors[x][y] - script_text = scripts.text[x][y] + script_text = scripts.buttons[x][y].script_str layout["buttons"][-1].append({"color": color, "text": script_text}) From 225938c93b6b4f5d912f03a1fa19ec89b242b6b1 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 17 Feb 2021 16:30:44 +0800 Subject: [PATCH 28/83] Preliminary changes leading to the Variable column allowing 3 values -- to determine if you get to use constants, or also variables by ref or value --- commands_mouse.py | 6 +++--- constants.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/commands_mouse.py b/commands_mouse.py index 07dd3ef..32ed6db 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -82,9 +82,9 @@ def __init__( super().__init__("M_SCROLL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Scroll amount", False, True, PT_INT, None, None), - ("X value", True, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("Scroll amount", False, VA_VAL, PT_INT, None, None), + ("X value", True, VA_VAL, PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) diff --git a/constants.py b/constants.py index 2a28709..ee76a4c 100644 --- a/constants.py +++ b/constants.py @@ -35,6 +35,11 @@ AV_P1_VALIDATION = 4 AV_P2_VALIDATION = 5 +# constants for allowing variables +VA_NO = False # 0 # only literals are allowed +VA_VAL = True # 1 # variables are passed by value (i.e. you can't change them) - CAN be a variuable +VA_REF = 2 # variables are passed by reference - MUST be a variable + # constants for parameter types # desc conv special (special means additional auto-validation) PT_INT = ("int", int, False) From 3b951f5cc1adf2e22584bbd28e099f2c9b1beaa8 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 17 Feb 2021 16:33:24 +0800 Subject: [PATCH 29/83] * add untested code to allow command removal (i.e. un-registering) * Significantly improved win32 routines to handle various exceptions --- commands_win32.py | 225 ++++++++++++++++++++++++++++++++++++++++++---- scripts.py | 13 +++ 2 files changed, 220 insertions(+), 18 deletions(-) diff --git a/commands_win32.py b/commands_win32.py index 4399469..d996e5f 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -1,15 +1,50 @@ # This module is VERY specific to Win32 -import command_base, ms, scripts, variables, win32gui, win32process, win32api +import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard from constants import * LIB = "cmds_wn32" # name of this library (for logging) +# ################################################## +# ### CLASS COMMAND_WIN32 ### +# ################################################## + +# class that defines more win32 stuff that gets used in various places +class Command_Win32(command_base.Command_Basic): + + # restores a window, returning its original state + def restore_window(self, hwnd, fg = False): + old_hwnd = win32gui.GetForegroundWindow() # save the current window + place = win32gui.GetWindowPlacement(hwnd) # get info about the window + + if place[1] == win32con.SW_SHOWMAXIMIZED: # if it is maximised + win32gui.ShowWindow(hwnd, win32con.SW_SHOWMAXIMISED) # then keep it maximised + elif place[1] == win32con.SW_SHOWMINIMIZED: # if minimised + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # then restore it + else: + win32gui.ShowWindow(hwnd, win32con.SW_NORMAL) # otherwise a normal show is fin + + if fg and (hwnd != old_hwnd): + win32gui.SetForegroundWindow(hwnd) + + return place[1], old_hwnd, hwnd # useful if you want to minimise it again + + # resets windows to what they were before the restore + def reset_window(self, old_state): + state, old_hwnd, hwnd = old_state + + if state == win32con.SW_SHOWMINIMIZED: # re-minimise if it was minimised + win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) + + if hwnd != old_hwnd: # set fg window if it was different + win32gui.SetForegroundWindow(old_hwnd) + + # ################################################## # ### CLASS WIN32_GET_CURSOR ### # ################################################## # class that defines the W_GET_CURSOR command -- gets the location of the cursor on the current form -class Win32_Get_Cursor(command_base.Command_Basic): +class Win32_Get_Cursor(Command_Win32): def __init__( self, ): @@ -58,7 +93,7 @@ def Process(self, btn, idx, split_line): # ################################################## # class that defines the W_GET_FG_HWND command - gets the handle of the current foreground window -class Win32_Get_Fg_Hwnd(command_base.Command_Basic): +class Win32_Get_Fg_Hwnd(Command_Win32): def __init__( self, ): @@ -90,7 +125,7 @@ def Process(self, btn, idx, split_line): # ################################################## # class that defines the W_SET_FG_HWND command - makes the window pointed to by hwnd the current window -class Win32_Set_Fg_Hwnd(command_base.Command_Basic): +class Win32_Set_Fg_Hwnd(Command_Win32): def __init__( self, ): @@ -108,10 +143,18 @@ def __init__( def Process(self, btn, idx, split_line): - hwnd = self.Get_param(btn, 1) # get the window handle from the passed variable (or constant) - win32gui.SetForegroundWindow(hwnd) # make it the foreground window - + hwnd = self.Get_param(btn, 1) # get the window handle from the passed variable (or constant) + + old_x, old_y = ms.get_pos() # save the position of the mouse + self.restore_window(hwnd) # show the window + + # positioning the mouse on the form while we make it the foreground seems to help + x, y = win32gui.ClientToScreen(hwnd, (10, 10)) # get a position just inside the window + ms.set_pos(x, y) # put the mouse on the form + win32gui.SetForegroundWindow(hwnd) # Make the window current + ms.set_pos(old_x, old_y) # restore the mouse position + scripts.Add_command(Win32_Set_Fg_Hwnd()) # register the command @@ -120,7 +163,7 @@ def Process(self, btn, idx, split_line): # ################################################## # class that defines the W_CLIENT_TO_SCREEN command - converts a form relative coordinate to a screen (absolute) coord -class Win32_Client_To_Screen(command_base.Command_Basic): +class Win32_Client_To_Screen(Command_Win32): def __init__( self, ): @@ -129,35 +172,84 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("HWND", False, True, PT_INT, None, None), ("X value", False, True, PT_VAR, None, None), ("Y value", False, True, PT_VAR, None, None), + ("HWND", True, True, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (3, " Convert form relative coord in ({1}, {2}) to screen (abs)"), + (2, " Convert form relative coord in ({1}, {2}) in curent window to screen (abs)"), + (3, " Convert form relative coord in ({1}, {2}) in window {3} to screen (abs)"), ) ) - - def Process(self, btn, idx, split_line): - x = self.Get_param(btn, 2) # get x,y value - y = self.Get_param(btn, 3) - x, y = win32gui.ClientToScreen(btn.symbols[SYM_PARAMS][1], (x, y)) # convert client coords to screen coords + def Process(self, btn, idx, split_line): + x = self.Get_param(btn, 1) # get x,y value + y = self.Get_param(btn, 2) - self.Set_param(btn, 2, x) # set new x, y values - self.Set_param(btn, 3, y) + hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window + state = self.restore_window(hwnd) + try: + x, y = win32gui.ClientToScreen(hwnd, (x, y)) # convert client coords to screen coords + + self.Set_param(btn, 1, x) # set new x, y values + self.Set_param(btn, 2, y) + finally: + self.reset_window(state) scripts.Add_command(Win32_Client_To_Screen()) # register the command +# ################################################## +# ### CLASS W_SCREEN_TO_CLIENT ### +# ################################################## + +# class that defines the W_CLIENT_TO_SCREEN command - converts a form relative coordinate to a screen (absolute) coord +class Win32_Screen_To_Client(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_SCREEN_TO_CLIENT", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X value", False, True, PT_VAR, None, None), + ("Y value", False, True, PT_VAR, None, None), + ("HWND", True, True, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Convert form absolute coord in ({1}, {2}) to relative to current window"), + (3, " Convert form absolute coord in ({1}, {2}) to relative to current window"), + ) ) + + + def Process(self, btn, idx, split_line): + x = self.Get_param(btn, 1) # get x,y value + y = self.Get_param(btn, 2) + + hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window + state = self.restore_window(hwnd) + try: + x, y = win32gui.ScreenToClient(hwnd, (x, y)) # convert client coords to screen coords + + self.Set_param(btn, 1, x) # set new x, y values + self.Set_param(btn, 2, y) + finally: + self.reset_window(state) + + +scripts.Add_command(Win32_Screen_To_Client()) # register the command + + # ################################################## # ### CLASS W_FIND_HWND ### # ################################################## # class that defines the W_FIND_HWND command - returns the nth matching window handle -class Win32_Find_Hwnd(command_base.Command_Basic): +class Win32_Find_Hwnd(Command_Win32): def __init__( self, ): @@ -205,3 +297,100 @@ def CheckWindow(hwnd, data): scripts.Add_command(Win32_Find_Hwnd()) # register the command + + +# ################################################## +# ### CLASS W_COPY ### +# ################################################## + +# class that defines the W_COPY command - copies and places (optionally) text into variable +class Win32_Copy(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_COPY", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Clipboard", True, True, PT_VAR, None, None), # variable to contain cut item + ), + ( + # num params, format string (trailing comma is important) + (0, " Copy into system clipboard"), + (1, " Copy into system clipboard and {1}"), + ) ) + + def Process(self, btn, idx, split_line): + + hwnd = win32gui.GetForegroundWindow() # get the current window + + try: # clear the clipboard + win32clipboard.OpenClipboard(hwnd) + win32clipboard.EmptyClipboard() + finally: + win32clipboard.CloseClipboard() + + try: # do the keyboard stuff for copy (sending a WM_COPY message does not always work) + kb.press(kb.sp('ctrl')) + kb.tap(kb.sp('c')) + finally: + kb.release(kb.sp('ctrl')) + #win32api.SendMessage(hwnd, win32con.WM_COPYDATA, 0, 0) # do a copy + + if self.Param_count(btn) > 0: # save to variable if required + try: + win32clipboard.OpenClipboard(hwnd) + t = win32clipboard.GetClipboardData(win32con.CF_TEXT) + self.Set_param(btn, 1, t) + finally: + win32clipboard.CloseClipboard() + + +scripts.Add_command(Win32_Copy()) # register the command + + +# ################################################## +# ### CLASS W_COPY ### +# ################################################## + +# class that defines the W_Paste command - copies and places (optionally) text into variable +class Win32_Paste(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_PASTE", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Clipboard", True, True, PT_VAR, None, None), # variable to contain item to paste + ), + ( + # num params, format string (trailing comma is important) + (0, " Paste from system clipboard"), + (1, " Paste from {1} via system clipboard"), + ) ) + + def Process(self, btn, idx, split_line): + + if self.Param_count(btn) > 0: # place variable into clipboard if required + hwnd = win32gui.GetForegroundWindow() # get the current window + + c = self.Get_param(btn, 1) # get the value + try: + win32clipboard.OpenClipboard(hwnd) + win32clipboard.EmptyClipboard() # clear the clipboard first (because that makes it work) + win32clipboard.SetClipboardText(str(c)) # and put the string in the clipboard + finally: + win32clipboard.CloseClipboard() + + # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste + try: + kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) + kb.tap(kb.sp('v')) + finally: + kb.release(kb.sp('ctrl')) + + +scripts.Add_command(Win32_Paste()) # register the command diff --git a/scripts.py b/scripts.py index ffb49e2..6dcf0b9 100644 --- a/scripts.py +++ b/scripts.py @@ -47,6 +47,19 @@ def Add_command( return p # return any replaced command +# Remove a command. This could be useful in handling subroutines (@@@ UNTESTED) + +def Remove_command( + command_name # the command to remove + ): + + if command_name in HEADERS: # if this was previously a header + HEADERS.pop(command_name) + + if command_name in VALID_COMMANDS: # if it already exists + HEADERS.pop(command_name) # remove it + + # Create a new symbol table. This contains information required for the script to run # it includes the locations of labels, loop counters, etc. If we implement variables # this is where we would place them From 3c09c39739eb3ccd2a1e370023ceebc9bdab6041 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 17 Feb 2021 16:35:22 +0800 Subject: [PATCH 30/83] * New command module for screen scraping. Initial command does OCR on a fragment of the screen. * Modification to command_list to add the new commands as optional. NOTE the python code to scrape the screen has a bug. A link is given to a solution --- command_list.py | 6 +++++ commands_scrape.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 commands_scrape.py diff --git a/command_list.py b/command_list.py index a879122..1ff5a3e 100644 --- a/command_list.py +++ b/command_list.py @@ -24,6 +24,12 @@ except ImportError: print("[LPHK] WARNING: Windows specific commands are not available") +# This library could be considered optional +try: + import commands_scrape +except ImportError: + print("[LPHK] WARNING: Screen scraping") + # Any that were not optional should set the error flag so we can exit if IMPORT_FATAL: sys.exit("[LPHK] ERROR: Required command modules are absent") diff --git a/commands_scrape.py b/commands_scrape.py new file mode 100644 index 0000000..c61ed5b --- /dev/null +++ b/commands_scrape.py @@ -0,0 +1,62 @@ +# This module is VERY specific to Win32 +import command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract +from constants import * + +LIB = "cmds_sscr" # name of this library (for logging) + +# ################################################## +# ### CLASS S_OCR_FORM_TEXT ### +# ################################################## + +# class that defines the S_OCR_FORM_TEXT command -- reads text from an image on the form +class Scrape_OCR_Form_Text(commands_win32.Command_Win32): + def __init__( + self, + ): + + super().__init__("S_OCR_FORM_TEXT", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X1 value", False, True, PT_INT, None, None), + ("Y1 value", False, True, PT_INT, None, None), + ("X2 value", False, True, PT_INT, None, None), + ("Y2 value", False, True, PT_INT, None, None), + ("OCR value", False, True, PT_VAR, None, None), + ("HWND", True, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (5, " OCR current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " OCR form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + ) ) + + + def Process(self, btn, idx, split_line): + p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) + + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) + state = self.restore_window(hwnd, True) + try: + import time + time.sleep(1) + p_from = win32gui.ClientToScreen(hwnd, p_from) + p_to = win32gui.ClientToScreen(hwnd, p_to) + box = p_from + p_to + print("@@@", box) + image = PIL.ImageGrab.grab(bbox = box) # @@@ bugfix https://github.com/python-pillow/Pillow/issues/1547 + image.save('C:/temp/temp.png') + finally: + self.reset_window(state) + + pytesseract.pytesseract.tesseract_cmd = r'C:\Users\HE123240\AppData\Local\Tesseract-OCR\tesseract.exe' + txt = pytesseract.image_to_string(image) + print("@@@", txt) + + self.Set_param(btn, 5, txt) # pass this back + + +scripts.Add_command(Scrape_OCR_Form_Text()) # register the command + + From 6992e784a3450fdee28bf329745c49409a4716b6 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 17 Feb 2021 18:42:12 +0800 Subject: [PATCH 31/83] Multiple fixes to command_scrape * Automatically finds the tesseract application in either of the default locations * Works on multi-monitor setups --- commands_scrape.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/commands_scrape.py b/commands_scrape.py index c61ed5b..db71a3b 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -1,9 +1,18 @@ # This module is VERY specific to Win32 -import command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract +import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract from constants import * LIB = "cmds_sscr" # name of this library (for logging) +T_PATH = os.getenv('LOCALAPPDATA') + '/Tesseract-OCR/tesseract.exe' +if not os.path.isfile(T_PATH): + T_PATH = os.getenv('PROGRAMFILES') + '/Tesseract-OCR/tesseract.exe' + if not os.path.isfile(T_PATH): + raise Exception("Tesseract OCR not installed or cannot be located") + +pytesseract.pytesseract.tesseract_cmd = T_PATH + + # ################################################## # ### CLASS S_OCR_FORM_TEXT ### # ################################################## @@ -33,28 +42,23 @@ def __init__( def Process(self, btn, idx, split_line): - p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) - p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) + p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) - state = self.restore_window(hwnd, True) + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the windoe we're using + state = self.restore_window(hwnd, True) # restore the window in question and make it FG try: - import time - time.sleep(1) - p_from = win32gui.ClientToScreen(hwnd, p_from) + p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screren coords p_to = win32gui.ClientToScreen(hwnd, p_to) - box = p_from + p_to - print("@@@", box) - image = PIL.ImageGrab.grab(bbox = box) # @@@ bugfix https://github.com/python-pillow/Pillow/issues/1547 - image.save('C:/temp/temp.png') + box = p_from + p_to # make a tuple with both coords + image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image + #image.save('C:/temp/temp.png') finally: - self.reset_window(state) + self.reset_window(state) # restore windows something like previous states - pytesseract.pytesseract.tesseract_cmd = r'C:\Users\HE123240\AppData\Local\Tesseract-OCR\tesseract.exe' - txt = pytesseract.image_to_string(image) - print("@@@", txt) + txt = pytesseract.image_to_string(image) # OCR the image - self.Set_param(btn, 5, txt) # pass this back + self.Set_param(btn, 5, txt) # pass the text back scripts.Add_command(Scrape_OCR_Form_Text()) # register the command From e6aa5ad3d08162c76f01e3c4abda32600abf99b5 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 15:52:05 +0800 Subject: [PATCH 32/83] * Change to argparse to better handle command line parameters * adding the ability to pass a layout on the command line to pre-load LPHK.py * add the ability to start LPHK minimised --- LPHK.py | 30 ++++++++++++++++++++---------- window.py | 18 +++++++++++++++++- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/LPHK.py b/LPHK.py index d0fa263..67ee012 100755 --- a/LPHK.py +++ b/LPHK.py @@ -1,4 +1,4 @@ -import sys, os, subprocess +import sys, os, subprocess, argparse from datetime import datetime print("\n!!!!!!!! DO NOT CLOSE THIS WINDOW WITHOUT SAVING !!!!!!!!\n") @@ -94,17 +94,27 @@ def datetime_str(): EXIT_ON_WINDOW_CLOSE = True - def init(): global EXIT_ON_WINDOW_CLOSE - if len(sys.argv) > 1: - if ("--debug" in sys.argv) or ("-d" in sys.argv): - EXIT_ON_WINDOW_CLOSE = False - print("[LPHK] Debugging mode active! Will not shut down on window close.") - print("[LPHK] Run shutdown() to manually close the program correctly.") - - else: - print("[LPHK] Invalid argument: " + sys.argv[1] + ". Ignoring...") + + ap = argparse.ArgumentParser() + ap.add_argument( + "-d", "--debug", + help = "turn on debugging mode", action="store_true") + ap.add_argument( + "-l", "--layout", + help = "load a layout", + type=argparse.FileType('r')) + ap.add_argument( + "-m", "--minimised", + help = "Start the application minimised", action="store_true") + + window.ARGS = vars(ap.parse_args()) + + if window.ARGS['debug']: + EXIT_ON_WINDOW_CLOSE = False + print("[LPHK] Debugging mode active! Will not shut down on window close.") + print("[LPHK] Run shutdown() to manually close the program correctly.") files.init(USER_PATH) sound.init(USER_PATH) diff --git a/window.py b/window.py index 8c51c46..2959b89 100644 --- a/window.py +++ b/window.py @@ -37,6 +37,7 @@ restart = False lp_object = None +ARGS = dict() load_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT, files.LEGACY_LAYOUT_EXT])] load_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT, files.LEGACY_SCRIPT_EXT])] @@ -205,7 +206,13 @@ def connect_lp(self): self.draw_canvas() self.enable_menu("Layout") self.stat["text"] = f"Connected to {lpcon.get_display_name(lp)}" - self.stat["bg"] = STAT_ACTIVE_COLOR + self.stat["bg"] = STAT_ACTIVE_COLOR + + # load a layout on startup + def load_initial_layout(self): + global ARGS + if ARGS['layout']: # did the user pass the option to load an initial layout? + files.load_layout_to_lp(ARGS['layout'].name) # Load it! def disconnect_lp(self): global lp_connected @@ -706,7 +713,13 @@ def make(): global app global root_destroyed global redetect_before_start + global ARGS root = tk.Tk() + + # does the user want to start the form minimised? + if ARGS['minimised']: + root.iconify() + root_destroyed = False root.protocol("WM_DELETE_WINDOW", close) root.resizable(False, False) @@ -717,7 +730,10 @@ def make(): root.iconbitmap(MAIN_ICON) app = Main_Window(root) app.raise_above_all() + app.after(100, app.connect_lp) + app.after(110, app.load_initial_layout) # Load the initial layout if you have specified one + app.mainloop() From ca7183321c930e8243bd4bb2563d2e132650ccb2 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 15:53:49 +0800 Subject: [PATCH 33/83] * a few minor documentation fixes * add an experimental "wait until idle" command --- commands_win32.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/commands_win32.py b/commands_win32.py index d996e5f..ca8c873 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -1,5 +1,5 @@ # This module is VERY specific to Win32 -import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard +import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard, win32event from constants import * LIB = "cmds_wn32" # name of this library (for logging) @@ -300,7 +300,7 @@ def CheckWindow(hwnd, data): # ################################################## -# ### CLASS W_COPY ### +# ### CLASS W_COPY ### # ################################################## # class that defines the W_COPY command - copies and places (optionally) text into variable @@ -351,7 +351,7 @@ def Process(self, btn, idx, split_line): # ################################################## -# ### CLASS W_COPY ### +# ### CLASS W_PASTE ### # ################################################## # class that defines the W_Paste command - copies and places (optionally) text into variable @@ -394,3 +394,40 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Win32_Paste()) # register the command + + +# ################################################## +# ### CLASS W_WAIT ### +# ################################################## + +# class that defines the W_WAIT command - waits until the process for a window handle is ready for input +class Win32_Wait(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_WAIT", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("HWND", False, True, PT_VAR, None, None), # variable to contain item to paste + ), + ( + # num params, format string (trailing comma is important) + (0, " Wait until {1} is ready for input"), + ) ) + + def Process(self, btn, idx, split_line): + + hwnd = self.Get_param(btn, 1) # get the window + tid, pid = win32process.GetWindowThreadProcessId(hwnd) # find the pid + hproc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION , False, pid) # find the process id + + res = win32con.WAIT_TIMEOUT + while res == win32con.WAIT_TIMEOUT: + res = win32event.WaitForInputIdle(hproc, 20) + if btn.Check_kill(): + return False + + +scripts.Add_command(Win32_Wait()) # register the command From cd16831c93be1b00e49c3f7df7208505106cb8f3 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 15:58:34 +0800 Subject: [PATCH 34/83] * change S_OCR_FORM_TEXT to S_OCR * new Scrape command S_HASH to get hash of an area of a window * new Scrape command S_FINGERPRINT to get fingerprint of an area of a screen * new Scrape command S_FDIST to get distance between fingerprints (to measure similarity of regions of a screen) --- commands_scrape.py | 167 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 158 insertions(+), 9 deletions(-) diff --git a/commands_scrape.py b/commands_scrape.py index db71a3b..248a6d5 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -1,5 +1,5 @@ # This module is VERY specific to Win32 -import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract +import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract, io, hashlib, imagehash, dhash from constants import * LIB = "cmds_sscr" # name of this library (for logging) @@ -13,17 +13,39 @@ pytesseract.pytesseract.tesseract_cmd = T_PATH +# ################################################## +# ### CLASS COMMAND_SCRAPE ### +# ################################################## + +# class that defines additional scraping stuff +class Command_Scrape(commands_win32.Command_Win32): + + # scrapes an image relative to a window + def get_image(self, hwnd, p_from, p_to): + state = self.restore_window(hwnd, True) # restore the window in question and make it FG + try: + p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords + p_to = win32gui.ClientToScreen(hwnd, p_to) + box = p_from + p_to # make a tuple with both coords + image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image + #image.save('C:/temp/temp.png') + finally: + self.reset_window(state) # restore windows something like previous states + + return image # return the image + + # ################################################## # ### CLASS S_OCR_FORM_TEXT ### # ################################################## # class that defines the S_OCR_FORM_TEXT command -- reads text from an image on the form -class Scrape_OCR_Form_Text(commands_win32.Command_Win32): +class Scrape_OCR_Form_Text(Command_Scrape): def __init__( self, ): - super().__init__("S_OCR_FORM_TEXT", # the name of the command as you have to enter it in the code + super().__init__("S_OCR", # the name of the command as you have to enter it in the code LIB, ( # Desc Opt Var type p1_val p2_val @@ -45,22 +67,149 @@ def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the windoe we're using + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using + + image = self.get_image(hwnd, p_from, p_to) # capture an image + + txt = pytesseract.image_to_string(image) # OCR the image + + self.Set_param(btn, 5, txt) # pass the text back + + +scripts.Add_command(Scrape_OCR_Form_Text()) # register the command + + +# ################################################## +# ### CLASS S_IMAGE_HASH ### +# ################################################## + +# class that defines the S_HASH command -- Takes an image and calculates a checksum +class Scrape_Image_Hash(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_HASH", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X1 value", False, True, PT_INT, None, None), + ("Y1 value", False, True, PT_INT, None, None), + ("X2 value", False, True, PT_INT, None, None), + ("Y2 value", False, True, PT_INT, None, None), + ("Hash value", False, True, PT_VAR, None, None), + ("HWND", True, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (5, " Hash of current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " Hash of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + ) ) + + + def Process(self, btn, idx, split_line): + p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords + + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using + + image = self.get_image(hwnd, p_from, p_to) # capture an image + + m = hashlib.md5() # create an md5 hash object + with io.BytesIO() as memf: # write image to memory + image.save(memf, 'PNG') # as png (lossless) + data = memf.getvalue() # get the data + m.update(data) # put it in the hash object + hash = m.hexdigest() # calculate the md5 hash + + self.Set_param(btn, 5, hash) # pass the hash back + + +scripts.Add_command(Scrape_Image_Hash()) # register the command + + +# ################################################## +# ### CLASS S_IMAGE_FINGERPRINT ### +# ################################################## + +# class that defines the S_FINGERPRINT command -- Takes an image and calculates a fingerprint +class Scrape_Image_Fingerprint(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_FINGERPRINT", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("X1 value", False, True, PT_INT, None, None), + ("Y1 value", False, True, PT_INT, None, None), + ("X2 value", False, True, PT_INT, None, None), + ("Y2 value", False, True, PT_INT, None, None), + ("Fingerprint",False, True, PT_VAR, None, None), + ("HWND", True, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (5, " Fingerprint of current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " Fingerprint of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + ) ) + + + def Process(self, btn, idx, split_line): + p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords + + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using state = self.restore_window(hwnd, True) # restore the window in question and make it FG try: - p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screren coords + p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords p_to = win32gui.ClientToScreen(hwnd, p_to) box = p_from + p_to # make a tuple with both coords image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image - #image.save('C:/temp/temp.png') + image.save('C:/temp/temp.png') finally: self.reset_window(state) # restore windows something like previous states - txt = pytesseract.image_to_string(image) # OCR the image + fingerprint = int(str(imagehash.dhash(image)),16) # calculate an image fingerprint - self.Set_param(btn, 5, txt) # pass the text back + self.Set_param(btn, 5, fingerprint) # pass the hash back -scripts.Add_command(Scrape_OCR_Form_Text()) # register the command +scripts.Add_command(Scrape_Image_Fingerprint()) # register the command +# ################################################## +# ### CLASS S_IMAGE_FINGERPRINT_DISTANCE ### +# ################################################## + +# class that defines the S_FDIST command -- calculates the hamming difference between fingerprints +class Scrape_Fingerprint_Distance(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("F1", False, True, PT_INT, None, None), + ("F2", False, True, PT_INT, None, None), + ("Distance", False, True, PT_VAR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), + ) ) + + + def Process(self, btn, idx, split_line): + f1 = self.Get_param(btn, 1) # get the fingerprints + f2 = self.Get_param(btn, 2) + + dist = dhash.get_num_bits_different(f1, f2) # hamming distance (number of bits different) + + self.Set_param(btn, 3, dist) # pass the distance back + + +scripts.Add_command(Scrape_Fingerprint_Distance()) # register the command From ff83025713556a1b147ca021af66a3405bc23a9b Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 17:32:22 +0800 Subject: [PATCH 35/83] * Command list documentation upgrade --- README.md | 38 +++++++++++++++++++++++++++++++++++--- commands_win32.py | 18 +++++++++--------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 771f0e3..cb1a1dc 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,38 @@ All Mouse movement commands can now use variables in place of constants. Variab * Local variables are local to the current script (and are maintained across executions) * The stack and local variables will be lost if the script is edited. +#### Win32 Commands [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +* `W_GET_CARET` + * Places the window-relative X and Y coordinates of the text cursor (caret) into the 2 variables passed as parameters +* `W_GET_FG_HWND` + * Places the handle of the currently active window into the variable passed as parameter 1 +* `W_SET_FG_HWND` + * Sets the current foreground window using the handle passed as the first parameter (variable or constant) +* `W_CLIENT_TO_SCREEN` + * Converts the X, Y values in the first 2 parameters from form relative to screen absolute. Assumes current FG window unless another handle is passed as parameter 3 +* `W_SCREEN_TO_CLIENT` + * Converts the X, Y values in the first 2 parameters from screen absolute to form relative. Assumes current FG window unless another handle is passed as parameter 3 +* `W_FIND_HWND` + * Searches for window with title (param 1) returning handle (param 2). Param 3 allows handles for duplicate windows, param 4 returns the number of duplicate windows. +* `W_COPY` + * Executes a "copy" to clipboard, and optionally to variable (param 1) on the current window +* `W_PASTE` + * Executes a "paste" from clipboard, or optionally from variable (param 1) in the current window +* `W_WAIT` + * Waits until the window (param 1) is ready for input + +#### Screen Scraping Commands [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +* `S_OCR` + * pass x1 y1 x2 y2 to describe a window-relative rectangle + * the region of the screen is OCRed and the result is returned in param 5 (a variable) + * if other than the current FG window, pass window handle as param 6 +* `S_HASH` + * same params as S_OCR except param 5 is a variable to hold the MD5 hash of the image area +* `S_FINGERPRINT` + * same params as S_OCR except param 5 is a variable to hold a fingerprint of the image area +* `S_FDIST` + * first 2 parameters are a pair of fingerprints. Third param is the distance between them 0 = very close. + ### Key Names [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) For the `PRESS`, `RELEASE`, and `TAP` commands, all single character non-whitespace keys and the following key names are allowed: * `alt` @@ -434,7 +466,7 @@ In order of priority: * Draw icons * With this, allow buttons to be bound with the light off * Add startup config file - * Default layout specification + * Default layout specification -- DONE * Auto connect overide * Force launchpad model setting * Add more sound commands @@ -451,8 +483,8 @@ In order of priority: * ~~Should use a `conda` environment created from an `environment.yml` file~~ * Should copy LPHK files into an appropriate directory * Should give options to add various shortcuts -* Add temporary command `__M_PRINT_POPUP__` that gives a pop-up with the current cursor position -* Option to minimize to system tray +* Add temporary command `__M_PRINT_POPUP__` that gives a pop-up with the current cursor position -- (Can now be done with a script) +* Option to minimize to system tray - DONE for startup * Tkinter does not provide a way to do this. There may be Windows-specific extentions I can use, but maybe not. * Add auto-update feature using `git` * ~~There will be a VERSION file in the main directory with the version string~~ diff --git a/commands_win32.py b/commands_win32.py index ca8c873..f030e21 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -43,13 +43,13 @@ def reset_window(self, old_state): # ### CLASS WIN32_GET_CURSOR ### # ################################################## -# class that defines the W_GET_CURSOR command -- gets the location of the cursor on the current form -class Win32_Get_Cursor(Command_Win32): +# class that defines the W_GET_CARET command -- gets the location of the cursor on the current form +class Win32_Get_Caret(Command_Win32): def __init__( self, ): - super().__init__("W_GET_CURSOR", # the name of the command as you have to enter it in the code + super().__init__("W_GET_CARET", # the name of the command as you have to enter it in the code LIB, ( # Desc Opt Var type p1_val p2_val @@ -58,12 +58,12 @@ def __init__( ), ( # num params, format string (trailing comma is important) - (2, " Store screen absolute cursor position in variables ({1}, {2})"), + (2, " Store screen absolute caret position in variables ({1}, {2})"), ) ) - def get_cursor(self): - # get current cursor position within window + def get_caret(self): + # get current caret position within window res = (-1, -1) # failure value @@ -85,7 +85,7 @@ def Process(self, btn, idx, split_line): self.Set_param(btn, 2, y) -scripts.Add_command(Win32_Get_Cursor()) # register the command +scripts.Add_command(Win32_Get_Caret()) # register the command # ################################################## @@ -205,7 +205,7 @@ def Process(self, btn, idx, split_line): # ### CLASS W_SCREEN_TO_CLIENT ### # ################################################## -# class that defines the W_CLIENT_TO_SCREEN command - converts a form relative coordinate to a screen (absolute) coord +# class that defines the W_SCREEN_TO_CLIENT command - converts a screen (absolute) coord to a form relative coordinate class Win32_Screen_To_Client(Command_Win32): def __init__( self, @@ -222,7 +222,7 @@ def __init__( ( # num params, format string (trailing comma is important) (2, " Convert form absolute coord in ({1}, {2}) to relative to current window"), - (3, " Convert form absolute coord in ({1}, {2}) to relative to current window"), + (3, " Convert form absolute coord in ({1}, {2}) to relative to window {3}"), ) ) From 4f74e0007347951e402d21faee087370923eb5ad Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 21:53:42 +0800 Subject: [PATCH 36/83] Modifications made to move the specification of the requirement for a variable from the type of the variable to the flag indicating a variable was allowed. What was once a True/False value now has three values AVV_NO, AVV_YES, and AVV_REQD. Subsequent to this, the type PT_VAR has been removed, and parameters that are passed by reference now have a type. However, that type is not currently checked at runtime. --- command_base.py | 63 +++++++++++++++++++++---------------------- command_list.py | 8 +++--- commands_control.py | 52 +++++++++++++++++------------------ commands_external.py | 4 +-- commands_keys.py | 16 +++++------ commands_mouse.py | 64 ++++++++++++++++++++++---------------------- commands_scrape.py | 50 +++++++++++++++++----------------- commands_win32.py | 52 +++++++++++++++++------------------ constants.py | 14 +++++----- variables.py | 3 ++- 10 files changed, 163 insertions(+), 163 deletions(-) diff --git a/command_base.py b/command_base.py index 5fbe20a..83effed 100644 --- a/command_base.py +++ b/command_base.py @@ -394,35 +394,34 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # should we do special validation? if ret == None or ((type(ret) == bool) and ret): - if not val[AV_TYPE][AVT_SPECIAL]: - return True - - if val_validation == AV_P1_VALIDATION: - if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only - # check for duplicate label - if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", btn.Line(idx)) - - # add label to symbol table # Add the new label to the labels in the symbol table - btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number - elif val[AV_TYPE] == PT_KEY: # Keys have pass 1 validation only - # check for valid key - if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? - return ("Unknown key", btn.Line(idx)) - elif val[AV_TYPE] == PT_BOOL: # booleans have pass 1 validation only - # check for valid boolean value - if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? - return ("Invalid boolean value", btn.Line(idx)) - elif val[AV_TYPE] == PT_VAR: # mandatory variables have pass 1 validation only - # check for valid variable name - if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? - return ("Invalid variable name", btn.Line(idx)) - - elif val_validation == AV_P2_VALIDATION: - if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only - # check for existance of label - if split_line[n] not in btn.symbols[SYM_LABELS]: - return ("Target not found", btn.Line(idx)) + if val[AV_VAR_OK] == AVV_REQD: + # check for valid variable name + if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? + return ("Invalid variable name", btn.Line(idx)) + + elif val[AV_TYPE][AVT_SPECIAL]: + if val_validation == AV_P1_VALIDATION: + if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only + # check for duplicate label + if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? + return ("Duplicate LABEL", btn.Line(idx)) + + # add label to symbol table # Add the new label to the labels in the symbol table + btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number + elif val[AV_TYPE] == PT_KEY: # Keys have pass 1 validation only + # check for valid key + if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? + return ("Unknown key", btn.Line(idx)) + elif val[AV_TYPE] == PT_BOOL: # booleans have pass 1 validation only + # check for valid boolean value + if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? + return ("Invalid boolean value", btn.Line(idx)) + + elif val_validation == AV_P2_VALIDATION: + if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only + # check for existance of label + if split_line[n] not in btn.symbols[SYM_LABELS]: + return ("Target not found", btn.Line(idx)) return True @@ -465,7 +464,7 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): if pass_no == 1: v = split_line[n] - if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK] and not (self.auto_validate[n-1][AV_TYPE] == PT_VAR) : + if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK] == AVV_YES: v = variables.get_value(split_line[n], btn.symbols) if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) @@ -502,7 +501,7 @@ def Get_param(self, btn, n, other=None): if param == None: ret = other else: - if val[AV_TYPE] == PT_VAR: + if val[AV_VAR_OK] == AVV_REQD: ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: ret = param @@ -514,7 +513,7 @@ def Get_param(self, btn, n, other=None): def Set_param(self, btn, n, val): param = btn.symbols[SYM_PARAMS][n] av = self.auto_validate[n-1] - if av[AV_TYPE] == PT_VAR: + if val[AV_VAR_OK] == AVV_REQD: variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable diff --git a/command_list.py b/command_list.py index 1ff5a3e..5b18fb8 100644 --- a/command_list.py +++ b/command_list.py @@ -15,20 +15,20 @@ # This library could be considered optional try: import commands_rpncalc -except ImportError: +except: print("[LPHK] WARNING: RPN_EVAL command is not available") # This library could be considered optional try: import commands_win32 -except ImportError: +except: print("[LPHK] WARNING: Windows specific commands are not available") # This library could be considered optional try: import commands_scrape -except ImportError: - print("[LPHK] WARNING: Screen scraping") +except: + print("[LPHK] WARNING: Screen scraping commands are unavailable") # Any that were not optional should set the error flag so we can exit if IMPORT_FATAL: diff --git a/commands_control.py b/commands_control.py index ce7cccd..dde425c 100644 --- a/commands_control.py +++ b/commands_control.py @@ -37,8 +37,8 @@ def __init__( "LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Label", False, False, PT_TARGET, None, None), + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_TARGET, None, None), ), ( # num params, format string (trailing comma is important) @@ -189,8 +189,8 @@ def __init__( "GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) @@ -215,8 +215,8 @@ def __init__( "IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) @@ -244,8 +244,8 @@ def __init__( "IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) @@ -273,9 +273,9 @@ def __init__( "REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -310,9 +310,9 @@ def __init__( "REPEAT", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("Label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -345,9 +345,9 @@ def __init__( "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -382,9 +382,9 @@ def __init__( "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -417,9 +417,9 @@ def __init__( "IF_UNPRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -454,9 +454,9 @@ def __init__( "IF_UNPRESSED_REPEAT", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, False, PT_LABEL, None, None), - ("Repeats", False, False, PT_INT, variables.Validate_gt_zero, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), + ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_external.py b/commands_external.py index 5d6fe6d..4a1f91f 100644 --- a/commands_external.py +++ b/commands_external.py @@ -152,8 +152,8 @@ def __init__( "SOUND_STOP", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Fade value", True , True, PT_INT, variables.Validate_gt_zero, None), + # Desc Opt Var type p1_val p2_val + ("Fade value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_keys.py b/commands_keys.py index 8fa669c..6c7d531 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -50,10 +50,10 @@ def __init__( "TAP", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, False, PT_KEY, None, None), - ("Times", True, True, PT_INT, variables.Validate_gt_zero, None), - ("Duration", True, True, PT_FLOAT, variables.Validate_ge_zero, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), + ("Times", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), + ("Duration", True, AVV_YES, PT_FLOAT, variables.Validate_ge_zero, None), ), ( # num params, format string (trailing comma is important) @@ -115,8 +115,8 @@ def __init__( "PRESS", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, False, PT_KEY, None, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), ), ( # num params, format string (trailing comma is important) @@ -146,8 +146,8 @@ def __init__( "RELEASE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, False, PT_KEY, None, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_mouse.py b/commands_mouse.py index 32ed6db..6d82eb3 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -16,9 +16,9 @@ def __init__( super().__init__("M_MOVE", # the name of the command as you have to enter it in the code LIB, # the name of this module ( # description of parameters - # Desc Opt Var type p1_val p2_val (trailing comma is important) - ("X value", False, True, PT_INT, None, None), - ("Y value", False, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val (trailing comma is important) + ("X value", False, AVV_NO, PT_INT, None, None), + ("Y value", False, AVV_NO, PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -49,9 +49,9 @@ def __init__( super().__init__("M_SET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_INT, None, None), - ("Y value", False, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_YES, PT_INT, None, None), + ("Y value", False, AVV_YES, PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -82,9 +82,9 @@ def __init__( super().__init__("M_SCROLL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Scroll amount", False, VA_VAL, PT_INT, None, None), - ("X value", True, VA_VAL, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("Scroll amount", True, AVV_NO, PT_INT, None, None), + ("X value", False, AVV_YES, PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -116,13 +116,13 @@ def __init__( super().__init__("M_LINE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X1 value", False, True, PT_INT, None, None), - ("Y1 value", False, True, PT_INT, None, None), - ("X2 value", False, True, PT_INT, None, None), - ("Y2 value", False, True, PT_INT, None, None), - ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), - ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + # Desc Opt Var type p1_val p2_val + ("X1 value", False, AVV_YES, PT_INT, None, None), + ("Y1 value", False, AVV_YES, PT_INT, None, None), + ("X2 value", False, AVV_YES, PT_INT, None, None), + ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("Wait value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -172,11 +172,11 @@ def __init__( super().__init__("M_LINE_MOVE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_INT, None, None), - ("Y value", False, True, PT_INT, None, None), - ("Wait value", True, True, PT_INT, variables.Validate_gt_zero, None), - ("Skip value", True, True, PT_INT, variables.Validate_ge_zero, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_YES, PT_INT, None, None), + ("Y value", False, AVV_YES, PT_INT, None, None), + ("Wait value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), + ("Skip value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), ), ( # num params, format string (trailing comma is important) @@ -225,11 +225,11 @@ def __init__( super().__init__("M_LINE_SET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_INT, None, None), - ("Y value", False, True, PT_INT, None, None), - ("Wait value", True, True, PT_INT, variables.Validate_ge_zero, None), - ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_YES, PT_INT, None, None), + ("Y value", False, AVV_YES, PT_INT, None, None), + ("Wait value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -274,9 +274,9 @@ def __init__( super().__init__("M_RECALL_LINE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Wait value", True , True, PT_INT, variables.Validate_ge_zero, None), - ("Skip value", True, True, PT_INT, variables.Validate_gt_zero, None), + # Desc Opt Var type p1_val p2_val + ("Wait value", True , AVV_YES, PT_INT, variables.Validate_ge_zero, None), + ("Skip value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) @@ -331,9 +331,9 @@ def __init__( super().__init__("M_STORE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", True , True, PT_VAR, None, None), - ("Y value", False, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("X value", True , AVV_REQD, PT_INT, None, None), + ("Y value", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_scrape.py b/commands_scrape.py index 248a6d5..288a149 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -48,13 +48,13 @@ def __init__( super().__init__("S_OCR", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X1 value", False, True, PT_INT, None, None), - ("Y1 value", False, True, PT_INT, None, None), - ("X2 value", False, True, PT_INT, None, None), - ("Y2 value", False, True, PT_INT, None, None), - ("OCR value", False, True, PT_VAR, None, None), - ("HWND", True, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("X1 value", False, AVV_YES, PT_INT, None, None), + ("Y1 value", False, AVV_YES, PT_INT, None, None), + ("X2 value", False, AVV_YES, PT_INT, None, None), + ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("OCR value", False, AVV_REQD, PT_TEXT, None, None), + ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -92,13 +92,13 @@ def __init__( super().__init__("S_HASH", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X1 value", False, True, PT_INT, None, None), - ("Y1 value", False, True, PT_INT, None, None), - ("X2 value", False, True, PT_INT, None, None), - ("Y2 value", False, True, PT_INT, None, None), - ("Hash value", False, True, PT_VAR, None, None), - ("HWND", True, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("X1 value", False, AVV_YES, PT_INT, None, None), + ("Y1 value", False, AVV_YES, PT_INT, None, None), + ("X2 value", False, AVV_YES, PT_INT, None, None), + ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("Hash value", False, AVV_REQD, PT_INT, None, None), + ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -141,13 +141,13 @@ def __init__( super().__init__("S_FINGERPRINT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X1 value", False, True, PT_INT, None, None), - ("Y1 value", False, True, PT_INT, None, None), - ("X2 value", False, True, PT_INT, None, None), - ("Y2 value", False, True, PT_INT, None, None), - ("Fingerprint",False, True, PT_VAR, None, None), - ("HWND", True, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("X1 value", False, AVV_YES, PT_INT, None, None), + ("Y1 value", False, AVV_YES, PT_INT, None, None), + ("X2 value", False, AVV_YES, PT_INT, None, None), + ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("Fingerprint",False, AVV_REQD, PT_INT, None, None), + ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -192,10 +192,10 @@ def __init__( super().__init__("S_FDIST", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("F1", False, True, PT_INT, None, None), - ("F2", False, True, PT_INT, None, None), - ("Distance", False, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("F1", False, AVV_YES, PT_INT, None, None), + ("F2", False, AVV_YES, PT_INT, None, None), + ("Distance", False, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_win32.py b/commands_win32.py index f030e21..1e442dd 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -52,9 +52,9 @@ def __init__( super().__init__("W_GET_CARET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_VAR, None, None), - ("Y value", False, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_REQD, PT_INT, None, None), + ("Y value", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -101,8 +101,8 @@ def __init__( super().__init__("W_GET_FG_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("HWND", False, True, PT_VAR, None, None), + # Desc Opt Var type p1_val p2_val + ("HWND", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -133,8 +133,8 @@ def __init__( super().__init__("W_SET_FG_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("HWND", False, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("HWND", False, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -171,10 +171,10 @@ def __init__( super().__init__("W_CLIENT_TO_SCREEN", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_VAR, None, None), - ("Y value", False, True, PT_VAR, None, None), - ("HWND", True, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_REQD, PT_INT, None, None), + ("Y value", False, AVV_REQD, PT_INT, None, None), + ("HWND", True, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -214,10 +214,10 @@ def __init__( super().__init__("W_SCREEN_TO_CLIENT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, True, PT_VAR, None, None), - ("Y value", False, True, PT_VAR, None, None), - ("HWND", True, True, PT_INT, None, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_REQD, PT_INT, None, None), + ("Y value", False, AVV_REQD, PT_INT, None, None), + ("HWND", True, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) @@ -257,11 +257,11 @@ def __init__( super().__init__("W_FIND_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Title", False, False, PT_TEXT, None, None), # name to search for (use ~ for space - ("HWND", False, True, PT_VAR, None, None), # variable to contain HWND - ("M", False, True, PT_VAR, None, None), # number of matches found (if M= AVV_YES and valid_var_name(split_line[p]): # a variable is OK here return True + else: return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), split_line[p]) if val[val_validation]: From cf42a80dc598e77e008e5ae5e6455eed21070dc8 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Feb 2021 22:46:04 +0800 Subject: [PATCH 37/83] Updated "NewCommands" describing the simplest way to add more commands --- NewCommands.md | 202 ++++++++++++++++----------------------------- commands_scrape.py | 6 +- 2 files changed, 74 insertions(+), 134 deletions(-) diff --git a/NewCommands.md b/NewCommands.md index 44628e7..845051c 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -31,29 +31,6 @@ You can take advantage of several levels of abstraction when writing your own co This is where I'm aiming to get. You will still need to write some code to perform the actual function, but you can do so by just writing that code, and all the rest is handled for you. (I'm doing all this work because I'm lazy, and I want you to be able to be lazy too!) -#### Requirements: - * Declare your parameters - * Declare your output message format(s) - * Define a method to perform the function on prevalidated parameters and include this method in the definition - * Register your command - -#### Benefits: - * NO individual Validate method - * NO individual Run method - * Optional parameters supported - * Allows use of internally managed numeric variables as well as literal values - * Minimal debugging needed - * Easily understandable, without needing to read code - * Unlikely to introduce maintenance overhead - -#### Costs: - * Parameters must conform to "standard" declarations - * Less flexible execution of command - -#### An example? - -There will be one as soon as this is developed! - ### Declarative definition, with minimal coding Currently working, this involves declarative definition of the parameters, with manual control over the stages of execution of the Run method. @@ -80,52 +57,42 @@ This has a shorter list of requirements, but it's more coding, and requires more #### An example - Decoding an old version of the MOUSE_SCROLL command -The `M_SCROLL` command looks like this: +The `S_FDIST` command looks like this: ```python - def __init__( - self, - ): - - super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, None, None), \ - ("Scroll amount", False, True, "integer", int, None, None) ) ) - +# ################################################## +# ### CLASS SCRAPE_FINGERPRINT_DISTANCE ### +# ################################################## - def Run( +# class that defines the S_FDIST command -- calculates the hamming difference between fingerprints +class Scrape_Fingerprint_Distance(command_base.Command_Basic): + def __init__( self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously ): - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [RS_INIT, RS_GET]) == -1: - return -1 - - if self.param[2]: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - - if self.Partial_run(*params, [RS_VALIDATE]) == -1: - return -1 - - if self.param[2]: - ms.scroll(float(self.param[2]), float(self.param[1])) - else: - ms.scroll(0, float(self.param[1])) - - return idx+1 - - finally: - self.Partial_run(*params, [RS_FINAL]) - + super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("F1", False, AVV_YES, PT_INT, None, None), + ("F2", False, AVV_YES, PT_INT, None, None), + ("Distance", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), + ) ) + + + def Process(self, btn, idx, split_line): + f1 = self.Get_param(btn, 1) # get the fingerprints + f2 = self.Get_param(btn, 2) + + dist = dhash.get_num_bits_different(f1, f2) # hamming distance (number of bits different) + + self.Set_param(btn, 3, dist) # pass the distance back -scripts.Add_command(Mouse_Scroll()) # register the command + +scripts.Add_command(Scrape_Fingerprint_Distance()) # register the command ``` We will examine the 6 parts you need to consider @@ -138,20 +105,20 @@ The class should always begin with some documentation to both highlight the star ```python # ################################################## -# ### CLASS Mouse_Scroll ### +# ### CLASS SCRAPE_FINGERPRINT_DISTANCE ### # ################################################## -# class that defines the M_SCROLL command (???) +# class that defines the S_FDIST command -- calculates the hamming difference between fingerprints ``` ##### Part 2 - The Class definition -The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. +The most important part of the class definition is that the new class is derived from the appropriate base class. For normal commands, this should be `command_base.Command_Basic` although experienced python programmers could also create a new command definition that derives from another command. Looking at some of the other commands you'll often find a new class defined based on `command_base.Command_Basic` or one of it's derivitives so that common functionality can be made available to all commands derived from it. It is also important that you define a unique name for your new command. I recommend using *Module*_*Command*, where *Module* is the part of the module name after `commands_`. ```python -class Mouse_Scroll(command_base.Command_Basic): +class Scrape_Fingerprint_Distance(command_base.Command_Basic): ``` ##### Part 3 - Class Initialization @@ -163,25 +130,36 @@ The initialization of a command class serves to define the name of the command. self, ): - super().__init__("M_SCROLL", lib, ( # the name of the command as you have to enter it in the code - # Desc Opt Var type conv p1_val p2_val - ("X value", False, True, "integer", int, None, None), \ - ("Scroll amount", False, True, "integer", int, None, None) ) ) + super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("F1", False, AVV_YES, PT_INT, None, None), + ("F2", False, AVV_YES, PT_INT, None, None), + ("Distance", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), + ) ) ``` -The 5th line defines the name of the command and also passes the name of the current library (lib) to the the object. Note that commands are case sensitive, so the name should be in all uppercase to be consistent with other commands. The current library will be used to define where the command originates from in some of the low level reporting functions. +The 5th line defines the name of the command. Note that command names are case sensitive, so the name should be in all uppercase to be consistent. + +Line 6 passes the name of the current library (lib) to the the object. The current library will be used to define where the command originates from in some of the low level reporting functions. -Lines 7 and 8 define the two parameters expected for this function. Note that this definition actually commences on line 5 with the opening parenthesis. +Lines 9 to 11 define the three parameters expected for this function. Note that this definition actually commences on line 7 with the opening parenthesis and ends on line 12 with the closing parenthesis. Each parameter is defined by a tuple. 7 values must be entered in each tuple. The parameters are: * The name of the parameter -- this will be used in multiple places to create messages that are understandable. * Is this parameter optional? -- Entering True here means that it is valid to pass all the previous parameters, but stop here. Entering False means that if you have supplied one fewer than this parameter, then you must also supply this one. - * Can a variable be substituted? - Entering True allows a variable name to be used here instead of a literal value. Entering False means that only literal values are permitted. Note that literal values are validated before execution, variables are validated at execution. - * What is the type of the parameter -- This is a human-readable description of the datatype required. It is used in messages. - * Conversion function to desired type -- as an example used here, "int()" is used to convert strings (and other types) to integers. Other functions, like float, or str can be used. Note that this is an actual function name, so it is acceptable to define your own function if needed. + * Can a variable be substituted? - Entering AVV_NO means that only literals are allowed. AVV_YES means that literals or a variable are permitted. AVV_REQD means a variable is required. AVV_REQD is typically used where the command returns or changes the value in this variable. Note that literal values are validated before execution, variables are validated at execution. + * What is the type of the parameter -- This is a human-readable description of the datatype required. It is used in messages, and in validation of literals and variables. * Pass 1 Validation - this is a function used to perform pass 1 evaluation of the parameter value. This will be called at validation for literal values, and at execution for variable values. Examples of this are variables.Validate_ge_zero, a function that validates a numeric parameter value is greater than or equal to zero. Use None if no validation is required. Write your own pass 1 validation routine if you wish! * Pass 2 Validation - this is a function called only on pass 2 of the validation. It is required for commands that reference labels (to determine if the label exists). None are defined yet, but that will change... +Line 15 contains a skeleton of logging information. This is a tuple that consists of the number of variables passed, and the format of the message. In this example it is only valid to pass all three parameters so there is only a single tuple defined. The magic values '{n}' are replaced by the literal or variable passed to the command. + ##### Part 4 - Command Validation No code is required here! The default validation code for the class can handle it all for you! @@ -189,77 +167,39 @@ No code is required here! The default validation code for the class can handle ##### Part 5 - Command Execution ```python - def Run( - self, - idx: int, # The current line number - split_line, # The current line, split - symbols, # The symbol table (a dictionary containing labels, loop counters etc.) - coords, # Tuple of printable coords as well as the individual x and y values - is_async # True if the script is running asynchronously - ): - - params = [idx, split_line, symbols, coords, is_async] - try: - if self.Partial_run(*params, [RS_INIT, RS_GET]) == -1: - return -1 - - if self.param[2]: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll (" + str(self.param[1]) + ", " + str(self.param[2]) + ")") - else: - print("[" + lib + "] " + coords + " Line:" + str(idx+1) + " Scroll " + str(self.param[1])) - - if self.Partial_run(*params, [RS_VALIDATE]) == -1: - return -1 - - if self.param[2]: - ms.scroll(float(self.param[2]), float(self.param[1])) - else: - ms.scroll(0, float(self.param[1])) - - return idx+1 - - finally: - self.Partial_run(*params, [RS_FINAL]) + def Process(self, btn, idx, split_line): + f1 = self.Get_param(btn, 1) # get the fingerprints + f2 = self.Get_param(btn, 2) + + dist = dhash.get_num_bits_different(f1, f2) # hamming distance (number of bits different) + + self.Set_param(btn, 3, dist) # pass the distance back ``` -The first 8 lines are the standard header and should be copied verbatim. - -The definition of SYM_PARAMS on line 10 is simply a shortcut so we don't have to type the exact same set of parameters over and over again for various management functions that need to know this stuff. - -The main code is placed inside a TRY...FINALLY block to ensure that the finalization code is called. Currently this doesn't do a lot, but as it may become more necessary in later versions, it is recommended that you code the execution in this manner. +The first line is the standard header and should be copied verbatim. -Within the TRY...FINALLY block, we call "self.Partial_run()" to execute parts of the standard execution flow. The parts you can call are: - * RS_INIT - Perform any initialization required (should ALWAYS be called) - * RS_GET - Get the parameters, and evaluates variables (not strictly required for a command with no parameters, but STRONGLY encouraged) - * RS_INFO - Display the command with the values. In most cases you'll want to implement this yourself as the default is pretty basic - * RS_VALIDATE - Perform run-time validation of them, possibly printing messages. (not requied if you're not using variables, but STRONGLY encouraged) - * RS_RUN - Perform the standard process of running the command. This should NEVER be used in this situation, as you will code this part! - * RS_FINAL - Perform any finalization required. (should ALWAYS be called - in the FINALLY) +The parameters are accessed using 'self.Get_param(btn, n)' where 'n' is the parameter number. This function takes care of both literal values and values passed in variables. A third (optional) parameter allows you to set a default value if that parameter was not supplied. It is also possible to use the functions 'self.Param_count(btn)' that returns the number of parameters passed, and 'self.Has_param(btn, n)' to determine if parameter number 'n' has been passed. -In this example, we are first running RS_INIT, and RS_GET. If a Partial_run returns -1, you should return immediately with -1. +Tyically, once the parameters are obtained, the code is inserted to perform the function. In this case it is a single line calling 'dhash.get_num_bits_different()' -After the RS_GET, you can refer to self.params[n] where n is from 0 to the maximum number of parameters. self.params[0] is the command name, and self.params[1] to self.params[n] are the parameters from 1 to n. At this point, optional parameters that have not been passed will be None, literal values will be validated and be the correct type, and variable values will be thethe correct types, but not yet validated. - -After performing the 3 initial steps, we do our own implementation of the RS_INFO. - -Next, the RS_VALIDATE step is performed. After this, variable values will be validated. - -After validation, we do what is required based on the parameters passed, and return idx + 1 (that's the next line). If we had a serious error we could return -1 to abort the script. Instead of returning idx+1, we could possibly just return the value of the RS_RUN process, however this requires more code, and introduces additional complexities. Just do the simple thing and return the correct value! - -No matter how we return, the FINALLY block will ensure that the finalization is called. +Finally, if there are any parameters to be returned, call 'self.Set_param(btn, n, v)' to return the value 'v' in the nth parameter. Note that attempting to return values for parameters that are not defined as AVV_REQD will silently fail. ##### Part 6 - Command Integration The final step is to include code to incorporate this command into the set of commands available for scripts. ```python -scripts.Add_command(Mouse_Scroll()) # register the command +scripts.Add_command(Scrape_Fingerprint_Distance()) # register the command ``` This line creates a command object, and passes it to the routine which adds it to the list of available commands. -It is important to note that the definition of more than one command with exactly the same name will result in only the second one being available. +It is important to note that the definition of more than one command with exactly the same name will result in only the last one being available. + + +## DO NOT READ ANY FURTHER IF YOU VALUE YOUR SANITY. I HAVE TO MAKE THIS EASIER TO UNDERSTAND +There is value (indeed necessity) in using some of these techniques, however many of them are just unneccessary now that I have refined the process of creating commands from their very early implementation. ### Declarative definition, with manual control over parameter handling diff --git a/commands_scrape.py b/commands_scrape.py index 288a149..8c6943b 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -180,11 +180,11 @@ def Process(self, btn, idx, split_line): # ################################################## -# ### CLASS S_IMAGE_FINGERPRINT_DISTANCE ### +# ### CLASS SCRAPE_FINGERPRINT_DISTANCE ### # ################################################## # class that defines the S_FDIST command -- calculates the hamming difference between fingerprints -class Scrape_Fingerprint_Distance(Command_Scrape): +class Scrape_Fingerprint_Distance(command_base.Command_Basic): def __init__( self, ): @@ -195,7 +195,7 @@ def __init__( # Desc Opt Var type p1_val p2_val ("F1", False, AVV_YES, PT_INT, None, None), ("F2", False, AVV_YES, PT_INT, None, None), - ("Distance", False, AVV_YES, PT_INT, None, None), + ("Distance", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) From 56bd596fe00a8be0655e0dd7faa8eab70ab87cbc Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 19 Feb 2021 21:33:59 +0800 Subject: [PATCH 38/83] * Several bug fixes * New command CODE_NOWAIT to run an arbitrary command and return the pid immediately * New command W_PIT_TO_HWND to return the handle of the primary window associated with a pid * Preliminary work with constants for handling variables better --- README.md | 6 ++++++ command_base.py | 11 +++++----- commands_external.py | 45 ++++++++++++++++++++++++++++++++++++---- commands_win32.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ constants.py | 20 ++++++++++-------- 5 files changed, 114 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cb1a1dc..c37e104 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,10 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * Open website (argument 1) in default browser. * `WEB_NEW` * Open website (argument 1) in default browser, try new window. +* `CODE` + * runs anything, waits for it to end +* `CODE_NOWAIT` + * runs anything, returns immediately with pid of process #### Keypresses [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) * `PRESS` @@ -374,6 +378,8 @@ All Mouse movement commands can now use variables in place of constants. Variab * Executes a "paste" from clipboard, or optionally from variable (param 1) in the current window * `W_WAIT` * Waits until the window (param 1) is ready for input +* `W_PID_TO_HWND` + * Converts PID in param 1 to HWND in param 2 #### Screen Scraping Commands [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) * `S_OCR` diff --git a/command_base.py b/command_base.py index 83effed..46db537 100644 --- a/command_base.py +++ b/command_base.py @@ -464,10 +464,11 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): if pass_no == 1: v = split_line[n] - if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_VAR_OK] == AVV_YES: - v = variables.get_value(split_line[n], btn.symbols) - if self.auto_validate and n <= len(self.auto_validate) and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: - v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) + if self.auto_validate and n <= len(self.auto_validate): + if self.auto_validate[n-1][AV_VAR_OK] == AVV_YES: + v = variables.get_value(split_line[n], btn.symbols) + elif self.auto_validate[n-1][AV_VAR_OK] != AVV_REQD and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: + v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) return v elif pass_no == 2: if len(self.auto_validate) != 0: @@ -513,7 +514,7 @@ def Get_param(self, btn, n, other=None): def Set_param(self, btn, n, val): param = btn.symbols[SYM_PARAMS][n] av = self.auto_validate[n-1] - if val[AV_VAR_OK] == AVV_REQD: + if av[AV_VAR_OK] == AVV_REQD: variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable diff --git a/commands_external.py b/commands_external.py index 4a1f91f..65fe4bf 100644 --- a/commands_external.py +++ b/commands_external.py @@ -125,11 +125,11 @@ def Run( ): if len(split_line) > 2: - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1] + \ " at volume " + str(split_line[2])) sound.play(split_line[1], float(split_line[2])) else: - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Play sound file " + split_line[1]) sound.play(split_line[1]) return idx+1 @@ -210,12 +210,12 @@ def Run( ): args = " ".join(split_line[1:]) - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Running code: " + args) + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Running code: " + args) try: subprocess.run(args) except Exception as e: - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) return idx+1 @@ -223,3 +223,40 @@ def Run( scripts.Add_command(External_Code()) # register the command +# ################################################## +# ### CLASS External_Code_NOWAIT ### +# ################################################## + +# class that defines the CODE_NOWAIT command (runs something). This returns immediately +class External_Code_Nowait(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("CODE_NOWAIT", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("PID", False, AVV_REQD, PT_INT, None, None), # variable to get PID of new process + ("Command", False, AVV_NO, PT_TEXT, None, None), # text of command + ), + ( + # num params, format string (trailing comma is important) + (2, " Run {2} retuning PID in {1}"), + ) ) + + def Process(self, btn, idx, split_line): + args = self.Get_param(btn, 2).replace('~', ' ') # get the command we want to run + + pid = -1 + try: + proc = subprocess.Popen(args) #['C:\Program Files\Internet Explorer\iexplore.exe', 'http://hsecs/']) + except Exception as e: + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) + + self.Set_param(btn, 1, proc.pid) # get the window + + +scripts.Add_command(External_Code_Nowait()) # register the command + + diff --git a/commands_win32.py b/commands_win32.py index 1e442dd..8d83779 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -38,6 +38,20 @@ def reset_window(self, old_state): if hwnd != old_hwnd: # set fg window if it was different win32gui.SetForegroundWindow(old_hwnd) + # returns a list of hwnds for a process id + def get_hwnds_for_pid(self, pid): + + def callback (hwnd, hwnds): + if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): + _, found_pid = win32process.GetWindowThreadProcessId(hwnd) + if found_pid == pid: + hwnds.append (hwnd) + return True + + hwnds = [] + win32gui.EnumWindows (callback, hwnds) + return hwnds + # ################################################## # ### CLASS WIN32_GET_CURSOR ### @@ -431,3 +445,38 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Win32_Wait()) # register the command + + +# ################################################## +# ### CLASS W_PID_TO_HWND ### +# ################################################## + +# class that defines the W_WAIT command - waits until the process for a window handle is ready for input +class Win32_Pid_To_Hwnd(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_PID_TO_HWND", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("pid", False, AVV_YES, PT_INT, None, None), # variable containing pid + ("hwnd", False, AVV_REQD, PT_INT, None, None), # variable to contain hwnd + ), + ( + # num params, format string (trailing comma is important) + (2, " return hwnd in {2} for pid {1}"), + ) ) + + def Process(self, btn, idx, split_line): + + pid = self.Get_param(btn, 1) # get the pid + hwnds = self.get_hwnds_for_pid(pid) # find any hwnds + if len(hwnds) == 1: + self.Set_param(btn, 2, hwnds[0]) + else: + self.Set_param(btn, 2, hwnds) + + +scripts.Add_command(Win32_Pid_To_Hwnd()) # register the command diff --git a/constants.py b/constants.py index 26a8d70..8fdd4c3 100644 --- a/constants.py +++ b/constants.py @@ -37,19 +37,23 @@ AVT_DESC = 0 # and this is what is inside the tuple AVT_CONV = 1 AVT_SPECIAL = 2 +AVT_LAST = 3 AV_P1_VALIDATION = 4 AV_P2_VALIDATION = 5 # constants for parameter types -# desc conv special (special means additional auto-validation) -PT_INT = ("int", int, False) -PT_FLOAT = ("float", float, False) -PT_TEXT = ("text", str, False) -PT_LABEL = ("label", str, True) # Note that this is for a reference to a label, not the definition of a label! -PT_TARGET = ("target", str, True) # Note that this is for the definition of a target (e.g. creating a label) -PT_KEY = ("key", str, True) # This is a key literal -PT_BOOL = ("bool", str, True) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables +# desc conv special last (special means additional auto-validation, last means MUST be last) +PT_INT = ("int", int, False, False) +PT_FLOAT = ("float", float, False, False) +#PT_STR = ("str", str, True, False) # a string without whitespace or a quoted string +#PT_STRS = ("strs", str, True, True) # 1 or more strings without whitespace or quoted +#PT_LINE = ("line", str, True, True) # the rest of the line following first preceeding whitespace +PT_TEXT = ("text", str, False, False) # a string without whitespace <-- obsolete once the above are added +PT_LABEL = ("label", str, True, False) # Note that this is for a reference to a label, not the definition of a label! +PT_TARGET = ("target", str, True, False) # Note that this is for the definition of a target (e.g. creating a label) +PT_KEY = ("key", str, True, False) # This is a key literal +PT_BOOL = ("bool", str, True, False) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables # constants for auto_message AM_COUNT = 0 From 7b9ff516a48be67e5f2758c7a16b4bca02008ca6 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 21 Feb 2021 21:01:51 +0800 Subject: [PATCH 39/83] * "Pass number" line only printed if there is an error * New parameter type PT_STR that requires quoted text (", ', or `) variables * New parameter type PT_STRS that allows multiple quoted strings or variables * New parameter type PT_LINE that takes the rest of the line as the value * Old PT_TEXT should be considered deprecated * Provision for certain parameter types to require placement at the end (e.g. PT_STRS and PT_LINE) * Split_text function now handles quoted strings (but not embedded double quotes -- yet) * Split text adds a leading " to parameters of type PT_STR or PT_STRS to allow variables to be distinguished * Many routines in class Command_Basic modified to handle variable parameter counts (PT_STRS) * Several routines in class Command_Basic modified to handle "last" parameters * Better handling of valid parameter counts to allow both a variable number of valid parameters and simultaneously no maximum * Correctly handle case where a command has no parameters * Parse_script handles errors returned from Split_text * New command RPN_SET to set a variable to a string value (not really RPN!) * Internal renaming of RPN classes. * All new commands changed from PT_TEXT to PT_STR * New commands now allow variables in place of strings where appropriate * New commands no longer use ~ to indicate an embedded space * Definition PLATFORM etc moved to constants.py * Modifications to make the code insensitive to actual values of some constants (by use of sets rather than assuming an order) * CODE_NOWAIT uses PT_STRS to ensure command and parameters are separated. This means the correct pid is returned (it works if you combine them into 1 argument, but the wrong pid is returned -- an intermediate process??) * WIN32 and SCRAPE commands are only imported if PLATFORM is "windows" * Several additional bugs squashed. * Some code simplified to make it more readable --- LPHK.py | 13 +--- command_base.py | 172 ++++++++++++++++++++++++++----------------- command_list.py | 25 ++++--- commands_external.py | 13 ++-- commands_rpncalc.py | 34 ++++++++- commands_scrape.py | 2 +- commands_win32.py | 8 +- constants.py | 41 ++++++++--- scripts.py | 107 ++++++++++++++++++++++++--- variables.py | 4 +- 10 files changed, 296 insertions(+), 123 deletions(-) diff --git a/LPHK.py b/LPHK.py index 67ee012..ab421f5 100755 --- a/LPHK.py +++ b/LPHK.py @@ -1,22 +1,11 @@ import sys, os, subprocess, argparse from datetime import datetime +from constants import * print("\n!!!!!!!! DO NOT CLOSE THIS WINDOW WITHOUT SAVING !!!!!!!!\n") LOG_TITLE = "LPHK.log" -# Get platform information -PLATFORMS = [ {"search_string": "win", "name_string": "windows"}, - {"search_string": "linux", "name_string": "linux"}, - {"search_string": "darwin", "name_string": "macintosh"} ] -PLATFORM = None -for plat in PLATFORMS: - if sys.platform.startswith(plat["search_string"]): - PLATFORM = plat["name_string"] - break -if PLATFORM == None: - PLATFORM = "other" - # Test if this is a PyInstaller executable or a .py file if getattr(sys, 'frozen', False): IS_EXE = True diff --git a/command_base.py b/command_base.py index 46db537..93867f8 100644 --- a/command_base.py +++ b/command_base.py @@ -25,6 +25,8 @@ def __init__( self.auto_validate = auto_validate # any auto-validation, if defined self.auto_message = auto_message # format for any messages we need + self.validate_init() + self.valid_max_params = self.Calc_valid_max_params() # calculate the max number of parmeters self.valid_num_params = self.Calc_valid_param_counts() # calculate the set of valid numbers of parameters @@ -32,6 +34,17 @@ def __init__( self.validation_states = [VS_COUNT, VS_PASS_1, VS_PASS_2] # by default we'll do a count and both passes if you don't override + def validate_init(self): + # This helps validate the parameters passed to __init__. It helps enforce the rules + avl = self.auto_validate + if avl != None: + np = len(avl) - 1 + for p, av in enumerate(avl): + if av[AV_TYPE][AVT_LAST] and p < np: # variable type is "last" but it's not the last + raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') is not the last parameter') + if not (av[AV_VAR_OK] in av[AV_TYPE][AVT_MAX_VAR]): # some types can't be passed variables + raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') specifies a variable type that is not permittted') + def Validate( # This is a low level validation routine. If you take over this function you must take # responsibility for all validation. @@ -297,8 +310,11 @@ def Calc_valid_max_params(self): # Return the maximum number of parameters. We can calculate this simply based on the number defined # in the auto_validate. If you aren't using the auto_validate, then you may need to set this yourself # in the __init__() - if self.auto_validate: - return len(self.auto_validate) + # if the last parameter is "last" then there is no maximum! + avl = self.auto_validate + if avl: + if len(avl) == 0 or not avl[-1][AV_TYPE][AVT_LAST]: + return len(avl) return None @@ -313,15 +329,18 @@ def Calc_valid_param_counts(self): # validation routine that n or more parameters are acceptable -- this is great for comments etc. ret = None - if self.auto_validate: + avl = self.auto_validate + if avl: ret = [] - vn = len(self.auto_validate) - for i in range(vn): - i_val = self.auto_validate[i] # get the parameter - if (i_val[AV_OPTIONAL] == True): # if this one is optional + vn = len(avl) + for i, av in enumerate(avl): + if av[AV_OPTIONAL]: # if this one is optional ret += [i] # then 1 fewer is OK - ret += [vn] # it's always valid to pass *all* the parameters + if vn > 0 and avl[-1][AV_TYPE][AVT_LAST]: + ret += [vn, None] # if the last parameter is a "last" parameter, there is no limit on parameters + else: + ret += [vn] # it's always valid to pass *all* the parameters return ret @@ -335,18 +354,20 @@ def Validate_param_count(self, ret, btn, idx, split_line): if not (ret == None or ((type(ret) == bool) and ret)): return ret - return variables.Check_num_params(btn, self, idx, split_line) + v = variables.Check_num_params(btn, self, idx, split_line) + + return v def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. # This function improves efficiency. - if ((self.valid_max_params == None and n_passed == 0) or (self.valid_max_params < n_passed)) or \ - (len(self.valid_num_params) == 2 and self.valid_num_params[1] == None): + vmp = self.valid_max_params + if (vmp == None) or (vmp < n_passed): return n_passed else: - return self.valid_max_params + return vmp def Validate_params(self, ret, btn, idx, split_line, val_validation): @@ -381,51 +402,56 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): if n >= len(split_line): return ret - if self.auto_validate == None: # no auto validation can be done + if self.auto_validate == None or self.auto_validate == (): # no auto validation can be done return ret if n <= len(self.auto_validate): # the normal auto-validation val = self.auto_validate[n-1] - - opt = self.valid_num_params == [] or (set(range(1,n)) & set(self.valid_num_params)) != [] - - ret = variables.Check_generic_param(btn, self, idx, split_line, n, val, val_validation) - - # should we do special validation? - if ret == None or ((type(ret) == bool) and ret): - if val[AV_VAR_OK] == AVV_REQD: - # check for valid variable name - if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? - return ("Invalid variable name", btn.Line(idx)) - - elif val[AV_TYPE][AVT_SPECIAL]: - if val_validation == AV_P1_VALIDATION: - if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only - # check for duplicate label - if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", btn.Line(idx)) - - # add label to symbol table # Add the new label to the labels in the symbol table - btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number - elif val[AV_TYPE] == PT_KEY: # Keys have pass 1 validation only - # check for valid key - if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? - return ("Unknown key", btn.Line(idx)) - elif val[AV_TYPE] == PT_BOOL: # booleans have pass 1 validation only - # check for valid boolean value - if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? - return ("Invalid boolean value", btn.Line(idx)) - - elif val_validation == AV_P2_VALIDATION: - if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only - # check for existance of label - if split_line[n] not in btn.symbols[SYM_LABELS]: - return ("Target not found", btn.Line(idx)) + else: + # special case for "last" parameters + val = self.auto_validate[-1] + + opt = self.valid_num_params == [] or \ + self.valid_num_params[-1] == None or \ + (set(range(1,n)) & set(self.valid_num_params)) != [] + + ret = variables.Check_generic_param(btn, self, idx, split_line, n, val, val_validation) + + # should we do special validation? + if ret == None or ((type(ret) == bool) and ret): + if val[AV_VAR_OK] == AVV_REQD: + # check for valid variable name + if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? + return ("Invalid variable name", btn.Line(idx)) - return True + elif val[AV_TYPE][AVT_SPECIAL]: + if val_validation == AV_P1_VALIDATION: + if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only + # check for duplicate label + if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? + return ("Duplicate LABEL", btn.Line(idx)) + + # add label to symbol table # Add the new label to the labels in the symbol table + btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number + elif val[AV_TYPE] == PT_KEY: # Keys have pass 1 validation only + # check for valid key + if kb.sp(split_line[n]) == None: # Does the key exist (if not, that's bad)? + return ("Unknown key", btn.Line(idx)) + elif val[AV_TYPE] == PT_BOOL: # booleans have pass 1 validation only + # check for valid boolean value + if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? + return ("Invalid boolean value", btn.Line(idx)) - return (ret, btn.Line(idx)) + elif val_validation == AV_P2_VALIDATION: + if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only + # check for existance of label + if split_line[n] not in btn.symbols[SYM_LABELS]: + return ("Target not found", btn.Line(idx)) + + return True + + return (ret, btn.Line(idx)) def Run_params(self, ret, btn, idx, split_line, pass_no): @@ -461,26 +487,31 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): # Note that pass 1 returns the variable value, where pass 2 returns a value indicating # if validation has passed. - if pass_no == 1: - v = split_line[n] - - if self.auto_validate and n <= len(self.auto_validate): - if self.auto_validate[n-1][AV_VAR_OK] == AVV_YES: + avl = self.auto_validate + if avl: + if n <= len(avl): + av = avl[n-1] + else: + av = avl[-1] + + if pass_no == 1: + v = split_line[n] + + if av[AV_VAR_OK] == AVV_YES: v = variables.get_value(split_line[n], btn.symbols) - elif self.auto_validate[n-1][AV_VAR_OK] != AVV_REQD and self.auto_validate[n-1][AV_TYPE] and self.auto_validate[n-1][AV_TYPE][AVT_CONV]: - v = self.auto_validate[n-1][AV_TYPE][AVT_CONV](v) - return v - elif pass_no == 2: - if len(self.auto_validate) != 0: - val = self.auto_validate[n-1] + elif av[AV_VAR_OK] != AVV_REQD and av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: + v = av[AV_TYPE][AVT_CONV](v) + + return v + elif pass_no == 2: ok = ret - if val[AV_P1_VALIDATION]: - ok = val[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, val[AV_DESCRIPTION], n, split_line[n]) + if av[AV_P1_VALIDATION]: + ok = av[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) if ok != True: print("[" + self.lib + "] " + btn.coords + " " + ok) ret = -1 - + return ret @@ -496,8 +527,12 @@ def Param_count(self, btn): # gets the value of the nth parameter (button is required for context). Other is default value if param does not exist - def Get_param(self, btn, n, other=None): - val = self.auto_validate[n-1] + def Get_param(self, btn, n, other=None): + # handle the repeating last parameter + avl = len(self.auto_validate) + m = min(n, avl) + val = self.auto_validate[m-1] + param = btn.symbols[SYM_PARAMS][n] if param == None: ret = other @@ -505,7 +540,10 @@ def Get_param(self, btn, n, other=None): if val[AV_VAR_OK] == AVV_REQD: ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: - ret = param + if type(param) == str and param[0:1] == '"': + ret = param[1:] + else: + ret = param return ret diff --git a/command_list.py b/command_list.py index 5b18fb8..b8aca8b 100644 --- a/command_list.py +++ b/command_list.py @@ -3,6 +3,7 @@ IMPORT_FATAL = False import sys +from constants import * import \ commands_header, \ @@ -15,20 +16,26 @@ # This library could be considered optional try: import commands_rpncalc -except: +except ImportError: print("[LPHK] WARNING: RPN_EVAL command is not available") # This library could be considered optional -try: - import commands_win32 -except: - print("[LPHK] WARNING: Windows specific commands are not available") +if PLATFORM == "windows": + try: + import commands_win32 + except ImportError: + print("[LPHK] ERROR: Windows specific commands are not available") +else: + print("[LPHK] WARNING: Windows specific commands can not be loaded") # This library could be considered optional -try: - import commands_scrape -except: - print("[LPHK] WARNING: Screen scraping commands are unavailable") +if PLATFORM == "windows": + try: + import commands_scrape + except ImportError: + print("[LPHK] ERROR: Screen scraping commands are not available") +else: + print("[LPHK] WARNING: Screen scraping commands can not be loaded") # Any that were not optional should set the error flag so we can exit if IMPORT_FATAL: diff --git a/commands_external.py b/commands_external.py index 65fe4bf..deb7fc5 100644 --- a/commands_external.py +++ b/commands_external.py @@ -238,7 +238,7 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("PID", False, AVV_REQD, PT_INT, None, None), # variable to get PID of new process - ("Command", False, AVV_NO, PT_TEXT, None, None), # text of command + ("Command", False, AVV_NO, PT_STRS, None, None), # text of command ), ( # num params, format string (trailing comma is important) @@ -246,17 +246,20 @@ def __init__( ) ) def Process(self, btn, idx, split_line): - args = self.Get_param(btn, 2).replace('~', ' ') # get the command we want to run + args = [] + for i in range(2, self.Param_count(btn)+1): + args += [self.Get_param(btn, i)] # get the command we want to run pid = -1 try: - proc = subprocess.Popen(args) #['C:\Program Files\Internet Explorer\iexplore.exe', 'http://hsecs/']) + proc = subprocess.Popen(args) + pid = proc.pid except Exception as e: print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) - self.Set_param(btn, 1, proc.pid) # get the window + self.Set_param(btn, 1, pid) # return the pid -scripts.Add_command(External_Code_Nowait()) # register the command +scripts.Add_command(External_Code_Nowait()) # register the command diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 167a6e1..49428cc 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -17,7 +17,7 @@ # passing parameters to and from other routines if the stack is preserved # in the symbol table. In this version The output is to the log, but it # is easily extended. -class RpnCalc_Rpn_Eval(command_base.Command_Basic): +class Rpn_Eval(command_base.Command_Basic): def __init__( self, ): @@ -663,5 +663,35 @@ def abort_script(self, symbols, cmd, cmds): return False -scripts.Add_command(RpnCalc_Rpn_Eval()) # register the command +scripts.Add_command(Rpn_Eval()) # register the command + +# ################################################## +# ### CLASS RPN_SET ### +# ################################################## + +# class that defines the RPN_SET command -- Sets a variable to a string value +class Rpn_Set(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RPN_SET", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Variable", False, AVV_REQD, PT_STR, None, None), + ("Value", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Assign '{2} to variable {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + val = self.Get_param(btn, 2) # Get the from coords + self.Set_param(btn, 1, val) # pass the hash back + + +scripts.Add_command(Rpn_Set()) # register the command \ No newline at end of file diff --git a/commands_scrape.py b/commands_scrape.py index 8c6943b..ed25f61 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -53,7 +53,7 @@ def __init__( ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), ("Y2 value", False, AVV_YES, PT_INT, None, None), - ("OCR value", False, AVV_REQD, PT_TEXT, None, None), + ("OCR value", False, AVV_REQD, PT_STR, None, None), ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( diff --git a/commands_win32.py b/commands_win32.py index 8d83779..1994bb5 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -272,7 +272,7 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_YES, PT_TEXT, None, None), # name to search for (use ~ for space + ("Title", False, AVV_NO, PT_STR, None, None), # name to search for ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M 0: + if l2[0] == q: + if len(l2) == 1 or l2[1] == ' ': + l2 = l2[1:].strip() + return True, out, l2 + elif line[1] == q: + out += q + l2 = l2[2:] + else: + return False, out, line + else: + out += l2[0] + l2 = l2[1:] + + return False, out, line + # for all other commands, split on spaces - return line.split(" ") + if isinstance(command, command_base.Command_Basic): + pline = line # something we can alter + avl = command.auto_validate + if avl != None and len(avl) > 0: + cmd, pline = split1(pline) # the command is always a simple split + sline = [cmd] # add it to the return variable + + n = -1 + while len(pline) > 0: + n += 1 + if n < len(avl): + av = avl[n] + else: + av = avl[-1] + + desc = av[AV_TYPE][AVT_DESC] + + if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Just the next parameter, unless it starts with a quote ['"`] + if pline[0] in ['"', "'", '`']: + if av[AV_VAR_OK] == AVV_REQD: + return ('Error, quoted string not permitted for param #' + str(n+1), line) # literal not expected + else: + ok, param, pline = strip_quoted(pline) + if ok: + sline += ['"'+param] + else: + return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote + else: + if av[AV_VAR_OK] != AVV_NO: + param = pline.split()[0] + pline = pline[len(param):].strip() + if not variables.valid_var_name(param): + return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... + else: + sline += [param] + else: + return ('Error starting quoted string for param#' + str(n+1), line) # This is generally a missing initial quote + + elif desc == PT_LINE[AVT_DESC]: # the rest of the line (regardless of spaces) + sline += [line] + pline = "" + + else: + param = pline.split(" ")[0] + sline += [param] + pline = pline[len(param):].strip() + + return sline + + else: + # without autovalidate we just split on spaces + return line.split(" ") # run a script @@ -319,9 +403,12 @@ def Main_logic(idx): command = VALID_COMMANDS[cmd_txt] split_line = self.Split_text(command, cmd_txt, line) - - # now run the command - return command.Run(self, idx, split_line) + + if type(split_line) == tuple: + print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") + else: + # now run the command + return command.Run(self, idx, split_line) else: print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") diff --git a/variables.py b/variables.py index 01b6674..67897b9 100644 --- a/variables.py +++ b/variables.py @@ -105,7 +105,7 @@ def Check_num_params(btn, cmd, idx, split_line): return True # anything is valid ln = len(cmd.valid_num_params) - n = len(split_line)-1 + n = len(split_line)-1 if ln == 2 and cmd.valid_num_params[1] == None: if n >= cmd.valid_num_params[0]: return True @@ -145,7 +145,7 @@ def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): try: temp = val[AV_TYPE][AVT_CONV](split_line[p]) except: - if val[AV_VAR_OK] >= AVV_YES and valid_var_name(split_line[p]): # a variable is OK here + if val[AV_VAR_OK] in AWS_YES and valid_var_name(split_line[p]): # a variable is OK here return True else: return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), split_line[p]) From 2e98c3447a249dbf5a6c371eb898f19f5bb24739 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 21 Feb 2021 21:32:01 +0800 Subject: [PATCH 40/83] * Bug introduced while checking in squashed :-o * And squashed another bug found while squashing that one! * And some documentation (that's a feature, not a bug) --- README.md | 17 +++++++++-------- command_base.py | 2 +- commands_rpncalc.py | 12 +++++++----- variables.py | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c37e104..f37c8e1 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,8 @@ All Mouse movement commands can now use variables in place of constants. Variab * The global variables are global to all scripts. * Local variables are local to the current script (and are maintained across executions) * The stack and local variables will be lost if the script is edited. - +* `RPN_SET` + * Appends all values passed as parameters 2 (strings and variables) onwards and assignes the result to the variable parameter 1 #### Win32 Commands [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) * `W_GET_CARET` * Places the window-relative X and Y coordinates of the text cursor (caret) into the 2 variables passed as parameters @@ -481,7 +482,7 @@ In order of priority: * Add `SOUND_VOLUME` to set the sound volume by label * Add `SOUND_STOP` to stop playing sound by label and delete the sound label * Add `SOUND_ALL_*` commands to stop/change the volume of all sounds -* Let `SOUND` use spaces in it's path if it has double quotes around it +* Let `SOUND` use spaces in it's path if it has double quotes around it (this can now be added easily) * Let program function as a layout editor without LP connection * Would probably be easier to write a "Dummy LP" class * Make an installer for Linux @@ -491,7 +492,7 @@ In order of priority: * Should give options to add various shortcuts * Add temporary command `__M_PRINT_POPUP__` that gives a pop-up with the current cursor position -- (Can now be done with a script) * Option to minimize to system tray - DONE for startup - * Tkinter does not provide a way to do this. There may be Windows-specific extentions I can use, but maybe not. + * Tkinter does not provide a way to do this (yes it does :-)). There may be Windows-specific extentions I can use, but maybe not. * Add auto-update feature using `git` * ~~There will be a VERSION file in the main directory with the version string~~ * This can be polled at `https://raw.githubusercontent.com/nimaid/LPHK/master/VERSION` @@ -525,7 +526,7 @@ In order of priority: * There are a few complex refactoring tasks required for this, I will be crossing them off here on the testing branch: * ~~Make a killable delay/time library that monitors thread kill flags~~ * ~~Port keyboard functions over to LPHKfunction modules~~ - * Make `commands.py` module to house the actual command logic + * Make `commands.py` module to house the actual command logic (DONE) * Move `@SIMPLE` to keyboard module. * Allow F['COMMAND']['macro'] = True to disallow other non-comment lines in the script. Default is False. * Macros will automatically have `_` added to the beginning (`@` will only be for headers) @@ -537,16 +538,16 @@ In order of priority: * Write the importer library (test standalone w/ simple delay) * ~~Lobotomize the program (read: remove the hellish logic in scripts.py)~~ * Integrate the importer into the main program (scripts.py) - * Find and kill all of the bugs + * Find and kill all of the bugs (DONE, replaced with brand new bugs) * Port the rest of the old logic to LPHKfuction modules * Deal with the Pandora's box that porting those functions will open (this list will probably grow) - * Make a way for modules to use standard commands, and to use other modules - * Take a drink and merge the branches + * Make a way for modules to use standard commands, and to use other modules (DONE?) + * Take a drink and merge the branches (Let me buy you a beer) * Allow named arguments for certain commands * Add a `Choose default MIDI device` option to the `Sound` menu. (For multiple launchpads plugged in) * Add a third argument to `SOUND` for overriding the default sound device * Add variables and mathematical evaluation (mostly done!) -* Add conditional jumps based on value comparisons (Would this make LPHKscript Turing complete? :D) +* Add conditional jumps based on value comparisons (Would this make LPHKscript Turing complete? :D) (DONE?) * Add syntax highlighting * Add GUI scaling * Full support for Launchpad Pro diff --git a/command_base.py b/command_base.py index 93867f8..a995e77 100644 --- a/command_base.py +++ b/command_base.py @@ -540,7 +540,7 @@ def Get_param(self, btn, n, other=None): if val[AV_VAR_OK] == AVV_REQD: ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: - if type(param) == str and param[0:1] == '"': + if type(param) == str and val[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': ret = param[1:] else: ret = param diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 49428cc..25f08f7 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -670,7 +670,7 @@ def abort_script(self, symbols, cmd, cmds): # ### CLASS RPN_SET ### # ################################################## -# class that defines the RPN_SET command -- Sets a variable to a string value +# class that defines the RPN_SET command -- Sets a variable to a string value (or a heap of appended string values) class Rpn_Set(command_base.Command_Basic): def __init__( self, @@ -681,17 +681,19 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Variable", False, AVV_REQD, PT_STR, None, None), - ("Value", False, AVV_YES, PT_STR, None, None), + ("Value", False, AVV_YES, PT_STRS, None, None), ), ( # num params, format string (trailing comma is important) - (2, " Assign '{2} to variable {1}"), + (2, " Assign '{2}' to variable {1}"), ) ) def Process(self, btn, idx, split_line): - val = self.Get_param(btn, 2) # Get the from coords - self.Set_param(btn, 1, val) # pass the hash back + val = '' + for i in range(2, self.Param_count(btn)+1): # for each parameter (after the first) + val += str(self.Get_param(btn, i)) # append all the values (force to string) + self.Set_param(btn, 1, val) # pass the combined string back scripts.Add_command(Rpn_Set()) # register the command \ No newline at end of file diff --git a/variables.py b/variables.py index 67897b9..a7a8e28 100644 --- a/variables.py +++ b/variables.py @@ -145,7 +145,7 @@ def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): try: temp = val[AV_TYPE][AVT_CONV](split_line[p]) except: - if val[AV_VAR_OK] in AWS_YES and valid_var_name(split_line[p]): # a variable is OK here + if val[AV_VAR_OK] in AVVS_YES and valid_var_name(split_line[p]): # a variable is OK here return True else: return (error_msg(idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p], 'not a valid ' + val[AV_TYPE][AVT_DESC]), split_line[p]) From 80358987a7b3bcbc3e683780d25b44579ac672ff Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 22 Feb 2021 20:46:42 +0800 Subject: [PATCH 41/83] * Slight modification to command_list to tell us why a module can't be loaded * environment.yml and environment-build.yml updates for additional dependencies * requirements.txt modified (but I'm not sure it's correct) --- INSTALL/environment-build.yml | 4 ++++ INSTALL/environment.yml | 4 ++++ INSTALL/requirements.txt | 4 ++++ command_list.py | 5 ++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index e7769a0..dcf8656 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -14,3 +14,7 @@ dependencies: - https://github.com/pyinstaller/pyinstaller/archive/develop.zip - py-getch - pyautogui + - pywin32 + - pytesseract + - imagehash + - dhash diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index da930e0..76817ef 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -13,3 +13,7 @@ dependencies: - tkcolorpicker - py-getch - pyautogui + - pywin32 + - pytesseract + - imagehash + - dhash diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index 4abc3f1..a91d50d 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -14,4 +14,8 @@ python3-xlib==0.15 PyTweening==1.0.3 six==1.14.0 tkcolorpicker==2.1.3 +pywin32==227 +pytesseract==0.3.7 +ImageHash==4.2.0 +dhash==1.3 -e git+git://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py diff --git a/command_list.py b/command_list.py index b8aca8b..6494218 100644 --- a/command_list.py +++ b/command_list.py @@ -2,7 +2,7 @@ IMPORT_FATAL = False -import sys +import sys, traceback from constants import * import \ @@ -18,6 +18,7 @@ import commands_rpncalc except ImportError: print("[LPHK] WARNING: RPN_EVAL command is not available") + traceback.print_exc() # This library could be considered optional if PLATFORM == "windows": @@ -25,6 +26,7 @@ import commands_win32 except ImportError: print("[LPHK] ERROR: Windows specific commands are not available") + traceback.print_exc() else: print("[LPHK] WARNING: Windows specific commands can not be loaded") @@ -34,6 +36,7 @@ import commands_scrape except ImportError: print("[LPHK] ERROR: Screen scraping commands are not available") + traceback.print_exc() else: print("[LPHK] WARNING: Screen scraping commands can not be loaded") From 51fe6bfc132b690aeb89afc7322ed308ad2138aa Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:55:14 +0800 Subject: [PATCH 42/83] * A little more internal documentation for kb.py, lp_colors.py, and lp_events.py --- kb.py | 48 +++++++++++++++++++++++++++--------------------- lp_colors.py | 42 +++++++++++++++++++++++------------------- lp_events.py | 38 ++++++++++++++++++++++---------------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/kb.py b/kb.py index c35d819..bf32d8c 100644 --- a/kb.py +++ b/kb.py @@ -9,44 +9,50 @@ pressed = set() +# convert key name to format ready for use def sp(name): if "mouse_" in name: - return name + return name # mouse keys are strings - return keyboard_api.sp(name) + return keyboard_api.sp(name) # real keys are keycodes of some sort +# to press a key def press(key): - pressed.add(key) - if type(key) == str: - if "mouse_" in key: - ms.press(key[6:]) + pressed.add(key) # add to the list of pressed keys + if type(key) == str: # if the key desciption is a string + if "mouse_" in key: # assume (but check that it's a mouse key) + ms.press(key[6:]) # then strip "mouse_" and press it return - keyboard_api.press(key) + keyboard_api.press(key) # otherwise press the key by keycode +# to release a key def release(key): - pressed.discard(key) - if type(key) == str: - if "mouse_" in key: - ms.release(key[6:]) + pressed.discard(key) # remove it from the list of pressed keys + if type(key) == str: # if the key desciption is a string + if "mouse_" in key: # assume (but check that it's a mouse key) + ms.release(key[6:]) # then strip "mouse_" and release it return - keyboard_api.release(key) + keyboard_api.release(key) # otherwise release the key by keycode +# release all keys resorded as pressed def release_all(): - for key in pressed.copy(): - release(key) - + for key in pressed.copy(): # for each key recorded as pressed (in no particular order) + release(key) # release the key + +# tap a key def tap(key): - if type(key) == str: - if "mouse_" in key: - ms.click(key[6:]) + if type(key) == str: # if it's a str + if "mouse_" in key: # assume (but check that it's a mouse key) + ms.click(key[6:]) # "click" the mouse button return - press(key) - release(key) + press(key) # otherwise press... + release(key) # ...and immendiately release the key +# pretend we're typing def write(string): - keyboard_api.write(string) + keyboard_api.write(string) # Emulates typing the contents of the string diff --git a/lp_colors.py b/lp_colors.py index 2242f50..9775ca9 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -4,11 +4,11 @@ import lp_events, scripts, window import colorsys -lp_object = None +lp_object = None # another suspiciously non-threadsafe way of doing things :-( def init(lp_object_in): global lp_object - lp_object = lp_object_in + lp_object = lp_object_in # this function just stores the object in a global variable (what?!) def code_to_RGB(code): # Used to convert old layouts to the new format only @@ -61,9 +61,11 @@ def RGB_to_RG(rgb): else: return rgb +# Set the colour of a button (but not the current colour of the button!) def setXY(x, y, color): curr_colors[x][y] = color +# Get the currently recorded colour of the button (not necessarily it's current colour!) def getXY(x, y): return curr_colors[x][y] @@ -84,37 +86,38 @@ def getXY_RGB(x, y): def luminance(r, g, b): return ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255.0 +# update the colour of a button def updateXY(x, y): - if window.lp_connected: - btn = scripts.buttons[x][y] - if (x, y) != (8, 0): - is_running = False + if window.lp_connected: # must be connected to a launchpad + btn = scripts.buttons[x][y] # get the button + if (x, y) != (8, 0): # this is the "missing" button in the top right corner + is_running = False # assume it's not running if btn.thread != None: if btn.thread.isAlive(): - is_running = True + is_running = True # unless it is - is_func_key = ((y == 0) or (x == 8)) + is_func_key = ((y == 0) or (x == 8)) # top row or right column #print("Update colors for (" + str(x) + ", " + str(y) + "), is_running = " + str(is_running)) - if is_running: - set_color = scripts.COLOR_PRIMED - color_modes[x][y] = "flash" - elif (x, y) in [l[1:] for l in scripts.to_run]: + if is_running: # if the button is running + set_color = scripts.COLOR_PRIMED # set the desired colour + color_modes[x][y] = "flash" # and mode + elif (x, y) in [l[1:] for l in scripts.to_run]: # is it waiting to run? if is_func_key: - set_color = scripts.COLOR_FUNC_KEYS_PRIMED + set_color = scripts.COLOR_FUNC_KEYS_PRIMED # function keys have one colour else: - set_color = scripts.COLOR_PRIMED + set_color = scripts.COLOR_PRIMED # and other keys have another color_modes[x][y] = "pulse" else: - set_color = curr_colors[x][y] - color_modes[x][y] = "solid" + set_color = curr_colors[x][y] # this button is not running (or not even asigned) + color_modes[x][y] = "solid" # set the mode alone - if window.lp_mode == "Mk1": + if window.lp_mode == "Mk1": # how to actually set the colours of Mk:1 launchpads if type(set_color) is int: set_color = code_to_RGB(set_color) lp_object.LedCtrlXY(x, y, set_color[0]//64, set_color[1]//64) - else: + else: # setting colours for all other launchpads if (color_modes[x][y] == "solid") or is_func_key: #pulse and flash only work on main grid if type(set_color) is list: @@ -134,11 +137,12 @@ def updateXY(x, y): else: print("[lp_colors] (" + str(x) + ", " + str(y) + ") Launchpad is disconnected, cannot update.") +# update the colours of all buttons def update_all(): if window.lp_connected: for x in range(9): for y in range(9): - updateXY(x, y) + updateXY(x, y) # call this for each buttn else: print("[lp_colors] Launchpad is disconnected, cannot update.") diff --git a/lp_events.py b/lp_events.py index 291aa76..6305d33 100644 --- a/lp_events.py +++ b/lp_events.py @@ -10,36 +10,42 @@ def unbound_press(x, y): press_funcs = [[unbound_press for y in range(9)] for x in range(9)] pressed = [[False for y in range(9)] for x in range(9)] -timer = None +timer = None # This is a global variable that is continually overwritten with the current timer event. + # I'm not happy that this is the safest way of doing this, but so far it seems to have + # worked. The fact that we're using threads rather than different processes probably + # allows us to get away with what is strictly not thread-safe logic. +# initialise an object by creating a timer object for it (but not starting it) def init(lp_object): global timer global press_funcs timer = threading.Timer(RUN_DELAY, run, (lp_object,)) +# "run" an object (a button) after a delay def run(lp_object): global timer - while True: - event = lp_object.ButtonStateXY() - if event != []: - x = event[0] + while True: # loop forever + event = lp_object.ButtonStateXY() # get a pending event + if event != []: # if there is an event + x = event[0] # determine the button it is y = event[1] - if event[2] == 0: + if event[2] == 0: # is this button released? pressed[x][y] = False - else: + else: # I presume this is "button pressed" pressed[x][y] = True - press_funcs[x][y](x, y) - lp_colors.updateXY(x, y) - else: - break - init(lp_object) + press_funcs[x][y](x, y) # do whatever you need to do with a pressed button + lp_colors.updateXY(x, y) # and update the button colour + else: # but if there's no event pending + break # break out of the loop + init(lp_object) # and schedule this button to run after the delay timer.start() +# "start" an object by initialising it then running it. def start(lp_object): - lp_colors.init(lp_object) - init(lp_object) - run(lp_object) - lp_colors.update_all() + lp_colors.init(lp_object) # assign this to a global (what?!) + init(lp_object) # create the timer object for this lp_object (button) + run(lp_object) # "run" the object (why not just start the timer?) + lp_colors.update_all() # update all the colours after exiting def bind_func_with_colors(x, y, func, off_color): global press_funcs From 3bac57464f011d7d6c0d9ae89dd0e875c2f3ef96 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 24 Feb 2021 16:37:18 +0800 Subject: [PATCH 43/83] * lots of internal documentation improved * "umpressed" in a message changed to "not pressed" * fix to strip_quoted() to make quoted quotes work so """" == '"' * flagged areas in the code that may need to be examined (marked with @@@) --- LPHK.py | 26 ++++---- command_list.py | 13 ++-- commands_control.py | 40 ++++++------ commands_external.py | 12 ++-- commands_header.py | 1 + commands_keys.py | 22 +++---- commands_win32.py | 26 ++++---- scripts.py | 147 ++++++++++++++++++++++--------------------- 8 files changed, 144 insertions(+), 143 deletions(-) diff --git a/LPHK.py b/LPHK.py index ab421f5..26b14bb 100755 --- a/LPHK.py +++ b/LPHK.py @@ -85,20 +85,20 @@ def datetime_str(): def init(): global EXIT_ON_WINDOW_CLOSE - - ap = argparse.ArgumentParser() - ap.add_argument( + + ap = argparse.ArgumentParser() # argparse makes argument processing easy + ap.add_argument( # reimnplementation of debug (-d or --debug) "-d", "--debug", help = "turn on debugging mode", action="store_true") - ap.add_argument( + ap.add_argument( # new option to automatically load a layout "-l", "--layout", help = "load a layout", type=argparse.FileType('r')) - ap.add_argument( + ap.add_argument( # new option to start minimised "-m", "--minimised", help = "Start the application minimised", action="store_true") - window.ARGS = vars(ap.parse_args()) + window.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to if window.ARGS['debug']: EXIT_ON_WINDOW_CLOSE = False @@ -109,19 +109,19 @@ def init(): sound.init(USER_PATH) def shutdown(): - if lp_events.timer != None: + if lp_events.timer != None: # cancel any outstanding events lp_events.timer.cancel() - scripts.to_run = [] + scripts.to_run = [] # remove anything from the list of scripts scheduled to run for x in range(9): for y in range(9): if scripts.buttons[x][y].thread != None: - scripts.buttons[x][y].thread.kill.set() + scripts.buttons[x][y].thread.kill.set() # request to kill any running threads if window.lp_connected: - scripts.Unbind_all() - lp_events.timer.cancel() - launchpad_connector.disconnect(lp) + scripts.Unbind_all() # unbind all the buttons + lp_events.timer.cancel() # cancel all the timers + launchpad_connector.disconnect(lp) # disconnect from the launchpad window.lp_connected = False - logger.stop() + logger.stop() # stop logging if window.restart: if IS_EXE: os.startfile(sys.argv[0]) diff --git a/command_list.py b/command_list.py index 6494218..94eeb50 100644 --- a/command_list.py +++ b/command_list.py @@ -5,6 +5,7 @@ import sys, traceback from constants import * +# any mandatory command-related stuff should be listed here import \ commands_header, \ commands_control, \ @@ -13,14 +14,14 @@ commands_pause, \ commands_external -# This library could be considered optional +# This library could be considered optional, but is not platform specific try: import commands_rpncalc except ImportError: print("[LPHK] WARNING: RPN_EVAL command is not available") traceback.print_exc() -# This library could be considered optional +# This library could be considered optional, and is also platform specific if PLATFORM == "windows": try: import commands_win32 @@ -30,17 +31,17 @@ else: print("[LPHK] WARNING: Windows specific commands can not be loaded") -# This library could be considered optional +# This library could be considered optional, and is also platform specific if PLATFORM == "windows": try: import commands_scrape except ImportError: - print("[LPHK] ERROR: Screen scraping commands are not available") + print("[LPHK] ERROR: Screen scraping commands are not available") traceback.print_exc() else: - print("[LPHK] WARNING: Screen scraping commands can not be loaded") + print("[LPHK] WARNING: Screen scraping commands can not be loaded") # Any that were not optional should set the error flag so we can exit -if IMPORT_FATAL: +if IMPORT_FATAL: # Not using this at present sys.exit("[LPHK] ERROR: Required command modules are absent") diff --git a/commands_control.py b/commands_control.py index dde425c..8ca55b0 100644 --- a/commands_control.py +++ b/commands_control.py @@ -89,34 +89,34 @@ def __init__( def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): - ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation - if ret == None or ((type(ret) == bool) and ret): - if self.loop_val_init_function: - self.loop_val_init_function(btn, idx, split_line) - ret = True + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if self.loop_val_init_function: # and if we have a validation + self.loop_val_init_function(btn, idx, split_line) # then perform the additional validation + ret = True # not sure why we always return true @@@ return ret def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): - ret = super().Partial_validate_step_pass_2(ret, btn, idx, split_line) + ret = super().Partial_validate_step_pass_2(ret, btn, idx, split_line) # perform the original pass 2 validation - if (ret == None or ((type(ret) == bool) and ret)): - if self.label_preceeds and btn.symbols[SYM_LABELS][split_line[1]] > idx: + if (ret == None or ((type(ret) == bool) and ret)): # if the original validation hasn't raised an error + if self.label_preceeds and btn.symbols[SYM_LABELS][split_line[1]] > idx: # If the label must preceed the command, ensure that it is so! ret = ("Line:" + str(idx+1) + " - Target for " + self.name + " (" + split_line[1] + ") must preceed the command.", btn.Line(idx)) return ret def Partial_run_step_info(self, ret, btn, idx, split_line): - ret = super().Partial_run_step_info(ret, btn, idx, split_line) + ret = super().Partial_run_step_info(ret, btn, idx, split_line) # perform the original notification of a partial execution if self.valid_function == None or self.valid_function(btn): # if no validation function, or it returns true, continue - if self.test_function and self.next_function: - if btn.symbols[SYM_REPEATS][idx] > 0: + if self.test_function and self.next_function: # if there is a test function and it returns true + if btn.symbols[SYM_REPEATS][idx] > 0: # if repeats remain print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " " + str(btn.symbols[SYM_REPEATS][idx]) + " repeats left.") - else: + else: # if no repeats remain print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " No repeats left, not repeating.") else: print(self.invalid_message) @@ -146,31 +146,31 @@ def Process(self, btn, idx, split_line): def Valid_key_pressed(self, btn): - return lp_events.pressed[btn.x][btn.y] + return lp_events.pressed[btn.x][btn.y] # Is the button pressed def Valid_key_unpressed(self, btn): - return not self.Valid_key_pressed(btn) + return not self.Valid_key_pressed(btn) # is the button unpressed - def Test_func_ge_zero(self, val): + def Test_func_ge_zero(self, val): # testing for a value >= 0 return val >= 0 - def Next_decrement(self, val): + def Next_decrement(self, val): # Standard decrement function return val-1 def Reset(self, btn, idx): - btn.symbols[SYM_REPEATS][idx] = btn.symbols[SYM_ORIGINAL][idx] + btn.symbols[SYM_REPEATS][idx] = btn.symbols[SYM_ORIGINAL][idx] # standard function to reset a loop counter def Init_n(self, btn, idx, split_line): - btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2]) + btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2]) # set repeats to n (will cause n+1 loop executions) self.Reset(btn, idx) - def Init_n_minus_1(self, btn, idx, split_line): + def Init_n_minus_1(self, btn, idx, split_line): # set repeats to n-1 (will cause n loop executions) btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 self.Reset(btn, idx) @@ -353,7 +353,7 @@ def __init__( # num params, format string (trailing comma is important) (2, " If key is pressed repeat label {1}, {2} times max"), ), - "the button is umpressed", + "the button is not pressed", self.Valid_key_pressed, False, False, diff --git a/commands_external.py b/commands_external.py index deb7fc5..abe43a7 100644 --- a/commands_external.py +++ b/commands_external.py @@ -7,7 +7,7 @@ # ### CLASS External_Web ### # ################################################## -# class that defines the WEB command +# class that defines the WEB command. @@@ this should be updated to use the more modern interface class External_Web(command_base.Command_Text_Basic): def __init__( self, @@ -44,7 +44,7 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Web_New ### # ################################################## -# class that defines the WEB_NEW command +# class that defines the WEB_NEW command. @@@ this should be updated to use the more modern interface class External_Web_New(External_Web): def __init__( self, @@ -67,7 +67,7 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Open ### # ################################################## -# class that defines the OPEN command +# class that defines the OPEN command. @@@ this should be updated to use the more modern interface class External_Open(command_base.Command_Text_Basic): def __init__( self, @@ -93,7 +93,7 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Sound ### # ################################################## -# class that defines the SOUND command (plays a sound file) +# class that defines the SOUND command (plays a sound file). @@@ this should be updated to use the more modern interface class External_Sound(command_base.Command_Basic): def __init__( self, @@ -163,7 +163,7 @@ def __init__( def Process(self, btn, idx, split_line): - delay = btn.symbols[SYM_PARAMS][1] + delay = btn.symbols[SYM_PARAMS][1] # @@@ update this if delay == None or delay <= 0: sound.stop() @@ -178,7 +178,7 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Code ### # ################################################## -# class that defines the CODE command (runs something) +# class that defines the CODE command (runs something). @@@ this should be updated to use the more modern interface class External_Code(command_base.Command_Basic): def __init__( self, diff --git a/commands_header.py b/commands_header.py index cf9e2e5..eafb295 100644 --- a/commands_header.py +++ b/commands_header.py @@ -117,6 +117,7 @@ def Run( # ### CLASS Header_Load_Layout ### # ################################################## +# Loads a new layout. @@@ This should probably be rewritten in the newest style class Header_Load_Layout(command_base.Command_Header): def __init__( self, diff --git a/commands_keys.py b/commands_keys.py index 6c7d531..ad1e7f2 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -64,24 +64,24 @@ def __init__( def Process(self, btn, idx, split_line): - cnt = self.Param_count(btn) - key = kb.sp(self.Get_param(btn, 1)) - releasefunc = lambda: None + cnt = self.Param_count(btn) # how many parameters? + key = kb.sp(self.Get_param(btn, 1)) # what key? + releasefunc = lambda: None # default is no release function - taps = 1 - if cnt >= 2: + taps = 1 # Assume 1 tap unless we are told there's more + if cnt >= 2: # @@@ this section can be simplified to taps = self.Get_param(btn, 2, 1) taps = self.Get_param(btn, 2) - delay = 0 + delay = 0 # assume no delay unless we're told there is one if cnt == 3: delay = self.Get_param(btn, 3) - releasefunc = lambda: kb.release(key) + releasefunc = lambda: kb.release(key) # and in this case we'll also need to set up a lambda to release it - precheck = delay == 0 and taps > 1 + precheck = delay == 0 and taps > 1 # we need to check if there's no delay and (possibly many) taps - for tap in range(taps): - if btn.Check_kill(releasefunc): - return idx+1 + for tap in range(taps): # for each tap + if btn.Check_kill(releasefunc): # see if we've been killed + return idx+1 # @@@ shouldn't this be -1? if delay == 0: kb.tap(key) diff --git a/commands_win32.py b/commands_win32.py index 1994bb5..87f95b5 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -127,8 +127,7 @@ def __init__( def Process(self, btn, idx, split_line): hwnd = win32gui.GetForegroundWindow() # get the current window - #variables.Auto_store(btn.symbols[SYM_PARAMS][1], hwnd, btn.symbols) # Return the current window - self.Set_param(btn, 1, hwnd) + self.Set_param(btn, 1, hwnd) # Return the current window scripts.Add_command(Win32_Get_Fg_Hwnd()) # register the command @@ -204,9 +203,9 @@ def Process(self, btn, idx, split_line): hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window state = self.restore_window(hwnd) try: - x, y = win32gui.ClientToScreen(hwnd, (x, y)) # convert client coords to screen coords + x, y = win32gui.ClientToScreen(hwnd, (x, y)) # convert client coords to screen coords - self.Set_param(btn, 1, x) # set new x, y values + self.Set_param(btn, 1, x) # set new x, y values self.Set_param(btn, 2, y) finally: self.reset_window(state) @@ -247,9 +246,9 @@ def Process(self, btn, idx, split_line): hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window state = self.restore_window(hwnd) try: - x, y = win32gui.ScreenToClient(hwnd, (x, y)) # convert client coords to screen coords + x, y = win32gui.ScreenToClient(hwnd, (x, y)) # convert client coords to screen coords - self.Set_param(btn, 1, x) # set new x, y values + self.Set_param(btn, 1, x) # set new x, y values self.Set_param(btn, 2, y) finally: self.reset_window(state) @@ -350,7 +349,6 @@ def Process(self, btn, idx, split_line): kb.tap(kb.sp('c')) finally: kb.release(kb.sp('ctrl')) - #win32api.SendMessage(hwnd, win32con.WM_COPYDATA, 0, 0) # do a copy if self.Param_count(btn) > 0: # save to variable if required try: @@ -437,11 +435,11 @@ def Process(self, btn, idx, split_line): tid, pid = win32process.GetWindowThreadProcessId(hwnd) # find the pid hproc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION , False, pid) # find the process id - res = win32con.WAIT_TIMEOUT - while res == win32con.WAIT_TIMEOUT: - res = win32event.WaitForInputIdle(hproc, 20) - if btn.Check_kill(): - return False + res = win32con.WAIT_TIMEOUT # set the failure mode to timeout + while res == win32con.WAIT_TIMEOUT: # while we're still timing out + res = win32event.WaitForInputIdle(hproc, 20) # wait a little while for window to become idle + if btn.Check_kill(): # check if we've been killed + return False # and die scripts.Add_command(Win32_Wait()) # register the command @@ -474,9 +472,9 @@ def Process(self, btn, idx, split_line): pid = self.Get_param(btn, 1) # get the pid hwnds = self.get_hwnds_for_pid(pid) # find any hwnds if len(hwnds) == 1: - self.Set_param(btn, 2, hwnds[0]) + self.Set_param(btn, 2, hwnds[0]) # return a value if we have a unique window id else: - self.Set_param(btn, 2, hwnds) + self.Set_param(btn, 2, hwnds) # @@@ not sure it's a good idea to return a list if > 1 id scripts.Add_command(Win32_Pid_To_Hwnd()) # register the command diff --git a/scripts.py b/scripts.py index d33c934..9bad0b2 100644 --- a/scripts.py +++ b/scripts.py @@ -284,82 +284,83 @@ def Split_text(self, command, cmd_txt, line): # just split the command from the rest of the text return [cmd_txt, line[len(cmd_txt)+1:]] else: - def split1(line): - param = line.split()[0] + def split1(line): # just strip off a single (non-quoted) parameter + param = line.split()[0] # get the parameter line = line[len(param):].strip() # strip off the parameter - return param, line + return param, line # return the parameter and the rest of the line + # grab a quoted string from the line passed. Does not handle embedded quotes @@@ but it should def strip_quoted(line): - l2 = line - q = l2[0] - out = '' - l2 = l2[1:] - while len(l2) > 0: - if l2[0] == q: - if len(l2) == 1 or l2[1] == ' ': - l2 = l2[1:].strip() - return True, out, l2 - elif line[1] == q: - out += q - l2 = l2[2:] + l2 = line # a copy of the line we can edit + q = l2[0] # the first character is assumed to be a quote + out = '' # nothing to output yet + l2 = l2[1:] # strip the quote from the string + while len(l2) > 0: # while something remains in the line + if l2[0] == q: # if the quote is repeated + if len(l2) == 1 or l2[1] == ' ': # and if it's the last character or followed by a space + l2 = l2[1:].strip() # clean up the rest of the string + return True, out, l2 # return success + elif len(l2) > 1 and l2[1] == q: # if we have 2 quotes in a row + out += q # then this is literally a quote + l2 = l2[2:] # but we need to clean up 2 characters this time else: - return False, out, line - else: - out += l2[0] - l2 = l2[1:] + return False, out, line # any other quote-related stuff must be an error + else: # for non-quote characters + out += l2[0] # we just pass them through to the output string + l2 = l2[1:] # and strip them off. - return False, out, line + return False, out, line # if we fall through, that's an error (no closing quote) # for all other commands, split on spaces if isinstance(command, command_base.Command_Basic): - pline = line # something we can alter + pline = line # something we can alter avl = command.auto_validate if avl != None and len(avl) > 0: - cmd, pline = split1(pline) # the command is always a simple split - sline = [cmd] # add it to the return variable - - n = -1 - while len(pline) > 0: - n += 1 - if n < len(avl): - av = avl[n] - else: - av = avl[-1] + cmd, pline = split1(pline) # the command is always a simple split + sline = [cmd] # add it to the return variable + + n = -1 # initialise parameter number pointer to one before the first + while len(pline) > 0: # keep stripping parameters while the line has some content + n += 1 # point to the next parameters + if n < len(avl): # if this parameter has an auto-validation + av = avl[n] # then grab it + else: # otherwise the last parameter must allow for multiple values + av = avl[-1] # so take the last auto-validation - desc = av[AV_TYPE][AVT_DESC] + desc = av[AV_TYPE][AVT_DESC] # get the description of the parameter type (not the description of the parameter!) - if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Just the next parameter, unless it starts with a quote ['"`] - if pline[0] in ['"', "'", '`']: - if av[AV_VAR_OK] == AVV_REQD: + if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Is this one that wants quoted strings? + if pline[0] in ['"', "'", '`']: # if so, does it start with an acceptable quote? + if av[AV_VAR_OK] == AVV_REQD: # it's a problem if a variable is required return ('Error, quoted string not permitted for param #' + str(n+1), line) # literal not expected else: - ok, param, pline = strip_quoted(pline) - if ok: - sline += ['"'+param] + ok, param, pline = strip_quoted(pline) # otherwise we can strip off a quoted string + if ok: # and if that suceeded + sline += ['"'+param] # we'll add it as the parameter value. Note we add a leading " to distinguish it from a variable else: return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote - else: - if av[AV_VAR_OK] != AVV_NO: - param = pline.split()[0] - pline = pline[len(param):].strip() - if not variables.valid_var_name(param): + else: # if we want a quoted string, but value doesn't start with a quote + if av[AV_VAR_OK] != AVV_NO: # Are we allowed to pass a variable? + param = pline.split()[0] # then that's OK, just strip off an un-quoted string + pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) + if not variables.valid_var_name(param): # but check it's a valid variable name return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... else: - sline += [param] + sline += [param] # add it to the list of parameters if it's OK else: return ('Error starting quoted string for param#' + str(n+1), line) # This is generally a missing initial quote elif desc == PT_LINE[AVT_DESC]: # the rest of the line (regardless of spaces) - sline += [line] - pline = "" + sline += [line] # just grab the rest of the line + pline = "" # and leave nothing behind - else: - param = pline.split(" ")[0] + else: # in all other cases + param = pline.split(" ")[0] # just strip the first unquoted parameter (@@@ why not use strip1()???) sline += [param] pline = pline[len(param):].strip() - return sline + return sline # return a list of command and parameters else: # without autovalidate we just split on spaces @@ -385,48 +386,48 @@ def Run_script(self): if len(self.script_lines) > 0: self.running = True - def Main_logic(idx): - if self.Check_kill(): - return idx + 1 + def Main_logic(idx): # the main logic to run a line of a script + if self.Check_kill(): # first check to see if we've been asked to die + return idx + 1 # we just return the next line, @@@ returning -1 is better - line = self.Line(idx) + line = self.Line(idx) # get the line of the script # Handle completely blank lines if line == "": return idx + 1 # Get the command text - cmd_txt = self.Split_cmd_text(line) + cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact # Now get the command object - if cmd_txt in VALID_COMMANDS: - command = VALID_COMMANDS[cmd_txt] + if cmd_txt in VALID_COMMANDS: # make sure it's a valid command + command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command - split_line = self.Split_text(command, cmd_txt, line) + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters - if type(split_line) == tuple: + if type(split_line) == tuple: # bad news if we get a tuple rather than a list print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") else: # now run the command - return command.Run(self, idx, split_line) + return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out else: print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") - return idx + 1 + return idx + 1 # defaut action is to ask for the next line - run = True - idx = 0 - while run: - idx = Main_logic(idx) - if (idx < 0) or (idx >= len(self.script_lines)): - run = False + run = True # flag that we're running + idx = 0 # point at the first line + while run: # and while we're still running + idx = Main_logic(idx) # run the current line + if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid + run = False # then we're not going to keep running! - if not self.is_async: - self.running = False + if not self.is_async: # async commands don't just end + self.running = False # they have to say they're not running - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours - print("[scripts] " + self.coords + " Script done running.") + print("[scripts] " + self.coords + " Script done running.") # and print (log?) that the script is complete # validating a script consists of doing the checks that we do prior to running, but @@ -448,7 +449,7 @@ def Validate_script(self): return self.validated # and tell us the result - +# define the buttons structure here. Note that subroutines will likely be a different sort of button, so this may change buttons = [[Button(x, y, "") for y in range(9)] for x in range(9)] to_run = [] @@ -465,9 +466,9 @@ def Bind(x, y, script_str, color): indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button for index in indexes[::-1]: # and for each of them (in reverse order) temp = to_run.pop(index) # Remove them from the list - return # Why do we return here? + return # @@@ Why do we return here? - schedule_script_bindable = lambda a, b: btn.Schedule_script() + schedule_script_bindable = lambda a, b: btn.Schedule_script() # @@@ What is this doing? lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) files.layout_changed_since_load = True # Mark the layout as changed From 1cb24dec095d39ed29d36791d77b77d29f20b7be Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 5 Mar 2021 09:08:56 +0800 Subject: [PATCH 44/83] Merge with "develop" branch of main repo (thanks duncte123) --- LICENSE | 674 +++++++++++++++++++++++++++++++ user_scripts/examples/test_1.lps | 15 + 2 files changed, 689 insertions(+) create mode 100644 LICENSE create mode 100644 user_scripts/examples/test_1.lps diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/user_scripts/examples/test_1.lps b/user_scripts/examples/test_1.lps new file mode 100644 index 0000000..499da0b --- /dev/null +++ b/user_scripts/examples/test_1.lps @@ -0,0 +1,15 @@ +- A test script to test as much as I can +STRING Test 1 +TAP enter +GOTO_LABEL skip +STRING Fail in GOTO command +LABEL skip +- in the loop +REPEAT skip 3 +WEB_NEW http://www.google.com +WEB http:/bing.com +M_STORE +M_MOVE 99999999 99999999 +TAP mouse_left +M_RECALL +SOUND examples/airhorn.wav From 66392dd2559e675d0e712296075cc1c24df0b5fe Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 5 Mar 2021 09:24:12 +0800 Subject: [PATCH 45/83] * Accepted recommended change by duncte123 --- command_list.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/command_list.py b/command_list.py index 94eeb50..d5d51d0 100644 --- a/command_list.py +++ b/command_list.py @@ -28,18 +28,14 @@ except ImportError: print("[LPHK] ERROR: Windows specific commands are not available") traceback.print_exc() -else: - print("[LPHK] WARNING: Windows specific commands can not be loaded") -# This library could be considered optional, and is also platform specific -if PLATFORM == "windows": try: import commands_scrape except ImportError: print("[LPHK] ERROR: Screen scraping commands are not available") traceback.print_exc() else: - print("[LPHK] WARNING: Screen scraping commands can not be loaded") + print("[LPHK] WARNING: Windows specific and screen scraping commands cannot be loaded") # Any that were not optional should set the error flag so we can exit if IMPORT_FATAL: # Not using this at present From b89aaf8a749de3e428331cf70ac156e38d90012b Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 5 Mar 2021 10:42:21 +0800 Subject: [PATCH 46/83] ALPHA version of subroutine code. Still has many problems (real and probably hidden). I need to check things like re-entrancy of subroutines, the ability to nest subroutines, the correct handling of aborting a thread, etc. The code is currently a little weak on checking that all subroutines and buttons are still error free after a subroutine is removed or replaced. Also documentation is required. * Cosmetic changes to command_base.py and NewCommand.md. * command_list.py imports commands_subroutines to enable the subroutine commands. This is currently only necessary for the new @SUB header, but other subroutine-related commands could be added here. * commands_keys.py has a trivial change to use an easier version of "default parameter values" * constants.py defines the prefix for subroutines (so you can't accidentally override an internal command with a subroutine) * variables.py has a more thorough check on variable names (limiting them to alphanumeric and underscore) to permit other characters to be used for other things... Additionally some routines for storage and retreval of variables have been added. This code was originally only in commands_rpncalc.py but is now needed in the subroutine code too. the commands_rpncalc.py code will probably be updated in the future to reduce duplication of code. * commands_subroutines.py is the new module that does the easy part of subroutines * files.py now defines a directory, extension, and path for subroutines. * saving layouts has been modified to save subroutines. Note that the file version remains the same (0.1.1) if no subroutines are present, but changes to 0.2 if subroutines are saved. * loading layouts now needs to unload subroutines, and load any subroutines in that layout * new routines to load *all* the subroutines in a file, and to load an individual subroutine. Note that a line consisting of just === in a subroutine file delimits subroutines. * directory user_subroutines and some trivial example subroutines added (one with an error) * window.py modified to add a new "subroutines" menu item with further options to load and clear subroutines. Note that loading is additive to the routines already loaded. Some preliminary work has been done to facilitate saving, bit there is no implementation of that. * New subroutines menu is enabled and disabled along with Layout menu. * two surprisingly simple routines added to support loading and clearing subroutines. * significant changes to scripts.py to support subroutines. This also includes some bug fixes found along the way and some cosmetic changes. * Button now support a "root". This is the button that was executed. This button is the one scheduled and killed by the scheduler. By default a button is its own root (and that's fine for real buttons. "fake" buttons are created for subroutines, and these must point back to the root button to access death notifications. * coords for a button are replaced with the generic text "SUB", and then later by the subroutine name for subroutine buttons. This allows easier logging of what's going on. * a new value is_button is true for normal buttons, and false for subroutines * The initial parsing needs to work with both a string containing the entire script, or a list of lines (since that's how subroutines arrive). * a new method Copy_parsed allows the symbol (and other) information from the parsed subroutine in the command to be transferred to a button without requiring re-parsing. * Check_kill is now Check_self_kill, and a new Check_kill calls Check_self_kill for the root button. This means that Check_kill will work for all types of buttons. * bug fixed in Schedule_script for async scripts. This should not be called for subroutines and there is a note that this is something to check for. * split_text is now commented that it handles embedded quotes (bug fix didn't remove comment) * Run_script has some cosmetic modifications, including a comment that it should check that it's not running a subroutine. * new method Run_subroutine is an analog of Run_script for subroutines. The major difference is that it does not try to fiddle with button colours and also logs comments about subroutines rather than scripts. This might end up merged back with Run_script as it is mostly identical, and I don't want to maintain 2 sets of code to do essentially the same task. * new method kill_all() to kill all threads, because it's needed when keys are unbound or subroutines unloaded. * unbind_all modified to use kill_all() * new method Unload_all to remove all subroutines. Note that this marks the layout as changed! --- NewCommands.md | 2 +- command_base.py | 2 +- command_list.py | 3 +- commands_keys.py | 4 +- commands_subroutines.py | 254 +++++++++++++++++++++++++++++++ constants.py | 2 + files.py | 50 +++++- scripts.py | 151 +++++++++++++++--- user_subroutines/AddOne.lpc | 2 + user_subroutines/AddOneError.lpc | 1 + variables.py | 50 +++++- window.py | 37 ++++- 12 files changed, 520 insertions(+), 38 deletions(-) create mode 100644 commands_subroutines.py create mode 100644 user_subroutines/AddOne.lpc create mode 100644 user_subroutines/AddOneError.lpc diff --git a/NewCommands.md b/NewCommands.md index 845051c..3fa21f9 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -644,7 +644,7 @@ Every command requires a validation. If you do not provide validation code, the try: temp = int(split_line[1]) - if valid_var_name(temp): + if variables.valid_var_name(temp): if temp < 1: return ("Line:" + str(idx+1) + " - '" + split_line[0] + " parameter 1 must be a positive number.", btn.line[idx]) diff --git a/command_base.py b/command_base.py index a995e77..185daff 100644 --- a/command_base.py +++ b/command_base.py @@ -554,7 +554,7 @@ def Set_param(self, btn, n, val): av = self.auto_validate[n-1] if av[AV_VAR_OK] == AVV_REQD: variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable - + # ################################################## # ### CLASS Command_Text_Basic ### diff --git a/command_list.py b/command_list.py index d5d51d0..525cb65 100644 --- a/command_list.py +++ b/command_list.py @@ -12,7 +12,8 @@ commands_keys, \ commands_mouse, \ commands_pause, \ - commands_external + commands_external, \ + commands_subroutines # This library could be considered optional, but is not platform specific try: diff --git a/commands_keys.py b/commands_keys.py index ad1e7f2..1359c8a 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -68,9 +68,7 @@ def Process(self, btn, idx, split_line): key = kb.sp(self.Get_param(btn, 1)) # what key? releasefunc = lambda: None # default is no release function - taps = 1 # Assume 1 tap unless we are told there's more - if cnt >= 2: # @@@ this section can be simplified to taps = self.Get_param(btn, 2, 1) - taps = self.Get_param(btn, 2) + taps = self.Get_param(btn, 2, 1) # Assume 1 tap unless we are told there's more delay = 0 # assume no delay unless we're told there is one if cnt == 3: diff --git a/commands_subroutines.py b/commands_subroutines.py new file mode 100644 index 0000000..00ebb52 --- /dev/null +++ b/commands_subroutines.py @@ -0,0 +1,254 @@ +import command_base, commands_header, scripts, variables +from constants import * + +LIB = "cmds_subr" # name of this library (for logging) + +# Note that this command module does not define a set of commands as such, but implements a method +# of calling subroutines by enabling a script to be registrered as a regular command. +# As such, it needs to define a new header, and a class that will be instansiated once for each +# subroutine that is loaded. + +# ################################################## +# ### CLASS Header_Subroutine ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Subroutine(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@SUB", # the name of the header as you have to enter it in the code + False) # You also define if the header causes the script to be asynchronous + + + # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if btn.is_button == True: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is not permitted in a button.", btn.Line(idx)) + + return True + + + # Dummy run routine. Simply passes execution to the next line + def Run( + self, + btn, + idx: int, # The current line number + split_line # The current line, split + ): + + return idx+1 + + +scripts.Add_command(Header_Subroutine()) # register the header + + +# ################################################## +# ### CLASS Subroutine_Define ### +# ################################################## + +# class that defines the CALL:xxxx command (runs a subroutine). This parses the routine (pass 1 and 2 validation) +# and adds it as a command if the parsing suceeds. It can then be called just like any other command +class Subroutine(command_base.Command_Basic): + def __init__( + self, + Name, # The name of the command + Params, # The parameter tuple + Lines # The text of the subroutine/function + ): + + super().__init__(SUBROUTINE_PREFIX + Name, # the name of the command as you have to enter it in the code + LIB, + Params, + ( + # num params, format string (trailing comma is important) + (0, " Call "+ Name), + ) ) + + self.routine = Lines # the routine to execute + self.btn = scripts.Button(-1, -1, self.routine) # we retain this so we only have to validate it once. executions use a deep-ish copy + + + # process for a subroutine handles parameter passing and then passes off the process to the script in a "dummy" button + def Process(self, btn, idx, split_line): + sub_btn = scripts.Button(-1, -1, self.routine, btn.root) # create a new button and pass the script to it + + self.btn.Copy_parsed(sub_btn, self.name) # copy the info created when parsed + + variables.Local_store('sub__np', self.Param_count(btn), sub_btn.symbols) # number of parameters passed + + d = variables.Local_recall('sub__d',btn.symbols) # get current call depth + variables.Local_store('sub__d', d+1, sub_btn.symbols) # and pass that + 1 + + #@@@ will fail with multiple parameters at the end! + #Which is not so much of a problem because there is no way to name or retrieve them currently + + for n in range(self.Param_count(btn)): # for all the params passed + pn = self.Get_param(btn, n+1) # get the param + variables.Local_store(self.auto_validate[n][AV_DESCRIPTION], pn, sub_btn.symbols) # and store it + + sub_btn.Run_subroutine() + + for n in range(self.Param_count(btn)): # for all the params passed + if self.auto_validate[n][AV_VAR_OK] == AVV_REQD: # if this is passed by reference + pn = variables.Local_recall(self.auto_validate[n][AV_DESCRIPTION], sub_btn.symbols) # get the variable + self.Set_param(btn, n+1, pn) # and store it + + + # This is not the parse routine called for validation! + def Parse_Sub(self): + try: + script_validate = self.btn.Parse_script() + except: + self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") + raise + + +MOD_TRANS = str.maketrans('-%#$!@', 'OIFSBR') # standardise the modifiers +MOD_CONSOLIDATE = str.maketrans('MOIFSBRV', 'MMIIIIVV') # consoidate the modifiers +VALID_CONSOLIDATED = {"M", "I", "V"} # set of all valid consolidate modifiers + + +def Get_Name_And_Params(lines, sub_n, fname): + # Have we found a line with @SUB? + found = False + + for lin_num, line in enumerate(lines): + line = line.strip() + if line == '' or line[0] == '-': + pass # ignore blank lines and comments + elif line.split()[0] != '@SUB': + return '', f'Error - Subroutine does not start with an @SUB header on line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num + else: + found = True + break + + # Not finding the header is a bad problem + if not found: + return '', f'Error - Subroutine has no content up to line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num + + # do we have a name? + sline = line.split() + if len(sline) < 2: + return '', f'Error - Subroutine does not have a name on line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num + + # this is the name + name = sline[1] + if name != name.upper(): + return '', f'Error - Subroutine name is not UPPERCASE on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + + # now work on the parameters + params = () + + # for each parameter + for p_num, param in enumerate(sline[2:]): + mods = '' # No modifiers yet + var = param # so everything else is the variable name + + # first find leading modifiers + for c in param: + if not variables.valid_var_name('A'+c): + mods += c + var = var[1:] + else: + break + + # next look for anything after a "+" + p = var.find('+') + if p >= 0: + mods += var[p+1:] + var = var[:p] + elif not variables.valid_var_name(var): # if there's no '+', look for trailing modifiers without one + l = len(var) + while l > 1: + if variables.valid_var_name(var[:l-1]): + mods += var[l:] + var = var[:l-1] + break + l -= 1 + + if var == '' or not variables.valid_var_name(var): + return name, f'Error - Parameter "{var}" is not a valid name on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + + # Standardise modifiers + mods = mods.upper().translate(MOD_TRANS) + + # Consolidate the modifiers + modc = mods.translate(MOD_CONSOLIDATE) + modcs = set(modc) + + if modcs > VALID_CONSOLIDATED: + e = modcs - VALID_CONSOLIDATED + return name, f'Error - Invalid modifier {modcs - VALID_CONSOLIDATED} specified on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + + # check for duplicate modifiers + if len(modc) != len(set(modc)): + return name, f'Error - Duplicate modifiers specified on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + + # Desc Opt Var type p1_val p2_val + prm = [var, False, AVV_YES, PT_INT, None, None] + + if mods.find('O') >= 0: + prm[1] = True # parameter is optional + + if mods.find('R') >= 0: + prm[2] = AVV_REQD # pass by reference is a required variable + + if mods.find('F') >= 0: + prm[3] = PT_FLOAT # parameter is a float + elif mods.find('S') >= 0: + prm[3] = PT_STR # parameter is a string + elif mods.find('K') >= 0: + prm[3] = PT_KEY # parameter is a key + prm[2] = AVV_NO # must be a constant + if mods.find('R') >= 0: + return name, f'Error - Key cannot be passed by reference on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + elif mods.find('B') >= 0: + prm[3] = PT_BOOL # parameter is a boolean + + params += (tuple(prm),) # add a new parameter + + return name, params, lin_num+1 # return the subroutine name, valid list of parameters, and the next line number + + +def Add_Function(lines, sub_n, fname): + # This function is passed a list of lines. The first non-comment line must define the header + + # first let's parse out the header to get the name and the parameters + name, params, lin = Get_Name_And_Params(lines, sub_n, fname) + if isinstance(params, str): + return False, name, params + + NewCommand = Subroutine(name, params, lines) # Create a new command object for this subroutine + + if NewCommand: + if NewCommand.name in scripts.VALID_COMMANDS: # does this command already exist? + old_cmd = scripts.VALID_COMMANDS[NewCommand.name] # get the command we will be replacing + else: + old_cmd = None # if not, nothing to replace + + try: + scripts.Add_command(NewCommand) # Add the command before we parse (to allow recursion) + script_validation = NewCommand.btn.Validate_script()# and validate with the internal btn held in the command. + except: + print("[subroutines] Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.") + raise + + if script_validation != True: # if thre is an error in validation + if old_cmd: # and there is a replaced command + scripts.Add_command(NewCommand) # put the old command back + pass # @@@ there must be more to do! :-) This is the error return + else: + pass # @@@ this is the success return. There must be more to do! + + return True, NewCommand.name, params + \ No newline at end of file diff --git a/constants.py b/constants.py index 99561fb..1fe7edb 100644 --- a/constants.py +++ b/constants.py @@ -91,3 +91,5 @@ COLOR_FUNC_KEYS_PRIMED = 9 #amber EXIT_UPDATE_DELAY = 0.1 DELAY_EXIT_CHECK = 0.025 + +SUBROUTINE_PREFIX = "CALL:" diff --git a/files.py b/files.py index fb83e4e..68adaf2 100644 --- a/files.py +++ b/files.py @@ -1,14 +1,18 @@ import lp_colors, scripts from time import sleep import os, json, platform, subprocess +from constants import * LAYOUT_DIR = "user_layouts" SCRIPT_DIR = "user_scripts" +SUBROUTINE_DIR = "user_subroutines" -FILE_VERSION = "0.1.1" +FILE_VERSION = "0.1.1" # file is unchanged if no subroutines are saved +FILE_VERSION_SUBS = "0.2" # file version if subroutines are saved LAYOUT_EXT = ".lpl" SCRIPT_EXT = ".lps" +SUBROUTINE_EXT = ".lpc" LEGACY_LAYOUT_EXT = ".LPHKlayout" LEGACY_SCRIPT_EXT = ".LPHKscript" @@ -16,6 +20,7 @@ USER_PATH = None LAYOUT_PATH = None SCRIPT_PATH = None +SUBROUTINE_PATH = None import window @@ -27,9 +32,11 @@ def init(user_path_in): global USER_PATH global LAYOUT_PATH global SCRIPT_PATH + global SUBROUTINE_PATH USER_PATH = user_path_in LAYOUT_PATH = os.path.join(USER_PATH, LAYOUT_DIR) SCRIPT_PATH = os.path.join(USER_PATH, SCRIPT_DIR) + SUBROUTINE_PATH = os.path.join(USER_PATH, SUBROUTINE_DIR) def save_layout(layout, name, printing=True): with open(name, "w") as f: @@ -102,7 +109,8 @@ def load_layout(name, popups=True, save_converted=True, printing=True): def save_lp_to_layout(name): layout = dict() - layout["version"] = FILE_VERSION + + has_subs = False layout["buttons"] = [] for x in range(9): @@ -112,6 +120,19 @@ def save_lp_to_layout(name): script_text = scripts.buttons[x][y].script_str layout["buttons"][-1].append({"color": color, "text": script_text}) + + for x in scripts.VALID_COMMANDS: # for all the commands that exist + if x.startswith(SUBROUTINE_PREFIX): # if this command is a subroutine + if not has_subs: + layout["subroutines"] = [] # only add the key if required + has_subs = True + cmd = scripts.VALID_COMMANDS[x] # get the command + layout["subroutines"] += [cmd.routine] # add the command to the list (name is embedded in the subroutine) + + if has_subs: # file version depends on the existance of subroutines + layout["version"] = FILE_VERSION_SUBS + else: + layout["version"] = FILE_VERSION save_layout(layout=layout, name=name) @@ -123,6 +144,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): converted_to_rg = False scripts.Unbind_all() + scripts.Unload_all() # remove all existing subroutines when you load a new layout window.app.draw_canvas() if preload == None: @@ -130,6 +152,11 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): else: layout = preload + # load subroutines before buttons so you don't get errors on buttons using them + if "subroutines" in layout: # were subroutines saved? + for sub in layout["subroutines"]: # for all the subroutines that were saved + load_subroutine(sub, 0, 'LAYOUT') # load the subroutine + for x in range(9): for y in range(9): button = layout["buttons"][x][y] @@ -163,6 +190,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): scripts.Bind(x, y, script_text, color) else: lp_colors.setXY(x, y, color) + lp_colors.update_all() window.app.draw_canvas() @@ -176,6 +204,24 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): else: layout_changed_since_load = False +# load all the subroutines in a file +def load_subroutines_to_lp(name, popups=True, preload=None): + with open(name, 'r') as in_subs: + subs = in_subs.read().split('\n===\n') + + for i, sub in enumerate(subs): + load_subroutine(sub.splitlines(), i+1, name) + +# load a single subroutine +def load_subroutine(sub, sub_n, fname): + import commands_subroutines + ok, name, params = commands_subroutines.Add_Function(sub, sub_n, fname) # Attempt to load the command + + if ok: + pass # @@@ there must be more to do! :-) + else: + pass # @@@ likewise + def import_script(name): with open(name, "r") as f: text = f.read() diff --git a/scripts.py b/scripts.py index 9bad0b2..baae7d4 100644 --- a/scripts.py +++ b/scripts.py @@ -19,7 +19,7 @@ # GLOBALS is likewise empty until global variables get created GLOBALS = dict() # the globals themselvs -GLOBAL_LOCK = threading.Lock() # a lock got the globals to prevent simultaneous access +GLOBAL_LOCK = threading.Lock() # a lock for the globals to prevent simultaneous access @@ -54,10 +54,10 @@ def Remove_command( ): if command_name in HEADERS: # if this was previously a header - HEADERS.pop(command_name) + HEADERS.pop(command_name) # remove the header if command_name in VALID_COMMANDS: # if it already exists - HEADERS.pop(command_name) # remove it + VALID_COMMANDS.pop(command_name) # remove the command # Create a new symbol table. This contains information required for the script to run @@ -90,11 +90,13 @@ def __init__( self, x, # The button column y, # The button row - script_str # The Script + script_str, # The Script + root = None # Who called us ): self.x = x self.y = y + self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine self.script_str = script_str # The script self.validated = False # Has the script been validated? self.symbols = None # The symbol table for the button @@ -102,7 +104,17 @@ def __init__( self.thread = None # the thread associated with this button self.running = False # is the script running? self.is_async = False # async execution flag - self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? + if self.is_button: + self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? + else: + self.coords = "(SUB)" # subroutines don't have coordinates + + # The "root" is the button that is scheduled. This allows subroutines to check if the + # initiating button has been killed. + if root == None: # if we are not being called + self.root = self # then we are the root + else: # otherwise + self.root = root # the caller is the root # Do what is required to parse the script. Parsing does not output any information unless it is an error @@ -111,7 +123,10 @@ def Parse_script(self): return True if self.script_lines == None: # A little setup if the script lines are not created - self.script_lines = self.script_str.split('\n') # Create the lines + if isinstance(self.script_str, list): # Subroutines already have this as a list of lines + self.script_lines = self.script_str # Copy the lines + else: # But commands just have the raw stream from a file + self.script_lines = self.script_str.split('\n') # Create the lines self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks self.symbols = New_symbol_table() # Create a shiny new symbol table @@ -156,7 +171,21 @@ def Parse_script(self): return err # success or failure - def Check_kill(self, killfunc=None): + # copies parsed info from self to new_btn + def Copy_parsed(self, new_btn, name="SUB"): + new_btn.script_lines = self.script_lines # Copy the lines + new_btn.coords = "(" + name + ")" # set the name + + new_btn.symbols = New_symbol_table() + new_btn.symbols[SYM_REPEATS] = self.symbols[SYM_REPEATS].copy() # copy the repeats + new_btn.symbols[SYM_ORIGINAL] = self.symbols[SYM_ORIGINAL].copy() # and the original values + new_btn.symbols[SYM_LABELS] = self.symbols[SYM_LABELS].copy() # and the position of labels + + new_btn.is_async = self.is_async # default is NOT async + + + # check "self" for death notification + def Check_self_kill(self, killfunc=None): if not self.thread: print ("expecting a thread in ", self.coords) return False @@ -174,8 +203,12 @@ def Check_kill(self, killfunc=None): return False - # a sleep method that works with the multiple threads + # Check_kill now checks the root button for death notifications + def Check_kill(self, killfunc=None): + return self.root.Check_self_kill(killfunc) + + # a sleep method that works with the multiple threads def Safe_sleep(self, time, endfunc=None): while time > DELAY_EXIT_CHECK: sleep(DELAY_EXIT_CHECK) @@ -204,6 +237,7 @@ def Is_ignorable_line(self, line): def Schedule_script(self): + # @@@ may be worth checking to see if it's a subroutine. Because subroutines shouldn't use this global to_run if self.thread != None: @@ -224,7 +258,7 @@ def Schedule_script(self): if self.is_async: print("[scripts] " + self.coords + " Starting asynchronous script in background...") - self.thread = threading.Thread(target=run_script, args=()) + self.thread = threading.Thread(target=Run_script, args=()) self.thread.kill = threading.Event() self.thread.start() elif not self.running: @@ -290,7 +324,7 @@ def split1(line): # just strip off a single (non-quoted) par return param, line # return the parameter and the rest of the line - # grab a quoted string from the line passed. Does not handle embedded quotes @@@ but it should + # grab a quoted string from the line passed. Handles embedded quotes def strip_quoted(line): l2 = line # a copy of the line we can edit q = l2[0] # the first character is assumed to be a quote @@ -369,6 +403,7 @@ def strip_quoted(line): # run a script def Run_script(self): + # @@@ maybe check we're not a subroutine (subroutines should not use this) lp_colors.updateXY(self.x, self.y) if self.Validate_script() != True: @@ -378,7 +413,7 @@ def Run_script(self): self.running = not self.is_async - cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters + cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd_txt in VALID_COMMANDS: command = VALID_COMMANDS[cmd_txt] command.Run(self, -1, [cmd_txt]) @@ -427,7 +462,68 @@ def Main_logic(idx): # the main logic t threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours - print("[scripts] " + self.coords + " Script done running.") # and print (log?) that the script is complete + print("[scripts] " + self.coords + " Script ended.") # and print (log?) that the script is complete + + + # run a subroutine. This is a simplified version of running a script because the script takes care of being scheduled and killed + # @@@ this is so close to run_script that it probably should be merged with it at some point -- after I know its working + def Run_subroutine(self): + # @@@ maybe check that we **are** a subroutine first. This is for subroutines ONLY + if self.Validate_script() != True: # validates if not validated + return + + print("[scripts] " + self.coords + " Now running subroutine ...") + + self.running = not self.is_async # @@@ not sure a async subroutine makes sense + + cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters + if cmd_txt in VALID_COMMANDS: + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) + + if len(self.script_lines) > 0: + self.running = True + + def Main_logic(idx): # the main logic to run a line of a script + if self.Check_kill(): # first check on our death notification + return idx + 1 # we just return the next line, @@@ returning -1 is better + + line = self.Line(idx) # get the line of the script + + # Handle completely blank lines + if line == "": + return idx + 1 + + # Get the command text + cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact + + # Now get the command object + if cmd_txt in VALID_COMMANDS: # make sure it's a valid command + command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command + + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters + + if type(split_line) == tuple: # bad news if we get a tuple rather than a list + print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") + else: + # now run the command + return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out + else: + print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") + + return idx + 1 # defaut action is to ask for the next line + + run = True # flag that we're running + idx = 0 # point at the first line + while run: # and while we're still running + idx = Main_logic(idx) # run the current line + if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid + run = False # then we're not going to keep running! + + if not self.is_async: # async commands don't just end @@@ again, not sure this makes sense for subroutines + self.running = False # they have to say they're not running + + print("[scripts] " + self.coords + " Subroutine ended.") # and print (log?) that the script is complete # validating a script consists of doing the checks that we do prior to running, but @@ -565,15 +661,13 @@ def Is_bound(x, y): return True # Otherwise it is -# Unbind all keys. -def Unbind_all(): +# kill all threads +def kill_all(): global buttons global to_run - lp_events.unbind_all() # Unbind all events - text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank to_run = [] # nothing queued to run - + for x in range(9): # For each column... for y in range(9): # ...and row btn = buttons[x][y] @@ -581,7 +675,30 @@ def Unbind_all(): if btn.thread.isAlive(): # ...and if the thread is alive... btn.thread.kill.set() # ...kill it + +# Unbind all keys. +def Unbind_all(): + lp_events.unbind_all() # Unbind all events + text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank + + kill_all() # stop everything running + files.curr_layout = None # There is no current layout files.layout_changed_since_load = False # So mark it as unchanged +# Unload all subroutines. +def Unload_all(): + kill_all() # stop everything running + + subs = [] # list of subroutines to remove + for cmd in VALID_COMMANDS: # for all the commands that exist + if cmd.startswith(SUBROUTINE_PREFIX):# if this command is a subroutine + subs += [cmd] # add the command to the list + + for cmd in subs: # for each subroutine we've found + Remove_command(cmd) # remove it + + files.layout_changed_since_load = True # mark layout as changed + + diff --git a/user_subroutines/AddOne.lpc b/user_subroutines/AddOne.lpc new file mode 100644 index 0000000..0054897 --- /dev/null +++ b/user_subroutines/AddOne.lpc @@ -0,0 +1,2 @@ +@SUB ADD_ONE @a% +RPN_EVAL view_l < a 1 + > a view_l \ No newline at end of file diff --git a/user_subroutines/AddOneError.lpc b/user_subroutines/AddOneError.lpc new file mode 100644 index 0000000..a144fc2 --- /dev/null +++ b/user_subroutines/AddOneError.lpc @@ -0,0 +1 @@ +RPN_EVAL < a 1 + > a \ No newline at end of file diff --git a/variables.py b/variables.py index a7a8e28..3e4d68d 100644 --- a/variables.py +++ b/variables.py @@ -1,9 +1,13 @@ from constants import * +import variables +import re # operations needed to access variables # NOTE that any locking is the responsibility of the calling code! +# Regular expression for validating variable names +VALID_RE = re.compile('^[A-Za-z][A-Za-z0-9_]*$') # Note that popping a value from an empty stack returns 0. An alternative is # to return an error @@ -67,9 +71,9 @@ def next_cmd(ret, cmds): return ret+1, v # and we return an updated pointer and the removed element -# variable names should start with an alpha character +# variable names should start with an alpha character and contain only alpha numeric and underscores def valid_var_name(v): - return isinstance(v, str) and len(v) > 0 and ord(v[0].upper()) in range(ord('A'), ord('Z')+1) + return isinstance(v, str) and VALID_RE.match(v) # return a properly formatted error message @@ -219,15 +223,45 @@ def Validate_ge_zero(v, idx, name, desc, p, param): return error_msg(idx, name, desc, p, param, 'must be an integer') -def Auto_store(v, a, symbols): +def Auto_store(v_name, value, symbols): # automatically stores the variable in the "right" place with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - if is_defined(v, symbols[SYM_LOCAL]): # Is it local... - put(v, a, symbols[SYM_LOCAL]) # ...then store it locally - elif is_defined(v, symbols[SYM_GLOBAL][1]): # Is it global... - put(v, a, symbols[SYM_GLOBAL][1]) # ...store it globally + if is_defined(v_name, symbols[SYM_LOCAL]): # Is it local... + put(v_name, value, symbols[SYM_LOCAL]) # ...then store it locally + elif is_defined(v_name, symbols[SYM_GLOBAL][1]): # Is it global... + put(v_name, value, symbols[SYM_GLOBAL][1]) # ...store it globally else: - put(v, a, symbols[SYM_LOCAL]) # default is to create new in locals + put(v_name, value, symbols[SYM_LOCAL]) # default is to create new in locals +def Local_store(v_name, value, symbols): + # stores the variable locally + put(v_name, value, symbols[SYM_LOCAL]) # and store it locally + +def Global_store(v_name, value, symbols): + # stores the variable globally + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + put(v_name, value, symbols[SYM_GLOBAL][1]) # and store it globally + + +def Auto_recall(v_name, symbols): + # automatically recalls the variable from the "right" place + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + a = variables.get(v_name, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1]) # try local, then global + + return a + + +def Local_recall(v_name, symbols): + # automatically recalls the local variable + a = variables.get(v_name, symbols[SYM_LOCAL], None) # get the value from the local vars + return a + + +def Global_recall(v_name, symbols): + # automatically recalls the global variable + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + a = variables.get(v_name, None, symbols[SYM_GLOBAL][1]) # grab the value from the global vars + + return a diff --git a/window.py b/window.py index 2959b89..1431f29 100644 --- a/window.py +++ b/window.py @@ -41,14 +41,18 @@ load_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT, files.LEGACY_LAYOUT_EXT])] load_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT, files.LEGACY_SCRIPT_EXT])] +load_subroutine_filetypes = [('LPHK subroutine files', [files.SUBROUTINE_EXT])] save_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT])] save_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT])] +save_subroutine_filetypes = [('LPHK subroutine files', [files.SUBROUTINE_EXT])] lp_connected = False lp_mode = None colors_to_set = [[DEFAULT_COLOR for y in range(9)] for x in range(9)] +MENU_LAYOUT = "Layout" +MENU_SUBROUTINES = "Subroutines" def init(lp_object_in, launchpad_in, path_in, prog_path_in, user_path_in, version_in, platform_in): global lp_object @@ -111,9 +115,16 @@ def init_window(self): self.m_Layout.add_command(label="Load Layout", command=self.load_layout) self.m_Layout.add_command(label="Save Layout", command=self.save_layout) self.m_Layout.add_command(label="Save Layout As...", command=self.save_layout_as) - self.m.add_cascade(label="Layout", menu=self.m_Layout) + self.m.add_cascade(label=MENU_LAYOUT, menu=self.m_Layout) - self.disable_menu("Layout") + self.disable_menu(MENU_LAYOUT) + + self.m_Subroutine = tk.Menu(self.m, tearoff=False) + self.m_Subroutine.add_command(label="Load", command=self.load_subroutines) + self.m_Subroutine.add_command(label="Clear", command=self.clear_subroutines) + self.m.add_cascade(label=MENU_SUBROUTINES, menu=self.m_Subroutine) + + self.disable_menu(MENU_SUBROUTINES) self.m_Help = tk.Menu(self.m, tearoff=False) open_readme = lambda: webbrowser.open("https://github.com/nimaid/LPHK#lphk-launchpad-hotkey") @@ -158,7 +169,8 @@ def connect_dummy(self): lp_connected = True lp_mode = "Dummy" self.draw_canvas() - self.enable_menu("Layout") + self.enable_menu(MENU_LAYOUT) + self.enable_menu(MENU_SUBROUTINES) def connect_lp(self): global lp_connected @@ -204,7 +216,8 @@ def connect_lp(self): lp_events.start(lp_object) self.draw_canvas() - self.enable_menu("Layout") + self.enable_menu(MENU_LAYOUT) + self.enable_menu(MENU_SUBROUTINES) self.stat["text"] = f"Connected to {lpcon.get_display_name(lp)}" self.stat["bg"] = STAT_ACTIVE_COLOR @@ -226,7 +239,8 @@ def disconnect_lp(self): self.clear_canvas() - self.disable_menu("Layout") + self.disable_menu(MENU_LAYOUT) + self.disable_menu(MENU_SUBROUTINES) self.stat["text"] = "No Launchpad Connected" self.stat["bg"] = STAT_INACTIVE_COLOR @@ -252,6 +266,19 @@ def load_layout(self): if name: files.load_layout_to_lp(name) + # user requests subroutine load + def load_subroutines(self): + name = tk.filedialog.askopenfilename(parent=app, + initialdir=files.SUBROUTINE_PATH, + title="Load subroutines", + filetypes=load_subroutine_filetypes) # get the filename + if name: + files.load_subroutines_to_lp(name) # and load routines if a file was selected + + # user requests clearing all subroutines + def clear_subroutines(self): + scripts.Unload_all() # unload all subroutines + def save_layout_as(self): name = tk.filedialog.asksaveasfilename(parent=app, initialdir=files.LAYOUT_PATH, From 6cc11497f6499c86a2d1c3be8af05d8c19a4fb73 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 8 Mar 2021 18:07:19 +0800 Subject: [PATCH 47/83] This version has badly broken variables due to changes made for subroutines :-( The problem is that variables and strings can easily be confused by the code, especially where the string is a valid variable name. This should be fixed by applying the artificial constraint that strings can only be either literals (var = no) or passed by reference (var = must), but that's only a bandaid. Variables *probably* worked ok prior to the subroutine hacks. * Changes everywhere to remove blanks on ends of lines * Changes made to implement opening in standalone mode (with no connection to launchpad) * global_vars.py added for a similar reason to constants.py, but for global variables. This is used for the arguments. * launchpad_fake.py added to create objects that pretend to be launchpads, but don't really do anything * new switch -s or --standalone to set standalone mode. Various routines cause launchpad object creation to point to either the real launchpad module, or the fake. * window.IsStandalone() returns true if in standalone mode. This shouldn't be needed very often! * Launchpad menu is disabled in standalone mode. * fix to the looping after an attempt to reconnect *may be* fixed by setting window_restart to False. Note that there still appears to be a need to press enter on the console once for each restart. Not sure why -- but at least it doesn't loop forever. * new function param_convs.py to define conversion functions for parameters. We can no longer just use str() or int() because these don't handle conversion of null values (i.e. missing variables) in a useful manner. * corresponding changes to constants.py to use these new routines. * initial work to address problems with parameters (incomplete - includes debugging code) * optionally passing conversion code to "get" to ensure variables are interpreted correctly * removed second parameter from window.init() - this was never used. * variables code changed to be more robust when non-strings are passed as variable names (this shouldn't happen!) --- LPHK.py | 58 ++++++++--- command_base.py | 223 +++++++++++++++++++++------------------- command_list.py | 6 +- commands_control.py | 154 +++++++++++++-------------- commands_external.py | 50 ++++----- commands_header.py | 8 +- commands_keys.py | 56 +++++----- commands_mouse.py | 88 ++++++++-------- commands_pause.py | 2 +- commands_rpncalc.py | 181 ++++++++++++++++---------------- commands_scrape.py | 76 +++++++------- commands_subroutines.py | 103 +++++++++---------- commands_win32.py | 164 ++++++++++++++--------------- constants.py | 24 +++-- files.py | 48 ++++----- global_vars.py | 5 + kb.py | 2 +- launchpad_fake.py | 50 +++++++++ logger.py | 10 +- lp_colors.py | 2 +- lp_events.py | 4 +- param_convs.py | 29 ++++++ scripts.py | 194 +++++++++++++++++----------------- sound.py | 3 +- variables.py | 130 ++++++++++++----------- window.py | 196 ++++++++++++++++++++--------------- 26 files changed, 1015 insertions(+), 851 deletions(-) create mode 100644 global_vars.py create mode 100644 launchpad_fake.py create mode 100644 param_convs.py diff --git a/LPHK.py b/LPHK.py index 26b14bb..fc819a2 100755 --- a/LPHK.py +++ b/LPHK.py @@ -1,4 +1,4 @@ -import sys, os, subprocess, argparse +import sys, os, subprocess, argparse, global_vars from datetime import datetime from constants import * @@ -10,7 +10,7 @@ if getattr(sys, 'frozen', False): IS_EXE = True PROG_FILE = sys.executable - PROG_PATH = os.path.dirname(PROG_FILE) + PROG_PATH = os.path.dirname(PROG_FILE) PATH = sys._MEIPASS else: IS_EXE = False @@ -65,10 +65,10 @@ def datetime_str(): # Try to import launchpad.py try: - import launchpad_py as launchpad + import launchpad_py as launchpad_real except ImportError: try: - import launchpad + import launchpad as launchpad_real except ImportError: sys.exit("[LPHK] Error loading launchpad.py") print("") @@ -79,7 +79,17 @@ def datetime_str(): # just import the control modules to automatically integrate them import command_list -lp = launchpad.Launchpad() + +# create a launchpad object, either real or fake +def Launchpad(): + if window.IsStandalone(): + import launchpad_fake + return launchpad_fake.launchpad + else: + return launchpad_real.Launchpad() + + +LP = None EXIT_ON_WINDOW_CLOSE = True @@ -88,51 +98,65 @@ def init(): ap = argparse.ArgumentParser() # argparse makes argument processing easy ap.add_argument( # reimnplementation of debug (-d or --debug) - "-d", "--debug", - help = "turn on debugging mode", action="store_true") - ap.add_argument( # new option to automatically load a layout - "-l", "--layout", - help = "load a layout", + "-d", "--debug", + help = "Turn on debugging mode", action="store_true") + ap.add_argument( # option to automatically load a layout + "-l", "--layout", + help = "Load an initial layout", type=argparse.FileType('r')) - ap.add_argument( # new option to start minimised - "-m", "--minimised", + ap.add_argument( # option to start minimised + "-m", "--minimised", help = "Start the application minimised", action="store_true") + ap.add_argument( # option to start without connecting to a Launchpad + "-s", "--standalone", + help = "Operate without connection to Launchpad", action="store_true") - window.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to + global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to - if window.ARGS['debug']: + if global_vars.ARGS['debug']: EXIT_ON_WINDOW_CLOSE = False print("[LPHK] Debugging mode active! Will not shut down on window close.") print("[LPHK] Run shutdown() to manually close the program correctly.") - + files.init(USER_PATH) sound.init(USER_PATH) + + global LP + LP = Launchpad() def shutdown(): if lp_events.timer != None: # cancel any outstanding events lp_events.timer.cancel() + scripts.to_run = [] # remove anything from the list of scripts scheduled to run + for x in range(9): for y in range(9): if scripts.buttons[x][y].thread != None: scripts.buttons[x][y].thread.kill.set() # request to kill any running threads + if window.lp_connected: scripts.Unbind_all() # unbind all the buttons lp_events.timer.cancel() # cancel all the timers - launchpad_connector.disconnect(lp) # disconnect from the launchpad + if LP != None and LP != -1: + launchpad_connector.disconnect(LP) # disconnect from the launchpad window.lp_connected = False + logger.stop() # stop logging + if window.restart: + window.restart = False # don't do this forever if IS_EXE: os.startfile(sys.argv[0]) else: os.execv(sys.executable, ["\"" + sys.executable + "\""] + sys.argv) + sys.exit("[LPHK] Shutting down...") def main(): init() - window.init(lp, launchpad, PATH, PROG_PATH, USER_PATH, VERSION, PLATFORM) + window.init(LP, PATH, PROG_PATH, USER_PATH, VERSION, PLATFORM) if EXIT_ON_WINDOW_CLOSE: shutdown() diff --git a/command_base.py b/command_base.py index 185daff..7748627 100644 --- a/command_base.py +++ b/command_base.py @@ -8,7 +8,7 @@ # Command_Basic is a class that describes a command class Command_Basic: def __init__( - self, + self, name: str, # The name of the command (what you put in the script) lib="LIB_UNSET", auto_validate=None, # Definition of the input parameters @@ -17,46 +17,46 @@ def __init__( # The information below MUST NOT be changed outside __init__ . # Remember - more than one command may be in execution at a time - # and we rely on the parameters to the methods to contain things - # unique to each one! Local variables are fine, self.anything is BAD + # and we rely on the parameters to the methods to contain things + # unique to each one! Local variables are fine, self.anything is BAD self.name = name # the literal name of our command self.lib = lib # the library we're part of self.auto_validate = auto_validate # any auto-validation, if defined self.auto_message = auto_message # format for any messages we need - + self.validate_init() - + self.valid_max_params = self.Calc_valid_max_params() # calculate the max number of parmeters self.valid_num_params = self.Calc_valid_param_counts() # calculate the set of valid numbers of parameters self.run_states = [RS_INIT, RS_GET, RS_INFO, RS_VALIDATE, RS_RUN, RS_FINAL] # by default we'll do everything if you don't override self.validation_states = [VS_COUNT, VS_PASS_1, VS_PASS_2] # by default we'll do a count and both passes if you don't override - - + + def validate_init(self): # This helps validate the parameters passed to __init__. It helps enforce the rules avl = self.auto_validate if avl != None: - np = len(avl) - 1 - for p, av in enumerate(avl): + np = len(avl) - 1 + for p, av in enumerate(avl): if av[AV_TYPE][AVT_LAST] and p < np: # variable type is "last" but it's not the last - raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') is not the last parameter') + raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') is not the last parameter') if not (av[AV_VAR_OK] in av[AV_TYPE][AVT_MAX_VAR]): # some types can't be passed variables - raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') specifies a variable type that is not permittted') + raise Exception('ERROR in command "' + self.name + '" - the parameter #' + str(p+1) + ' (' + av[AVT_DESC] + ') specifies a variable type that is not permittted') def Validate( # This is a low level validation routine. If you take over this function you must take # responsibility for all validation. - + # If you have set up the auto_valudate structure, it will do most of the validation for you. # If you need to do more, you may be able to override a more specific routine. - + # This routine will be called twice, once for pass_no 1 and again for pass_no 2 # Pass 1 is for general validation of literal commands and literal parameters, and also # for adding symbols (for example, labels). # Pass 2 is typically used for checking for the presence of symbols (labels, for example) - + # This method should return True if the validation was successful, otherwise it should # return a tuple of the error message and the line causing the error. self, @@ -72,29 +72,29 @@ def Validate( try: # invalid return, but indicates nothing done yet. ret = None - + # If it's pass 1 if pass_no == VS_PASS_1: # validate the count if required if VS_COUNT in self.validation_states: ret = self.Partial_validate_step_count(ret, btn, idx, split_line) - + # do pass 1 validation if required if VS_PASS_1 in self.validation_states: ret = self.Partial_validate_step_pass_1(ret, btn, idx, split_line) - + # if it's pass 2 elif pass_no == VS_PASS_2: # call Pass 2 if required if VS_PASS_1 in self.validation_states: ret = self.Partial_validate_step_pass_2(ret, btn, idx, split_line) - + except: import traceback traceback.print_exc() ret = ("", "") - finally: + finally: if type(ret) == tuple: return ret elif ret == None or ((type(ret) == bool) and ret): @@ -104,11 +104,11 @@ def Validate( def Partial_validate_step_count(self, ret, btn, idx, split_line): - # Validation of the count is separated from the pass 1 validation because sometinmes + # Validation of the count is separated from the pass 1 validation because sometinmes # you want to override one but not the other. You would override this if you have some # odd way of counting parameters, or the count depends on something complex. - ret = self.Validate_param_count(ret, btn, idx, split_line) - return ret + ret = self.Validate_param_count(ret, btn, idx, split_line) + return ret def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): @@ -117,7 +117,7 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. ret = self.Validate_params(ret, btn, idx, split_line, AV_P1_VALIDATION) - return ret + return ret def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): @@ -126,7 +126,7 @@ def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): # structure cannot pass something important about the validation. If you override this, # you may wish to call the ancestor first. ret = self.Validate_params(ret, btn, idx, split_line, AV_P2_VALIDATION) - return ret + return ret def Parse( @@ -146,7 +146,7 @@ def Parse( ret = self.Validate(btn, idx, split_line, pass_no) if ((type(ret) == bool) and ret): - return True + return True if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.Line(idx)) @@ -158,17 +158,17 @@ def Parse( def Run( - # The low level run command. Override this if you want to take complete control of the execution of + # The low level run command. Override this if you want to take complete control of the execution of # the command. Typically you'll want to override one of the Partial_run... methods or the Perform() # method - + # This should return idx+1 normally (this causes the script to continue at the next line (or exit - # when it falls off the end. - + # when it falls off the end. + # If you wish to abort the script, you should return a value outside of the range of valid line numbers. # Typically -1 is returned, however in some cases a very high number can also be returned. - - # To cause the script to jump to a different line, simply return the line number you wish to go to. + + # To cause the script to jump to a different line, simply return the line number you wish to go to. self, btn, idx: int, @@ -177,27 +177,27 @@ def Run( try: ret = None # this is an invalid return value, but it indicates nothing has happened yet - + if RS_INIT in self.run_states: # Do the initialisation if required (highly recommended) ret = self.Partial_run_step_init(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret - - if RS_GET in self.run_states: # Get the parameters if required + + if RS_GET in self.run_states: # Get the parameters if required ret = self.Partial_run_step_get(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret - + if RS_INFO in self.run_states: # Display info if required ret = self.Partial_run_step_info(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret - - if RS_VALIDATE in self.run_states: # Validate the parameters if required + + if RS_VALIDATE in self.run_states: # Validate the parameters if required ret = self.Partial_run_step_validate(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): return ret - + if RS_RUN in self.run_states: # Actualy do the command! (calls Perform() ret = self.Partial_run_step_run(ret, btn, idx, split_line) if ret == -1 or ((type(ret) == bool) and not ret): @@ -207,8 +207,8 @@ def Run( import traceback traceback.print_exc() ret = -1 - - finally: + + finally: if RS_FINAL in self.run_states: # Do the finalisation if required (highly recommended) self.Partial_run_step_final(ret, btn, idx, split_line) @@ -223,15 +223,15 @@ def Run( def Partial_run_step_init(self, ret, btn, idx, split_line): # information about *this* run of the command MUST be in the symbol table - + # You might be tempted to not run the init if the variables below aren't needed, # however this could have consequences in the future, so it's best to run it. - - # If you need more temporary data, you can override this, call the ancestor, and - # create what you need. + + # If you need more temporary data, you can override this, call the ancestor, and + # create what you need. btn.symbols[SYM_PARAMS] = [self.name] + [None] * self.Param_validation_count(len(split_line)-1) btn.symbols[SYM_PARAM_CNT] = 0 - + return ret @@ -246,11 +246,11 @@ def Partial_run_step_get(self, ret, btn, idx, split_line): def Partial_run_step_info(self, ret, btn, idx, split_line): # This step matches the number of parameters passed with the definitions for messages, # printing the matching message, or a default message if no matching message can be found. - + # If you have messages that don't fit a simple template (e.g. 2 different possible messages # for the same number of parameters then you're going to want to override this method. # If you're overriding the method, you will rarely want to call the ancestor method. - msg = False + msg = False if self.auto_message: params = btn.symbols[SYM_PARAMS] param_cnt = btn.symbols[SYM_PARAM_CNT] @@ -280,7 +280,7 @@ def Partial_run_step_run(self, ret, btn, idx, split_line): ret = self.Process(btn, idx, split_line) if ret == None: ret = idx + 1 - + return ret @@ -290,7 +290,7 @@ def Partial_run_step_final(self, ret, btn, idx, split_line): # of a command this might start to get more important. # If you override this, it is conventional to call the ancestor function last, but there's no reason - # at present that you must. + # at present that you must. del btn.symbols[SYM_PARAMS] del btn.symbols[SYM_PARAM_CNT] @@ -298,13 +298,13 @@ def Partial_run_step_final(self, ret, btn, idx, split_line): def Process(self, btn, idx, split_line): - # This is the default process called to run a command. Override it to do something other than + # This is the default process called to run a command. Override it to do something other than # nothing at runtime. - + # This is probably the most common method you will override. It is designed in such a way that # you do not need to call the ancestor. - pass # default process is to do nothing - + pass # default process is to do nothing + def Calc_valid_max_params(self): # Return the maximum number of parameters. We can calculate this simply based on the number defined @@ -315,7 +315,7 @@ def Calc_valid_max_params(self): if avl: if len(avl) == 0 or not avl[-1][AV_TYPE][AVT_LAST]: return len(avl) - + return None @@ -334,31 +334,31 @@ def Calc_valid_param_counts(self): ret = [] vn = len(avl) for i, av in enumerate(avl): - if av[AV_OPTIONAL]: # if this one is optional + if av[AV_OPTIONAL]: # if this one is optional ret += [i] # then 1 fewer is OK if vn > 0 and avl[-1][AV_TYPE][AVT_LAST]: ret += [vn, None] # if the last parameter is a "last" parameter, there is no limit on parameters else: - ret += [vn] # it's always valid to pass *all* the parameters + ret += [vn] # it's always valid to pass *all* the parameters return ret - def Validate_param_count(self, ret, btn, idx, split_line): - # Should only be called from pass 1 (actually within VS_COUNT that happens just prior to + def Validate_param_count(self, ret, btn, idx, split_line): + # Should only be called from pass 1 (actually within VS_COUNT that happens just prior to # VS_PASS_1 # Whilst you can override this method, you're more likely to override the Validation_step_count() - # method which does no more than just call this. + # method which does no more than just call this. if not (ret == None or ((type(ret) == bool) and ret)): return ret - + v = variables.Check_num_params(btn, self, idx, split_line) return v - - + + def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. @@ -368,12 +368,12 @@ def Param_validation_count(self, n_passed): return n_passed else: return vmp - + def Validate_params(self, ret, btn, idx, split_line, val_validation): - # This command is called from both pass 1 and 2 of validation It is really just a method to + # This command is called from both pass 1 and 2 of validation It is really just a method to # call the validation of the parameters one by one. If you haven't set up the maximum parameters - # (if you haven't used the auto_validate structure) then you can override this to validate each + # (if you haven't used the auto_validate structure) then you can override this to validate each # of your parameters. You will need to remember that this gets called for both pass 1 and 2. if not (ret == None or ((type(ret) == bool) and ret)): return ret @@ -388,30 +388,30 @@ def Validate_params(self, ret, btn, idx, split_line, val_validation): def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # This method validates parameters. For custom parameters, you're best off defining new validation - # methods (like the current variables.Validate_gt_zero()) unless you need access to the symbol + # methods (like the current variables.Validate_gt_zero()) unless you need access to the symbol # table. - + # Note that this function, because it runs during validation, accesses the split_line, not the # symbol table. - + # Where a variable type is defined as having "special" validation, that validation is currently # hard coded here. It would be better to register validation routines, but... later. if not (ret == None or ((type(ret) == bool) and ret)): return ret - + if n >= len(split_line): return ret if self.auto_validate == None or self.auto_validate == (): # no auto validation can be done return ret - + if n <= len(self.auto_validate): # the normal auto-validation val = self.auto_validate[n-1] else: # special case for "last" parameters val = self.auto_validate[-1] - + opt = self.valid_num_params == [] or \ self.valid_num_params[-1] == None or \ (set(range(1,n)) & set(self.valid_num_params)) != [] @@ -424,7 +424,7 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # check for valid variable name if not variables.valid_var_name(split_line[n]): # Is it a valid variable name? return ("Invalid variable name", btn.Line(idx)) - + elif val[AV_TYPE][AVT_SPECIAL]: if val_validation == AV_P1_VALIDATION: if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only @@ -442,27 +442,27 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): # check for valid boolean value if not (split_line[n].upper() in VALID_BOOL): # Is it a valid boolean? return ("Invalid boolean value", btn.Line(idx)) - + elif val_validation == AV_P2_VALIDATION: if val[AV_TYPE] == PT_LABEL: # references (to a label) have pass 2 validation only # check for existance of label if split_line[n] not in btn.symbols[SYM_LABELS]: return ("Target not found", btn.Line(idx)) - + return True - + return (ret, btn.Line(idx)) - - + + def Run_params(self, ret, btn, idx, split_line, pass_no): # This method gets the parameters. Oddly enough it has 2 passes too. The first pass simply gets the # variables, while the second pass gets them and does validation. - + # This method actually just calls the Run_Param_n method that does all the hard work for each parameter if ret == None: ret = True - - if pass_no == 1: + + if pass_no == 1: param_cnt = len(split_line) - 1 btn.symbols[SYM_PARAM_CNT] = param_cnt btn.symbols[SYM_PARAMS][0] = split_line[0] @@ -470,7 +470,7 @@ def Run_params(self, ret, btn, idx, split_line, pass_no): for i in range(self.Param_validation_count(param_cnt)): if i < param_cnt: btn.symbols[SYM_PARAMS][i+1] = self.Run_param_n(ret, btn, idx, split_line, pass_no, i+1) - + elif pass_no == 2: # for pass 2 we don't try to validate null variables param_cnt = len(split_line) - 1 @@ -484,7 +484,7 @@ def Run_params(self, ret, btn, idx, split_line, pass_no): def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): # This function gets called to firstly get the parameter (pass_no = 1) and then # to validate it (with pass_no = 2) - + # Note that pass 1 returns the variable value, where pass 2 returns a value indicating # if validation has passed. avl = self.auto_validate @@ -493,68 +493,77 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): av = avl[n-1] else: av = avl[-1] - + if pass_no == 1: v = split_line[n] - - if av[AV_VAR_OK] == AVV_YES: - v = variables.get_value(split_line[n], btn.symbols) - elif av[AV_VAR_OK] != AVV_REQD and av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: - v = av[AV_TYPE][AVT_CONV](v) - + print(v) #@@@ + + if av[AV_VAR_OK] == AVV_YES: # if a variable is allowed + if valid_var_name(v): # if it's a variable + v = variables.get_value(split_line[n], btn.symbols) # get the value + print(v) #@@@ + + if av[AV_VAR_OK] != AVV_REQD: # if it's not required (i.e. the variable name is not to be passed through) + if av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: # if there is a type + v = av[AV_TYPE][AVT_CONV](v) # convert the variable to that type + print(v) #@@@ + return v elif pass_no == 2: ok = ret - + if av[AV_P1_VALIDATION]: + print(btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ ok = av[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) if ok != True: print("[" + self.lib + "] " + btn.coords + " " + ok) ret = -1 - + return ret # Is there a parameter n? - def Has_param(self, btn, n): + def Has_param(self, btn, n): val = btn.symbols[SYM_PARAMS][n] return not (val is None) # How many parameters do we have? - def Param_count(self, btn): + def Param_count(self, btn): return btn.symbols[SYM_PARAM_CNT] # gets the value of the nth parameter (button is required for context). Other is default value if param does not exist - def Get_param(self, btn, n, other=None): + def Get_param(self, btn, n, other=None): # handle the repeating last parameter avl = len(self.auto_validate) - m = min(n, avl) + m = min(n, avl) val = self.auto_validate[m-1] - + param = btn.symbols[SYM_PARAMS][n] + print(param) #@@@ if param == None: ret = other else: if val[AV_VAR_OK] == AVV_REQD: - ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) + ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: if type(param) == str and val[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': ret = param[1:] else: ret = param - + return ret - + # sets the value of the nth parameter (if it is a variable) - def Set_param(self, btn, n, val): + def Set_param(self, btn, n, val): param = btn.symbols[SYM_PARAMS][n] av = self.auto_validate[n-1] if av[AV_VAR_OK] == AVV_REQD: + print(btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable - + # ################################################## # ### CLASS Command_Text_Basic ### @@ -563,11 +572,11 @@ def Set_param(self, btn, n, val): # class that defines an object that can handle just text after the command class Command_Text_Basic(Command_Basic): def __init__( - self, - name: str, # The name of the command (what you put in the script) + self, + name: str, # The name of the command (what you put in the script) lib, info_msg): # what we display before the text - + super().__init__(name, # the name of the command as you have to enter it in the code lib, (), @@ -576,7 +585,7 @@ def __init__( # this command does not have a standard list of fields, so we need to do some stuff manually self.valid_max_params = 32767 # There is no maximum, but this is a reasonable limit! self.valid_num_params = [0, None] # zero or more is OK - + if "{1}" in info_msg: self.info_msg = info_msg # customised message text before parameter text else: @@ -593,9 +602,9 @@ def Partial_run_step_info(self, ret, btn, idx, split_line): # Command_Header is a class specifically defining a header command class Command_Header(Command_Basic): - + def __init__( - self, + self, name: str, # The name of the command (what you put in the script) is_async: bool, # is this async? lib="LIB_UNSET", diff --git a/command_list.py b/command_list.py index 525cb65..9dd7074 100644 --- a/command_list.py +++ b/command_list.py @@ -19,7 +19,7 @@ try: import commands_rpncalc except ImportError: - print("[LPHK] WARNING: RPN_EVAL command is not available") + print("[LPHK] WARNING: RPN_EVAL command is not available") traceback.print_exc() # This library could be considered optional, and is also platform specific @@ -27,7 +27,7 @@ try: import commands_win32 except ImportError: - print("[LPHK] ERROR: Windows specific commands are not available") + print("[LPHK] ERROR: Windows specific commands are not available") traceback.print_exc() try: @@ -35,7 +35,7 @@ except ImportError: print("[LPHK] ERROR: Screen scraping commands are not available") traceback.print_exc() -else: +else: print("[LPHK] WARNING: Windows specific and screen scraping commands cannot be loaded") # Any that were not optional should set the error flag so we can exit diff --git a/commands_control.py b/commands_control.py index 8ca55b0..fd03b1c 100644 --- a/commands_control.py +++ b/commands_control.py @@ -12,7 +12,7 @@ # to allow it to work without a space following it class Control_Comment(command_base.Command_Text_Basic): def __init__( - self, + self, ): super().__init__("-", # the name of the command as you have to enter it in the code @@ -29,20 +29,20 @@ def __init__( # class that defines the LABEL command (a target of GOTO's etc) class Control_Label(command_base.Command_Basic): - def __init__( - self + def __init__( + self ): super().__init__( "LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Label", False, AVV_NO, PT_TARGET, None, None), + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_TARGET, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Label {1}"), + (1, " Label {1}"), ) ) @@ -58,12 +58,12 @@ def __init__( # THIS IS NOT REGISTERED. IT IS AN ANCESTOR CLASS FOR OTHER MORE POWERFUL COMMANDS class Control_Flow_Basic(command_base.Command_Basic): def __init__( - self, + self, name: str, # The name of the command (what you put in the script) lib=LIB, auto_validate=None, # Definition of the input parameters auto_message=None, # Definition of the message format - invalid_message=None, # Info message if invalid + invalid_message=None, # Info message if invalid valid_function=None, # Test to be performed to determine validity label_preceeds=False, # must the label preceed this line reset=False, # do we do reset at end of loop? @@ -76,7 +76,7 @@ def __init__( lib, auto_validate, auto_message); - + # note that it is safe to have these extra variables in the class, as they are # constant for a given child class. self.invalid_message = invalid_message @@ -90,49 +90,49 @@ def __init__( def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation - + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error if self.loop_val_init_function: # and if we have a validation self.loop_val_init_function(btn, idx, split_line) # then perform the additional validation ret = True # not sure why we always return true @@@ - + return ret def Partial_validate_step_pass_2(self, ret, btn, idx, split_line): ret = super().Partial_validate_step_pass_2(ret, btn, idx, split_line) # perform the original pass 2 validation - + if (ret == None or ((type(ret) == bool) and ret)): # if the original validation hasn't raised an error if self.label_preceeds and btn.symbols[SYM_LABELS][split_line[1]] > idx: # If the label must preceed the command, ensure that it is so! ret = ("Line:" + str(idx+1) + " - Target for " + self.name + " (" + split_line[1] + ") must preceed the command.", btn.Line(idx)) - + return ret - + def Partial_run_step_info(self, ret, btn, idx, split_line): ret = super().Partial_run_step_info(ret, btn, idx, split_line) # perform the original notification of a partial execution - + if self.valid_function == None or self.valid_function(btn): # if no validation function, or it returns true, continue if self.test_function and self.next_function: # if there is a test function and it returns true if btn.symbols[SYM_REPEATS][idx] > 0: # if repeats remain print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " " + str(btn.symbols[SYM_REPEATS][idx]) + " repeats left.") else: # if no repeats remain - print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " No repeats left, not repeating.") + print(AM_PREFIX.format(self.lib, btn.coords, str(idx+1)) + " No repeats left, not repeating.") else: print(self.invalid_message) return ret - + def Process(self, btn, idx, split_line): ret = idx+1 # if all else fails! if self.valid_function == None or self.valid_function(btn): # if no validation function, or it returns true, continue - + if self.next_function: # if we can calc the next value of the loop val = btn.symbols[SYM_REPEATS][idx] # get this value btn.symbols[SYM_REPEATS][idx] = self.next_function(val) # and calculate the next - + if not (self.test_function or self.next_function): # it's either both or none at the moment, if neither ret = btn.symbols[SYM_LABELS][btn.symbols[SYM_PARAMS][1]] # this is unconditional elif (self.test_function and self.next_function): # if both, then we can do the test @@ -143,32 +143,32 @@ def Process(self, btn, idx, split_line): self.Reset(btn, idx) # potential reset if it doesn't return ret - + def Valid_key_pressed(self, btn): return lp_events.pressed[btn.x][btn.y] # Is the button pressed - - + + def Valid_key_unpressed(self, btn): return not self.Valid_key_pressed(btn) # is the button unpressed - - + + def Test_func_ge_zero(self, val): # testing for a value >= 0 return val >= 0 - - + + def Next_decrement(self, val): # Standard decrement function return val-1 - - + + def Reset(self, btn, idx): btn.symbols[SYM_REPEATS][idx] = btn.symbols[SYM_ORIGINAL][idx] # standard function to reset a loop counter - + def Init_n(self, btn, idx, split_line): btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2]) # set repeats to n (will cause n+1 loop executions) self.Reset(btn, idx) - + def Init_n_minus_1(self, btn, idx, split_line): # set repeats to n-1 (will cause n loop executions) btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 @@ -182,19 +182,19 @@ def Init_n_minus_1(self, btn, idx, split_line): # set repeats # class that defines the GOTO_LABEL command class Control_Goto_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Label", False, AVV_NO, PT_LABEL, None, None), + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Goto label {1}"), + (1, " Goto label {1}"), ) ) # don't even need the additional parameters! @@ -208,19 +208,19 @@ def __init__( # class that defines the IF_PRESSED_GOTO_LABEL command class Control_If_Pressed_Goto_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) - (1, " if pressed goto label {1}"), + (1, " if pressed goto label {1}"), ), "the button is not pressed", self.Valid_key_pressed @@ -237,19 +237,19 @@ def __init__( # class that defines the IF_UNPRESSED_GOTO_LABEL command class Control_If_Unpressed_Goto_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ), ( # num params, format string (trailing comma is important) - (1, " if unpressed goto label {1}"), + (1, " if unpressed goto label {1}"), ), "the button is pressed", self.Valid_key_unpressed @@ -266,20 +266,20 @@ def __init__( # class that defines the REPEAT_LABEL command class Control_Repeat_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("Label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " Repeat label {1}, {2} times max"), + (2, " Repeat label {1}, {2} times max"), ), None, None, @@ -298,25 +298,25 @@ def __init__( # ### CLASS Control_Repeat ### # ################################################## -# class that defines the REPEAT command. This operates more like a +# class that defines the REPEAT command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end class Control_Repeat(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "REPEAT", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("Label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " Repeat {1}, {2} times max"), + (2, " Repeat {1}, {2} times max"), ), None, None, @@ -338,20 +338,20 @@ def __init__( # class that defines the IF_PRESSED_REPEAT_LABEL command. class Control_If_Pressed_Repeat_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " If key is pressed repeat label {1}, {2} times max"), + (2, " If key is pressed repeat label {1}, {2} times max"), ), "the button is not pressed", self.Valid_key_pressed, @@ -370,25 +370,25 @@ def __init__( # ### CLASS Control_If_Pressed_Repeat ### # ################################################## -# class that defines the IF_PRESSED command. This operates more like a +# class that defines the IF_PRESSED command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end class Control_If_Pressed_Repeat(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " If key is not pressed repeat label {1}, {2} times max"), + (2, " If key is not pressed repeat label {1}, {2} times max"), ), "the button is not pressed", self.Valid_key_pressed, @@ -410,20 +410,20 @@ def __init__( # class that defines the IF_UNPRESSED_REPEAT_LABEL command. class Control_If_Unpressed_Repeat_Label(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_UNPRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " If key is not pressed repeat label {1}, {2} times max"), + (2, " If key is not pressed repeat label {1}, {2} times max"), ), "the button is pressed", self.Valid_key_unpressed, @@ -442,25 +442,25 @@ def __init__( # ### CLASS Control_If_Unpressed_Repeat ### # ################################################## -# class that defines the IF_UNPRESSED_REPEAT command. This operates more like a +# class that defines the IF_UNPRESSED_REPEAT command. This operates more like a # traditional repeat/until by causing the code to repeat n times (rather than # n+1, and it resets the counter at the end class Control_If_Unpressed_Repeat(Control_Flow_Basic): def __init__( - self + self ): super().__init__( "IF_UNPRESSED_REPEAT", # the name of the command as you have to enter it in the code LIB, ( - # desc opt var type p1_val p2_val - ("label", False, AVV_NO, PT_LABEL, None, None), + # desc opt var type p1_val p2_val + ("label", False, AVV_NO, PT_LABEL, None, None), ("Repeats", False, AVV_NO, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " If key is not pressed repeat {1}, {2} times max"), + (2, " If key is not pressed repeat {1}, {2} times max"), ), "the button is pressed", self.Valid_key_unpressed, @@ -470,7 +470,7 @@ def __init__( self.Next_decrement, self.Test_func_ge_zero ) - + scripts.Add_command(Control_If_Unpressed_Repeat()) # register the command @@ -481,11 +481,11 @@ def __init__( # class that defines the RESET_REPEATS command # -# Here's a command that could just be defined into action, but the +# Here's a command that could just be defined into action, but the # basic implementation using the low level interface is so simple. class Control_Reset_Repeats(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("RESET_REPEATS") # the name of the command as you have to enter it in the code @@ -534,14 +534,14 @@ def Run( # This is really like a comment that returns the next line as -1 class Control_End(command_base.Command_Text_Basic): def __init__( - self, + self, ): super().__init__("END", # the name of the command as you have to enter it in the code LIB, "SCRIPT ENDED" ) - - + + def Process(self, btn, idx, split_line): return -1 @@ -558,9 +558,9 @@ def Process(self, btn, idx, split_line): # This is effectively the same as END, but the message (and the implication) is different class Control_Abort(Control_End): def __init__( - self, + self, ): - + super().__init__() self.name = "ABORT" diff --git a/commands_external.py b/commands_external.py index abe43a7..ea0c34c 100644 --- a/commands_external.py +++ b/commands_external.py @@ -10,7 +10,7 @@ # class that defines the WEB command. @@@ this should be updated to use the more modern interface class External_Web(command_base.Command_Text_Basic): def __init__( - self, + self, ): super().__init__( @@ -25,11 +25,11 @@ def __init__( def Partial_run_step_get(self, ret, btn, idx, split_line): # This gets the values as normal, then modifies them as required ret = super().Partial_run_step_get(ret, btn, idx, split_line) - + link = split_line[1] if "http" not in link: - split_line[1] = "http://" + link - + split_line[1] = "http://" + link + return ret @@ -44,10 +44,10 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Web_New ### # ################################################## -# class that defines the WEB_NEW command. @@@ this should be updated to use the more modern interface +# class that defines the WEB_NEW command. @@@ this should be updated to use the more modern interface class External_Web_New(External_Web): def __init__( - self, + self, ): super().__init__() @@ -70,7 +70,7 @@ def Process(self, btn, idx, split_line): # class that defines the OPEN command. @@@ this should be updated to use the more modern interface class External_Open(command_base.Command_Text_Basic): def __init__( - self, + self, ): super().__init__( @@ -93,10 +93,10 @@ def Process(self, btn, idx, split_line): # ### CLASS External_Sound ### # ################################################## -# class that defines the SOUND command (plays a sound file). @@@ this should be updated to use the more modern interface +# class that defines the SOUND command (plays a sound file). @@@ this should be updated to use the more modern interface class External_Sound(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("SOUND") # the name of the command as you have to enter it in the code @@ -142,29 +142,29 @@ def Run( # ### CLASS External_Sound_STOP ### # ################################################## -# class that defines the SOUND_STOP command (stops sound) +# class that defines the SOUND_STOP command (stops sound) class External_Sound_Stop(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( "SOUND_STOP", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Fade value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (0, " Stopping sounds immediately"), - (1, " Stopping sounds with {1} milliseconds fadeout time"), + (0, " Stopping sounds immediately"), + (1, " Stopping sounds with {1} milliseconds fadeout time"), ) ) def Process(self, btn, idx, split_line): delay = btn.symbols[SYM_PARAMS][1] # @@@ update this - + if delay == None or delay <= 0: sound.stop() else: @@ -181,7 +181,7 @@ def Process(self, btn, idx, split_line): # class that defines the CODE command (runs something). @@@ this should be updated to use the more modern interface class External_Code(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("CODE") # the name of the command as you have to enter it in the code @@ -227,32 +227,32 @@ def Run( # ### CLASS External_Code_NOWAIT ### # ################################################## -# class that defines the CODE_NOWAIT command (runs something). This returns immediately +# class that defines the CODE_NOWAIT command (runs something). This returns immediately class External_Code_Nowait(command_base.Command_Basic): def __init__( self, - ): - + ): + super().__init__("CODE_NOWAIT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("PID", False, AVV_REQD, PT_INT, None, None), # variable to get PID of new process ("Command", False, AVV_NO, PT_STRS, None, None), # text of command ), ( # num params, format string (trailing comma is important) - (2, " Run {2} retuning PID in {1}"), + (2, " Run {2} retuning PID in {1}"), ) ) - + def Process(self, btn, idx, split_line): args = [] for i in range(2, self.Param_count(btn)+1): - args += [self.Get_param(btn, i)] # get the command we want to run + args += [self.Get_param(btn, i)] # get the command we want to run - pid = -1 + pid = -1 try: - proc = subprocess.Popen(args) + proc = subprocess.Popen(args) pid = proc.pid except Exception as e: print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Error with running code: " + str(e)) diff --git a/commands_header.py b/commands_header.py index eafb295..ceda43c 100644 --- a/commands_header.py +++ b/commands_header.py @@ -7,7 +7,7 @@ class Header_Async(command_base.Command_Header): def __init__( - self, + self, ): super().__init__("@ASYNC", # the name of the header as you have to enter it in the code @@ -20,7 +20,7 @@ def Validate( split_line, # The current line, split pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - + if pass_no == 1: if idx > 0: # headers normally have to check the line number return ("Line:" + str(idx+1) + " - " + self.name + " must appear on the first line.", btn.Line(0)) @@ -50,7 +50,7 @@ def Run( class Header_Simple(command_base.Command_Header): def __init__( - self, + self, ): super().__init__("@SIMPLE", # the name of the header as you have to enter it in the code @@ -120,7 +120,7 @@ def Run( # Loads a new layout. @@@ This should probably be rewritten in the newest style class Header_Load_Layout(command_base.Command_Header): def __init__( - self, + self, ): super().__init__("@LOAD_LAYOUT", # the name of the header as you have to enter it in the code diff --git a/commands_keys.py b/commands_keys.py index 1359c8a..a85af2a 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -10,7 +10,7 @@ # class that defines the WAIT_PRESSED command (wait while a button is pressed?) class Keys_Wait_Pressed(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( @@ -28,9 +28,9 @@ def Process(self, btn, idx, split_line): while lp_events.pressed[btn.x][btn.y]: sleep(DELAY_EXIT_CHECK) if btn.Check_kill(): - return idx + 1 + return idx + 1 - return idx + 1 + return idx + 1 scripts.Add_command(Keys_Wait_Pressed()) # register the command @@ -43,23 +43,23 @@ def Process(self, btn, idx, split_line): # class that defines the TAP command (tap button a button) class Keys_Tap(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( "TAP", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, AVV_NO, PT_KEY, None, None), - ("Times", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), - ("Duration", True, AVV_YES, PT_FLOAT, variables.Validate_ge_zero, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), + ("Times", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), + ("Duration", True, AVV_YES, PT_FLOAT, variables.Validate_ge_zero, None), ), ( # num params, format string (trailing comma is important) - (1, " Tap key {1}"), - (2, " Tap key {1}, {2} times"), - (3, " Tap key {1}, {2} times for {3} seconds each"), + (1, " Tap key {1}"), + (2, " Tap key {1}, {2} times"), + (3, " Tap key {1}, {2} times for {3} seconds each"), ) ) @@ -68,33 +68,33 @@ def Process(self, btn, idx, split_line): key = kb.sp(self.Get_param(btn, 1)) # what key? releasefunc = lambda: None # default is no release function - taps = self.Get_param(btn, 2, 1) # Assume 1 tap unless we are told there's more + taps = self.Get_param(btn, 2, 1) # Assume 1 tap unless we are told there's more delay = 0 # assume no delay unless we're told there is one if cnt == 3: delay = self.Get_param(btn, 3) releasefunc = lambda: kb.release(key) # and in this case we'll also need to set up a lambda to release it - + precheck = delay == 0 and taps > 1 # we need to check if there's no delay and (possibly many) taps for tap in range(taps): # for each tap if btn.Check_kill(releasefunc): # see if we've been killed return idx+1 # @@@ shouldn't this be -1? - + if delay == 0: kb.tap(key) else: kb.press(key) - + if precheck and btn.Check_kill(releasefunc): return -1 if delay > 0: if not btn.Safe_sleep(delay, releasefunc): return -1 - + releasefunc() - + scripts.Add_command(Keys_Tap()) # register the command @@ -106,19 +106,19 @@ def Process(self, btn, idx, split_line): # class that defines the PRESS command (press a button) class Keys_Press(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( "PRESS", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, AVV_NO, PT_KEY, None, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Press key {1}"), + (1, " Press key {1}"), ) ) @@ -137,19 +137,19 @@ def Process(self, btn, idx, split_line): # class that defines the RELEASE command (un-press a button) class Keys_Release(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( "RELEASE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("Key", False, AVV_NO, PT_KEY, None, None), + # Desc Opt Var type p1_val p2_val + ("Key", False, AVV_NO, PT_KEY, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Release key {1}"), + (1, " Release key {1}"), ) ) @@ -168,7 +168,7 @@ def Process(self, btn, idx, split_line): # class that defines the RELEASE_ALL command (un-press all keys) class Keys_Release_All(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__( @@ -177,7 +177,7 @@ def __init__( (), ( # num params, format string (trailing comma is important) - (0, " Release all keys"), + (0, " Release all keys"), ) ) @@ -196,7 +196,7 @@ def Process(self, btn, idx, split_line): class Keys_String(command_base.Command_Text_Basic): def __init__( self ): - + super().__init__("STRING", # the name of the command as you have to enter it in the code LIB, "Type out string" ) diff --git a/commands_mouse.py b/commands_mouse.py index 6d82eb3..1b22d66 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -17,21 +17,21 @@ def __init__( LIB, # the name of this module ( # description of parameters # Desc Opt Var type p1_val p2_val (trailing comma is important) - ("X value", False, AVV_NO, PT_INT, None, None), + ("X value", False, AVV_NO, PT_INT, None, None), ("Y value", False, AVV_NO, PT_INT, None, None), ), - ( # How to log runtime execution + ( # How to log runtime execution # num params, format string (trailing comma is important) - (2, " Relative mouse movement ({1}, {2})"), + (2, " Relative mouse movement ({1}, {2})"), ) ) def Process(self, btn, idx, split_line): x = self.Get_param(btn, 1) y = self.Get_param(btn, 1) - + ms.move_to_pos(x, y) - + scripts.Add_command(Mouse_Move()) # register the command @@ -49,20 +49,20 @@ def __init__( super().__init__("M_SET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", False, AVV_YES, PT_INT, None, None), ("Y value", False, AVV_YES, PT_INT, None, None), ), - ( # How to log runtime execution + ( # How to log runtime execution # num params, format string (trailing comma is important) - (2, " Set mouse position to ({1}, {2})"), + (2, " Set mouse position to ({1}, {2})"), ) ) def Process(self, btn, idx, split_line): x = self.Get_param(btn, 1) y = self.Get_param(btn, 2) - + ms.set_pos(x, y) @@ -82,21 +82,21 @@ def __init__( super().__init__("M_SCROLL", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Scroll amount", True, AVV_NO, PT_INT, None, None), ("X value", False, AVV_YES, PT_INT, None, None), ), - ( # How to log runtime execution + ( # How to log runtime execution # num params, format string (trailing comma is important) - (1, " Scroll {1}"), - (2, " Scroll ({1}, {2})"), + (1, " Scroll {1}"), + (2, " Scroll ({1}, {2})"), ) ) def Process(self, btn, idx, split_line): s = self.Get_Param(btn, 1) x = self.Get_param(btn, 2, 0) - + ms.scroll(x, s) @@ -116,19 +116,19 @@ def __init__( super().__init__("M_LINE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X1 value", False, AVV_YES, PT_INT, None, None), - ("Y1 value", False, AVV_YES, PT_INT, None, None), - ("X2 value", False, AVV_YES, PT_INT, None, None), - ("Y2 value", False, AVV_YES, PT_INT, None, None), - ("Wait value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), + # Desc Opt Var type p1_val p2_val + ("X1 value", False, AVV_YES, PT_INT, None, None), + ("Y1 value", False, AVV_YES, PT_INT, None, None), + ("X2 value", False, AVV_YES, PT_INT, None, None), + ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("Wait value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), ("Skip value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), - ( # How to log runtime execution + ( # How to log runtime execution # num params, format string (trailing comma is important) - (4, " Mouse line from ({1}, {2}) to ({3}, {4})"), - (5, " Mouse line from ({1}, {2}) to ({3}, {4}) and wait {5}ms between steps"), - (6, " Mouse line from ({1}, {2}) to ({3}, {4}) by {6} pixels per step and wait {5}ms between steps"), + (4, " Mouse line from ({1}, {2}) to ({3}, {4})"), + (5, " Mouse line from ({1}, {2}) to ({3}, {4}) and wait {5}ms between steps"), + (6, " Mouse line from ({1}, {2}) to ({3}, {4}) by {6} pixels per step and wait {5}ms between steps"), ) ) @@ -172,17 +172,17 @@ def __init__( super().__init__("M_LINE_MOVE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val - ("X value", False, AVV_YES, PT_INT, None, None), - ("Y value", False, AVV_YES, PT_INT, None, None), - ("Wait value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), + # Desc Opt Var type p1_val p2_val + ("X value", False, AVV_YES, PT_INT, None, None), + ("Y value", False, AVV_YES, PT_INT, None, None), + ("Wait value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ("Skip value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), ), ( # num params, format string (trailing comma is important) - (2, " Mouse line move relative ({1}, {2})"), - (3, " Mouse line move relative ({1}, {2}) and wait {3}ms between steps"), - (4, " Mouse line move relative ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), + (2, " Mouse line move relative ({1}, {2})"), + (3, " Mouse line move relative ({1}, {2}) and wait {3}ms between steps"), + (4, " Mouse line move relative ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), ) ) @@ -192,7 +192,7 @@ def Process(self, btn, idx, split_line): delay = float(self.Get_param(btn, 3)) / 1000.0 skip = int(self.Get_param(btn, 4, 1)) - + x_C, y_C = ms.get_pos() x_N, y_N = x_C + self.Get_param(btn, 1), y_C + self.Get_param(btn, 2) points = ms.line_coords(x_C, y_C, x_N, y_N) @@ -225,7 +225,7 @@ def __init__( super().__init__("M_LINE_SET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", False, AVV_YES, PT_INT, None, None), ("Y value", False, AVV_YES, PT_INT, None, None), ("Wait value", True, AVV_YES, PT_INT, variables.Validate_ge_zero, None), @@ -233,9 +233,9 @@ def __init__( ), ( # num params, format string (trailing comma is important) - (2, " Mouse set line ({1}, {2})"), - (3, " Mouse set line ({1}, {2}) and wait {3}ms between steps"), - (4, " Mouse set line ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), + (2, " Mouse set line ({1}, {2})"), + (3, " Mouse set line ({1}, {2}) and wait {3}ms between steps"), + (4, " Mouse set line ({1}, {2}) by {4} pixels per step and wait {3}ms between steps"), ) ) @@ -274,15 +274,15 @@ def __init__( super().__init__("M_RECALL_LINE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Wait value", True , AVV_YES, PT_INT, variables.Validate_ge_zero, None), ("Skip value", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), ), ( # num params, format string (trailing comma is important) - (0, " Recall mouse position () in a line"), - (1, " Recall mouse position () in a line and wait {1} milliseconds between each step"), - (2, " Recall mouse position () in a line by {2} pixels per step and wait {1} milliseconds between each step"), + (0, " Recall mouse position () in a line"), + (1, " Recall mouse position () in a line and wait {1} milliseconds between each step"), + (2, " Recall mouse position () in a line by {2} pixels per step and wait {1} milliseconds between each step"), ) ) @@ -331,20 +331,20 @@ def __init__( super().__init__("M_STORE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", True , AVV_REQD, PT_INT, None, None), ("Y value", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (0, " Store mouse position"), - (2, " Store mouse position in variables ({1}, {2})"), + (0, " Store mouse position"), + (2, " Store mouse position in variables ({1}, {2})"), ) ) def Process(self, btn, idx, split_line): mpos = ms.get_pos() - + if self.Has_param(btn, 1): # do we have a parameter 1? self.Set_param(btn, 1, mpos[0]) # store into first and second patrameters self.Set_param(btn, 2, mpos[1]) diff --git a/commands_pause.py b/commands_pause.py index a387fb3..27ddde1 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -9,7 +9,7 @@ # class that defines the Delay command (a target of GOTO's etc) class Pause_Delay(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("DELAY") # the name of the command as you have to enter it in the code diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 25f08f7..c705359 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -1,10 +1,11 @@ import command_base, lp_events, scripts, variables, sys from constants import * +from param_convs import * LIB = "cmds_rpnc" # name of this library (for logging) # note that if you don't like RPN and prefer to write algebraic expressions -# all you need to do is create a command that converts algebraic commands to +# all you need to do is create a command that converts algebraic commands to # postfix (RPN) and you can use the RPN evaluator to process it. # ################################################## @@ -13,18 +14,18 @@ # class that defines the RPN_EVAL command. # This command allows math to be performed on a simulated RPN calculator. -# This is useful because as a stack model it also provides the framework for +# This is useful because as a stack model it also provides the framework for # passing parameters to and from other routines if the stack is preserved # in the symbol table. In this version The output is to the log, but it # is easily extended. class Rpn_Eval(command_base.Command_Basic): def __init__( - self, + self, ): super().__init__("RPN_EVAL", # the name of the command as you have to enter it in the code LIB) - + # this command does not have a standard list of fields, so we need to do some stuff manually self.valid_max_params = 255 # There is no maximum, but this is a reasonable limit! self.valid_num_params = [1, None] # one or more is OK @@ -38,21 +39,21 @@ def __init__( # Now register the operators self.Register_operators() - + # We can simply override the first pass validation def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): # validate the number of parameters - ret = self.Validate_param_count(ret, btn, idx, split_line) + ret = self.Validate_param_count(ret, btn, idx, split_line) if ((type(ret) == bool) and ret): c_len = len(split_line) # Number of tokens i = 1 while i < c_len: # for each item of the line of tokens cmd = split_line[i] # get the current one - + n = None try: - n = float(cmd) # we'll be happy with a float (since an int is a subset) + n = float(cmd) # we'll be happy with a float (since an int is a subset) except ValueError: pass else: @@ -71,30 +72,30 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): return ("Line:" + str(idx+1) + " - parameter#" + str(p+1) + " '" + param + "' of operator #" + str(i) + " '" + cmd + " must start with alpha character in " + self.name, btn.Line(idx)) i = i + 1 + self.operators[opr][1] # pull of additional parameters if required if i > c_len: - return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) + return ("Line:" + str(idx+1) + " - Insufficient parameters after operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) else: # if invalid, report it - return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) - + return ("Line:" + str(idx+1) + " - Invalid operator #" + str(i) + " '" + cmd + "' in " + self.name, btn.Line(idx)) + return ret - - # define how to process. We could override something at a lower level, but + + # define how to process. We could override something at a lower level, but # this retains any initialisation and finalization and simplifies return # requirements def Process(self, btn, idx, split_line): print("[" + self.lib + "] " + btn.coords + " Line:" + str(idx+1) + " " + self.name + ": ", split_line[1:]) # btn.coords is the text "(x, y)" i = 1 # using a loop counter rather than an itterator because it's hard to pass iters as params - + while i < len(split_line): # for each item of the line of tokens cmd = split_line[i] # get the current one - + n = None # what we get if it's not a number try: - n = int(cmd) # is it an integer? + n = int(cmd) # is it an integer? except ValueError: try: - n = float(cmd) # how about a float? + n = float(cmd) # how about a float? except ValueError: pass @@ -108,11 +109,11 @@ def Process(self, btn, idx, split_line): try: # capture the return value from the operator o_ret = self.operators[opr][0](btn.symbols, opr, split_line[i:]) # run it - + # boolean returns are special if type(o_ret) == bool: if o_ret: - # True just does a normal "go to next" + # True just does a normal "go to next" i = i + self.operators[opr][1] + 1 else: # but False aborts the execution of the RPN calc AND terminates the script @@ -127,7 +128,7 @@ def Process(self, btn, idx, split_line): else: # if invalid, report it print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") break - + return idx+1 # Normal default exit to the next line @@ -180,27 +181,27 @@ def Register_operators(self): self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc - def add(self, + def add(self, symbols, # the symbol table (stack, global vars, etc.) cmd, # the current command cmds): # the rest of the commands on the command line - - ret = 1 # always initialise ret to 1, because the default is to + + ret = 1 # always initialise ret to 1, because the default is to # step token by token along the expression - + a = variables.pop(symbols) # add requires 2 params, pop them off the stack... - b = variables.pop(symbols) # + b = variables.pop(symbols) # symbols[SYM_LOCAL]['last x'] = a try: c = b+a # RPN functions are defined as b (operator) a except: raise Exception("Error in addition: " + str(b) + " + " + str(a)) # error message in case of problem - + variables.push(symbols, c) # the result is pushed back on the stack return ret # and we return the number of tokens to skip (normally 1) - + def subtract(self, symbols, cmd, cmds): ret = 1 @@ -212,9 +213,9 @@ def subtract(self, symbols, cmd, cmds): c = b-a except: raise Exception("Error in subtraction: " + str(b) + " - " + str(a)) - + variables.push(symbols, c) - + return ret @@ -228,9 +229,9 @@ def multiply(self, symbols, cmd, cmds): c = b*a except: raise Exception("Error in multiplication: " + str(b) + " * " + str(a)) - + variables.push(symbols, c) - + return ret @@ -244,11 +245,11 @@ def divide(self, symbols, cmd, cmds): c = b/a except: raise Exception("Error in division: " + str(b) + " / " + str(a)) # Errors are highly possible here - + variables.push(symbols, c) - + return ret - + def i_div(self, symbols, cmd, cmds): ret = 1 @@ -260,11 +261,11 @@ def i_div(self, symbols, cmd, cmds): c = b//a except: raise Exception("Error in division: " + str(b) + " // " + str(a)) # Errors are highly possible here - + variables.push(symbols, c) - + return ret - + def mod(self, symbols, cmd, cmds): ret = 1 @@ -276,17 +277,17 @@ def mod(self, symbols, cmd, cmds): c = b%a except: raise Exception("Error in mod: " + str(b) + " % " + str(a)) # Errors are highly possible here - + variables.push(symbols, c) - + return ret - + def view(self, symbols, cmd, cmds): # view the top of the stack (typically where results are) ret = 1 print('Top of stack = ', variables.top(symbols, 1)) # we're going to peek at the top of the stack without popping - + return ret @@ -294,7 +295,7 @@ def view_s(self, symbols, cmd, cmds): # View the entire stack. Probably a debugging tool. ret = 1 print('Stack = ', symbols[SYM_STACK]) # show the entire stack - + return ret @@ -302,7 +303,7 @@ def view_l(self, symbols, cmd, cmds): # View the local variables. Probably a debugging tool. ret = 1 print('Local = ', symbols[SYM_LOCAL]) # show all local variables - + return ret @@ -311,7 +312,7 @@ def view_g(self, symbols, cmd, cmds): ret = 1 with symbols[SYM_GLOBAL][0]: # lock the globals while we do this print('Global = ', symbols[SYM_GLOBAL][1]) - + return ret @@ -324,9 +325,9 @@ def one_on_x(self, symbols, cmd, cmds): variables.push(symbols, 1/a) except: raise Exception("Error in 1/x: " + str(a)) # Errors are highly possible here - + return ret - + def int_x(self, symbols, cmd, cmds): # get the integer part of x @@ -338,9 +339,9 @@ def int_x(self, symbols, cmd, cmds): variables.push(symbols, int(a)) except: raise Exception("Error in '" + cmd + "' " + str(a)) # Errors are highly unlikely here - + return ret - + def frac_x(self, symbols, cmd, cmds): # get the fractionasl part of x @@ -352,9 +353,9 @@ def frac_x(self, symbols, cmd, cmds): variables.push(symbols, a - int(a)) except: raise Exception("Error in '" + cmd + "' " + str(a)) # Errors are highly unlikely here - + return ret - + def chs(self, symbols, cmd, cmds): ret = 1 @@ -364,9 +365,9 @@ def chs(self, symbols, cmd, cmds): variables.push(symbols, -a) except: raise Exception("Error in chs: " + str(a)) # Errors are highly improbable here - + return ret - + def sqr(self, symbols, cmd, cmds): # calculates the square @@ -378,9 +379,9 @@ def sqr(self, symbols, cmd, cmds): c = a**2 except: raise Exception("Error in squaring: " + str(a)) - + variables.push(symbols, c) - + return ret @@ -397,7 +398,7 @@ def y_to_x(self, symbols, cmd, cmds): raise Exception("Error raising: " + str(b) + " to the " + str(a) + "th power") # Errors are highly possible here variables.push(symbols, c) - + return ret @@ -405,7 +406,7 @@ def dup(self, symbols, cmd, cmds): # duplicates the value on the top of the stack ret = 1 variables.push(symbols, variables.top(symbols, 1)) - + return ret @@ -413,7 +414,7 @@ def pop(self, symbols, cmd, cmds): # removes top item from the stack ret = 1 variables.pop(symbols) - + return ret @@ -421,7 +422,7 @@ def clst(self, symbols, cmd, cmds): # clears the stack ret = 1 symbols[SYM_STACK].clear() - + return ret @@ -432,9 +433,9 @@ def last_x(self, symbols, cmd, cmds): a = symbols[SYM_LOCAL]['last x'] # attempt to get the last-x value except: a = 0 # default is zero - + variables.push(symbols, a) # and push it onto the stack - + return ret @@ -442,7 +443,7 @@ def cl_l(self, symbols, cmd, cmds): # clears the stack ret = 1 symbols[SYM_LOCAL].clear() - + return ret @@ -450,7 +451,7 @@ def stack_len(self, symbols, cmd, cmds): # returns stack length ret = 1 variables.push(symbols, len(symbols[SYM_STACK])) - + return ret @@ -470,66 +471,66 @@ def swap_x_y(self, symbols, cmd, cmds): def sto(self, symbols, cmd, cmds): # stores the value in local var if it exists, otherwise global var. If neither, creates local ret = 1 - ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? + ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? a = variables.top(symbols, 1) # will be stored from the top of the stack variables.Auto_store(v, a, symbols) # "auto store" the value - + return ret - - + + def sto_g(self, symbols, cmd, cmds): # stores the value on the top of the stack into the global variable named by the next token ret = 1 - ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? + ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? a = variables.top(symbols, 1) # will be stored from the top of the stack with symbols[SYM_GLOBAL][0]: # lock the globals variables.put(v, a, symbols[SYM_GLOBAL][1]) # and store it there - + return ret - - + + def sto_l(self, symbols, cmd, cmds): # stores the value on the top of the stack into the local variable named by the next token ret = 1 - ret, v = variables.next_cmd(ret, cmds) + ret, v = variables.next_cmd(ret, cmds) a = variables.top(symbols, 1) variables.put(v, a, symbols[SYM_LOCAL]) - + return ret def rcl(self, symbols, cmd, cmds): # recalls a variable. Try local first, then global ret = 1 - ret, v = variables.next_cmd(ret, cmds) - with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - a = variables.get(v, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1]) + ret, v = variables.next_cmd(ret, cmds) + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + a = variables.get(v, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1], _int) # as an integer variables.push(symbols, a) - + return ret def rcl_l(self, symbols, cmd, cmds): # recalls a local variable (not overly useful, but avoids ambiguity) ret = 1 - ret, v = variables.next_cmd(ret, cmds) - a = variables.get(v, symbols[SYM_LOCAL], None) + ret, v = variables.next_cmd(ret, cmds) + a = variables.get(v, symbols[SYM_LOCAL], None, _int) # as an integer variables.push(symbols, a) - + return ret - + def rcl_g(self, symbols, cmd, cmds): # recalls a global variable (useful if you define an identical local var) ret = 1 - ret, v = variables.next_cmd(ret, cmds) - with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - a = variables.get(v, None, symbols[SYM_GLOBAL][1]) # grab the value from the global vars - variables.push(symbols, a) # and push onto the stack - + ret, v = variables.next_cmd(ret, cmds) + with symbols[SYM_GLOBAL][0]: # lock the globals while we do this + a = variables.get(v, None, symbols[SYM_GLOBAL][1], _int) # grab the value from the global vars as an integer + variables.push(symbols, a) # and push onto the stack + return ret - + def x_eq_zero(self, symbols, cmd, cmds): # only continues eval if the top of the stack is 0 if variables.top(symbols, 1) == 0: @@ -679,13 +680,13 @@ def __init__( super().__init__("RPN_SET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Variable", False, AVV_REQD, PT_STR, None, None), ("Value", False, AVV_YES, PT_STRS, None, None), ), ( # num params, format string (trailing comma is important) - (2, " Assign '{2}' to variable {1}"), + (2, " Assign '{2}' to variable {1}"), ) ) @@ -693,7 +694,7 @@ def Process(self, btn, idx, split_line): val = '' for i in range(2, self.Param_count(btn)+1): # for each parameter (after the first) val += str(self.Get_param(btn, i)) # append all the values (force to string) - self.Set_param(btn, 1, val) # pass the combined string back + self.Set_param(btn, 1, val) # pass the combined string back + - scripts.Add_command(Rpn_Set()) # register the command \ No newline at end of file diff --git a/commands_scrape.py b/commands_scrape.py index ed25f61..49f4875 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -23,15 +23,15 @@ class Command_Scrape(commands_win32.Command_Win32): # scrapes an image relative to a window def get_image(self, hwnd, p_from, p_to): state = self.restore_window(hwnd, True) # restore the window in question and make it FG - try: - p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords - p_to = win32gui.ClientToScreen(hwnd, p_to) + try: + p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords + p_to = win32gui.ClientToScreen(hwnd, p_to) box = p_from + p_to # make a tuple with both coords image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image #image.save('C:/temp/temp.png') finally: self.reset_window(state) # restore windows something like previous states - + return image # return the image @@ -48,7 +48,7 @@ def __init__( super().__init__("S_OCR", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X1 value", False, AVV_YES, PT_INT, None, None), ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), @@ -58,24 +58,24 @@ def __init__( ), ( # num params, format string (trailing comma is important) - (5, " OCR current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " OCR form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (5, " OCR current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " OCR form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), ) ) def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords - p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using - + image = self.get_image(hwnd, p_from, p_to) # capture an image txt = pytesseract.image_to_string(image) # OCR the image - - self.Set_param(btn, 5, txt) # pass the text back - + self.Set_param(btn, 5, txt) # pass the text back + + scripts.Add_command(Scrape_OCR_Form_Text()) # register the command @@ -92,7 +92,7 @@ def __init__( super().__init__("S_HASH", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X1 value", False, AVV_YES, PT_INT, None, None), ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), @@ -102,15 +102,15 @@ def __init__( ), ( # num params, format string (trailing comma is important) - (5, " Hash of current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " Hash of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (5, " Hash of current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " Hash of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), ) ) def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords - p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using image = self.get_image(hwnd, p_from, p_to) # capture an image @@ -121,10 +121,10 @@ def Process(self, btn, idx, split_line): data = memf.getvalue() # get the data m.update(data) # put it in the hash object hash = m.hexdigest() # calculate the md5 hash - - self.Set_param(btn, 5, hash) # pass the hash back - + self.Set_param(btn, 5, hash) # pass the hash back + + scripts.Add_command(Scrape_Image_Hash()) # register the command @@ -141,7 +141,7 @@ def __init__( super().__init__("S_FINGERPRINT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X1 value", False, AVV_YES, PT_INT, None, None), ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), @@ -151,20 +151,20 @@ def __init__( ), ( # num params, format string (trailing comma is important) - (5, " Fingerprint of current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " Fingerprint of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (5, " Fingerprint of current form from ({1}, {2}) to ({3}, {4}) into {5}"), + (6, " Fingerprint of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), ) ) def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords - p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - + p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords + hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using state = self.restore_window(hwnd, True) # restore the window in question and make it FG - try: - p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords - p_to = win32gui.ClientToScreen(hwnd, p_to) + try: + p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords + p_to = win32gui.ClientToScreen(hwnd, p_to) box = p_from + p_to # make a tuple with both coords image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image image.save('C:/temp/temp.png') @@ -172,10 +172,10 @@ def Process(self, btn, idx, split_line): self.reset_window(state) # restore windows something like previous states fingerprint = int(str(imagehash.dhash(image)),16) # calculate an image fingerprint - - self.Set_param(btn, 5, fingerprint) # pass the hash back - + self.Set_param(btn, 5, fingerprint) # pass the hash back + + scripts.Add_command(Scrape_Image_Fingerprint()) # register the command @@ -192,24 +192,24 @@ def __init__( super().__init__("S_FDIST", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("F1", False, AVV_YES, PT_INT, None, None), ("F2", False, AVV_YES, PT_INT, None, None), ("Distance", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), + (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) def Process(self, btn, idx, split_line): f1 = self.Get_param(btn, 1) # get the fingerprints f2 = self.Get_param(btn, 2) - + dist = dhash.get_num_bits_different(f1, f2) # hamming distance (number of bits different) - - self.Set_param(btn, 3, dist) # pass the distance back - + self.Set_param(btn, 3, dist) # pass the distance back + + scripts.Add_command(Scrape_Fingerprint_Distance()) # register the command diff --git a/commands_subroutines.py b/commands_subroutines.py index 00ebb52..3b75680 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -16,7 +16,7 @@ # but is ignored in the normal running of commands class Header_Subroutine(command_base.Command_Header): def __init__( - self, + self, ): super().__init__("@SUB", # the name of the header as you have to enter it in the code @@ -64,49 +64,49 @@ def __init__( Name, # The name of the command Params, # The parameter tuple Lines # The text of the subroutine/function - ): - + ): + super().__init__(SUBROUTINE_PREFIX + Name, # the name of the command as you have to enter it in the code LIB, Params, ( # num params, format string (trailing comma is important) - (0, " Call "+ Name), + (0, " Call "+ Name), ) ) - + self.routine = Lines # the routine to execute self.btn = scripts.Button(-1, -1, self.routine) # we retain this so we only have to validate it once. executions use a deep-ish copy - - + + # process for a subroutine handles parameter passing and then passes off the process to the script in a "dummy" button def Process(self, btn, idx, split_line): sub_btn = scripts.Button(-1, -1, self.routine, btn.root) # create a new button and pass the script to it - + self.btn.Copy_parsed(sub_btn, self.name) # copy the info created when parsed - + variables.Local_store('sub__np', self.Param_count(btn), sub_btn.symbols) # number of parameters passed - + d = variables.Local_recall('sub__d',btn.symbols) # get current call depth variables.Local_store('sub__d', d+1, sub_btn.symbols) # and pass that + 1 - + #@@@ will fail with multiple parameters at the end! #Which is not so much of a problem because there is no way to name or retrieve them currently - + for n in range(self.Param_count(btn)): # for all the params passed pn = self.Get_param(btn, n+1) # get the param variables.Local_store(self.auto_validate[n][AV_DESCRIPTION], pn, sub_btn.symbols) # and store it sub_btn.Run_subroutine() - + for n in range(self.Param_count(btn)): # for all the params passed if self.auto_validate[n][AV_VAR_OK] == AVV_REQD: # if this is passed by reference pn = variables.Local_recall(self.auto_validate[n][AV_DESCRIPTION], sub_btn.symbols) # get the variable self.Set_param(btn, n+1, pn) # and store it - - + + # This is not the parse routine called for validation! def Parse_Sub(self): - try: + try: script_validate = self.btn.Parse_script() except: self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") @@ -119,9 +119,9 @@ def Parse_Sub(self): def Get_Name_And_Params(lines, sub_n, fname): - # Have we found a line with @SUB? + # Have we found a line with @SUB? found = False - + for lin_num, line in enumerate(lines): line = line.strip() if line == '' or line[0] == '-': @@ -131,8 +131,8 @@ def Get_Name_And_Params(lines, sub_n, fname): else: found = True break - - # Not finding the header is a bad problem + + # Not finding the header is a bad problem if not found: return '', f'Error - Subroutine has no content up to line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num @@ -141,32 +141,32 @@ def Get_Name_And_Params(lines, sub_n, fname): if len(sline) < 2: return '', f'Error - Subroutine does not have a name on line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num - # this is the name + # this is the name name = sline[1] if name != name.upper(): return '', f'Error - Subroutine name is not UPPERCASE on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num - - # now work on the parameters + + # now work on the parameters params = () # for each parameter for p_num, param in enumerate(sline[2:]): mods = '' # No modifiers yet - var = param # so everything else is the variable name - + var = param # so everything else is the variable name + # first find leading modifiers for c in param: if not variables.valid_var_name('A'+c): mods += c - var = var[1:] + var = var[1:] else: break - + # next look for anything after a "+" p = var.find('+') if p >= 0: mods += var[p+1:] - var = var[:p] + var = var[:p] elif not variables.valid_var_name(var): # if there's no '+', look for trailing modifiers without one l = len(var) while l > 1: @@ -177,65 +177,65 @@ def Get_Name_And_Params(lines, sub_n, fname): l -= 1 if var == '' or not variables.valid_var_name(var): - return name, f'Error - Parameter "{var}" is not a valid name on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num - + return name, f'Error - Parameter "{var}" is not a valid name on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num + # Standardise modifiers mods = mods.upper().translate(MOD_TRANS) - # Consolidate the modifiers + # Consolidate the modifiers modc = mods.translate(MOD_CONSOLIDATE) modcs = set(modc) - + if modcs > VALID_CONSOLIDATED: e = modcs - VALID_CONSOLIDATED return name, f'Error - Invalid modifier {modcs - VALID_CONSOLIDATED} specified on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num - + # check for duplicate modifiers if len(modc) != len(set(modc)): return name, f'Error - Duplicate modifiers specified on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num - - # Desc Opt Var type p1_val p2_val + + # Desc Opt Var type p1_val p2_val prm = [var, False, AVV_YES, PT_INT, None, None] - + if mods.find('O') >= 0: prm[1] = True # parameter is optional - + if mods.find('R') >= 0: prm[2] = AVV_REQD # pass by reference is a required variable - + if mods.find('F') >= 0: prm[3] = PT_FLOAT # parameter is a float elif mods.find('S') >= 0: - prm[3] = PT_STR # parameter is a string + prm[3] = PT_STR # parameter is a string elif mods.find('K') >= 0: - prm[3] = PT_KEY # parameter is a key + prm[3] = PT_KEY # parameter is a key prm[2] = AVV_NO # must be a constant if mods.find('R') >= 0: return name, f'Error - Key cannot be passed by reference on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num elif mods.find('B') >= 0: - prm[3] = PT_BOOL # parameter is a boolean + prm[3] = PT_BOOL # parameter is a boolean params += (tuple(prm),) # add a new parameter - + return name, params, lin_num+1 # return the subroutine name, valid list of parameters, and the next line number - - + + def Add_Function(lines, sub_n, fname): - # This function is passed a list of lines. The first non-comment line must define the header - + # This function is passed a list of lines. The first non-comment line must define the header + # first let's parse out the header to get the name and the parameters name, params, lin = Get_Name_And_Params(lines, sub_n, fname) if isinstance(params, str): - return False, name, params + return False, name, params NewCommand = Subroutine(name, params, lines) # Create a new command object for this subroutine - + if NewCommand: if NewCommand.name in scripts.VALID_COMMANDS: # does this command already exist? old_cmd = scripts.VALID_COMMANDS[NewCommand.name] # get the command we will be replacing else: old_cmd = None # if not, nothing to replace - + try: scripts.Add_command(NewCommand) # Add the command before we parse (to allow recursion) script_validation = NewCommand.btn.Validate_script()# and validate with the internal btn held in the command. @@ -249,6 +249,5 @@ def Add_Function(lines, sub_n, fname): pass # @@@ there must be more to do! :-) This is the error return else: pass # @@@ this is the success return. There must be more to do! - - return True, NewCommand.name, params - \ No newline at end of file + + return True, NewCommand.name, params diff --git a/commands_win32.py b/commands_win32.py index 87f95b5..ac135cb 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -15,17 +15,17 @@ class Command_Win32(command_base.Command_Basic): def restore_window(self, hwnd, fg = False): old_hwnd = win32gui.GetForegroundWindow() # save the current window place = win32gui.GetWindowPlacement(hwnd) # get info about the window - + if place[1] == win32con.SW_SHOWMAXIMIZED: # if it is maximised win32gui.ShowWindow(hwnd, win32con.SW_SHOWMAXIMISED) # then keep it maximised elif place[1] == win32con.SW_SHOWMINIMIZED: # if minimised win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # then restore it else: win32gui.ShowWindow(hwnd, win32con.SW_NORMAL) # otherwise a normal show is fin - + if fg and (hwnd != old_hwnd): win32gui.SetForegroundWindow(hwnd) - + return place[1], old_hwnd, hwnd # useful if you want to minimise it again # resets windows to what they were before the restore @@ -34,20 +34,20 @@ def reset_window(self, old_state): if state == win32con.SW_SHOWMINIMIZED: # re-minimise if it was minimised win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) - + if hwnd != old_hwnd: # set fg window if it was different win32gui.SetForegroundWindow(old_hwnd) # returns a list of hwnds for a process id def get_hwnds_for_pid(self, pid): - + def callback (hwnd, hwnds): if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd): _, found_pid = win32process.GetWindowThreadProcessId(hwnd) if found_pid == pid: hwnds.append (hwnd) return True - + hwnds = [] win32gui.EnumWindows (callback, hwnds) return hwnds @@ -66,13 +66,13 @@ def __init__( super().__init__("W_GET_CARET", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", False, AVV_REQD, PT_INT, None, None), ("Y value", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (2, " Store screen absolute caret position in variables ({1}, {2})"), + (2, " Store screen absolute caret position in variables ({1}, {2})"), ) ) @@ -80,7 +80,7 @@ def get_caret(self): # get current caret position within window res = (-1, -1) # failure value - + fg_win = win32gui.GetForegroundWindow() # find the current foreground window fg_thread, fg_process = win32process.GetWindowThreadProcessId(fg_win) # get thread and process information current_thread = win32api.GetCurrentThreadId() # find the current thread @@ -115,18 +115,18 @@ def __init__( super().__init__("W_GET_FG_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("HWND", False, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Return the handle of the current foreground window into {1}"), + (1, " Return the handle of the current foreground window into {1}"), ) ) def Process(self, btn, idx, split_line): hwnd = win32gui.GetForegroundWindow() # get the current window - + self.Set_param(btn, 1, hwnd) # Return the current window @@ -146,28 +146,28 @@ def __init__( super().__init__("W_SET_FG_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("HWND", False, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Make window {1} the current window"), + (1, " Make window {1} the current window"), ) ) def Process(self, btn, idx, split_line): hwnd = self.Get_param(btn, 1) # get the window handle from the passed variable (or constant) - + old_x, old_y = ms.get_pos() # save the position of the mouse self.restore_window(hwnd) # show the window - + # positioning the mouse on the form while we make it the foreground seems to help x, y = win32gui.ClientToScreen(hwnd, (10, 10)) # get a position just inside the window ms.set_pos(x, y) # put the mouse on the form win32gui.SetForegroundWindow(hwnd) # Make the window current ms.set_pos(old_x, old_y) # restore the mouse position - + scripts.Add_command(Win32_Set_Fg_Hwnd()) # register the command @@ -184,27 +184,27 @@ def __init__( super().__init__("W_CLIENT_TO_SCREEN", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", False, AVV_REQD, PT_INT, None, None), ("Y value", False, AVV_REQD, PT_INT, None, None), ("HWND", True, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (2, " Convert form relative coord in ({1}, {2}) in curent window to screen (abs)"), - (3, " Convert form relative coord in ({1}, {2}) in window {3} to screen (abs)"), + (2, " Convert form relative coord in ({1}, {2}) in curent window to screen (abs)"), + (3, " Convert form relative coord in ({1}, {2}) in window {3} to screen (abs)"), ) ) - + def Process(self, btn, idx, split_line): x = self.Get_param(btn, 1) # get x,y value y = self.Get_param(btn, 2) - + hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window state = self.restore_window(hwnd) try: x, y = win32gui.ClientToScreen(hwnd, (x, y)) # convert client coords to screen coords - + self.Set_param(btn, 1, x) # set new x, y values self.Set_param(btn, 2, y) finally: @@ -227,29 +227,29 @@ def __init__( super().__init__("W_SCREEN_TO_CLIENT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("X value", False, AVV_REQD, PT_INT, None, None), ("Y value", False, AVV_REQD, PT_INT, None, None), ("HWND", True, AVV_YES, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (2, " Convert form absolute coord in ({1}, {2}) to relative to current window"), - (3, " Convert form absolute coord in ({1}, {2}) to relative to window {3}"), + (2, " Convert form absolute coord in ({1}, {2}) to relative to current window"), + (3, " Convert form absolute coord in ({1}, {2}) to relative to window {3}"), ) ) - + def Process(self, btn, idx, split_line): x = self.Get_param(btn, 1) # get x,y value y = self.Get_param(btn, 2) - + hwnd = self.Get_param(btn, 3, win32gui.GetForegroundWindow()) # get the window state = self.restore_window(hwnd) - try: + try: x, y = win32gui.ScreenToClient(hwnd, (x, y)) # convert client coords to screen coords - + self.Set_param(btn, 1, x) # set new x, y values - self.Set_param(btn, 2, y) + self.Set_param(btn, 2, y) finally: self.reset_window(state) @@ -265,12 +265,12 @@ def Process(self, btn, idx, split_line): class Win32_Find_Hwnd(Command_Win32): def __init__( self, - ): - + ): + super().__init__("W_FIND_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Title", False, AVV_NO, PT_STR, None, None), # name to search for ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M 0: # save to variable if required try: - win32clipboard.OpenClipboard(hwnd) + win32clipboard.OpenClipboard(hwnd) t = win32clipboard.GetClipboardData(win32con.CF_TEXT) self.Set_param(btn, 1, t) finally: @@ -370,39 +370,39 @@ def Process(self, btn, idx, split_line): class Win32_Paste(Command_Win32): def __init__( self, - ): - + ): + super().__init__("W_PASTE", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("Clipboard", True, AVV_REQD, PT_STR, None, None), # variable to contain item to paste ), ( # num params, format string (trailing comma is important) - (0, " Paste from system clipboard"), - (1, " Paste from {1} via system clipboard"), + (0, " Paste from system clipboard"), + (1, " Paste from {1} via system clipboard"), ) ) - + def Process(self, btn, idx, split_line): - + if self.Param_count(btn) > 0: # place variable into clipboard if required hwnd = win32gui.GetForegroundWindow() # get the current window - + c = self.Get_param(btn, 1) # get the value try: - win32clipboard.OpenClipboard(hwnd) + win32clipboard.OpenClipboard(hwnd) win32clipboard.EmptyClipboard() # clear the clipboard first (because that makes it work) win32clipboard.SetClipboardText(str(c)) # and put the string in the clipboard finally: win32clipboard.CloseClipboard() - - # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste + + # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste try: kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) - kb.tap(kb.sp('v')) + kb.tap(kb.sp('v')) finally: - kb.release(kb.sp('ctrl')) + kb.release(kb.sp('ctrl')) scripts.Add_command(Win32_Paste()) # register the command @@ -416,25 +416,25 @@ def Process(self, btn, idx, split_line): class Win32_Wait(Command_Win32): def __init__( self, - ): - + ): + super().__init__("W_WAIT", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("HWND", False, AVV_YES, PT_INT, None, None), # variable to contain item to paste ), ( # num params, format string (trailing comma is important) - (0, " Wait until {1} is ready for input"), + (0, " Wait until {1} is ready for input"), ) ) - + def Process(self, btn, idx, split_line): - + hwnd = self.Get_param(btn, 1) # get the window tid, pid = win32process.GetWindowThreadProcessId(hwnd) # find the pid hproc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION , False, pid) # find the process id - + res = win32con.WAIT_TIMEOUT # set the failure mode to timeout while res == win32con.WAIT_TIMEOUT: # while we're still timing out res = win32event.WaitForInputIdle(hproc, 20) # wait a little while for window to become idle @@ -453,22 +453,22 @@ def Process(self, btn, idx, split_line): class Win32_Pid_To_Hwnd(Command_Win32): def __init__( self, - ): - + ): + super().__init__("W_PID_TO_HWND", # the name of the command as you have to enter it in the code LIB, ( - # Desc Opt Var type p1_val p2_val + # Desc Opt Var type p1_val p2_val ("pid", False, AVV_YES, PT_INT, None, None), # variable containing pid ("hwnd", False, AVV_REQD, PT_INT, None, None), # variable to contain hwnd ), ( # num params, format string (trailing comma is important) - (2, " return hwnd in {2} for pid {1}"), + (2, " return hwnd in {2} for pid {1}"), ) ) - + def Process(self, btn, idx, split_line): - + pid = self.Get_param(btn, 1) # get the pid hwnds = self.get_hwnds_for_pid(pid) # find any hwnds if len(hwnds) == 1: diff --git a/constants.py b/constants.py index 1fe7edb..10362e5 100644 --- a/constants.py +++ b/constants.py @@ -1,5 +1,7 @@ # Constants used all over the place. An excuse to use "from constants import *" +from param_convs import * + # Get platform information PLATFORMS = [ {"search_string": "win", "name_string": "windows"}, {"search_string": "linux", "name_string": "linux"}, @@ -62,17 +64,17 @@ AV_P2_VALIDATION = 5 # constants for parameter types -# desc conv special last var (special means additional auto-validation, last means MUST be last, var is the max AV_VAR allowed) -PT_INT = ("int", int, False, False, AVVS_ALL) -PT_FLOAT = ("float", float, False, False, AVVS_ALL) -PT_STR = ("str", str, True, False, AVVS_ALL) # a quoted string -PT_STRS = ("strs", str, True, True, AVVS_ALL) # 1 or more quoted strings -PT_LINE = ("line", str, True, True, AVVS_NO) # the rest of the line following first preceeding whitespace -PT_TEXT = ("text", str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED -PT_LABEL = ("label", str, True, False, AVVS_NO) # Note that this is for a reference to a label, not the definition of a label! -PT_TARGET = ("target", str, True, False, AVVS_NO) # Note that this is for the definition of a target (e.g. creating a label) -PT_KEY = ("key", str, True, False, AVVS_NO) # This is a key literal -PT_BOOL = ("bool", str, True, False, AVVS_ALL) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables +# desc conv special last var (special means additional auto-validation, last means MUST be last, var is the max AV_VAR allowed) +PT_INT = ("int", _int, False, False, AVVS_ALL) +PT_FLOAT = ("float", _float, False, False, AVVS_ALL) +PT_STR = ("str", _str, True, False, AVVS_ALL) # a quoted string +PT_STRS = ("strs", _str, True, True, AVVS_ALL) # 1 or more quoted strings +PT_LINE = ("line", _str, True, True, AVVS_NO) # the rest of the line following first preceeding whitespace +PT_TEXT = ("text", _str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED +PT_LABEL = ("label", _str, True, False, AVVS_NO) # Note that this is for a reference to a label, not the definition of a label! +PT_TARGET = ("target", _str, True, False, AVVS_NO) # Note that this is for the definition of a target (e.g. creating a label) +PT_KEY = ("key", _str, True, False, AVVS_NO) # This is a key literal +PT_BOOL = ("bool", _str, True, False, AVVS_ALL) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables # constants for auto_message AM_COUNT = 0 diff --git a/files.py b/files.py index 68adaf2..413c8dd 100644 --- a/files.py +++ b/files.py @@ -50,11 +50,11 @@ def load_layout_json(name, printing=True): if printing: print("[files] Loaded layout " + name) return layout - + def load_layout_legacy(name, printing=True): layout = dict() layout["version"] = "LEGACY" - + layout["buttons"] = [] with open(name, "r") as f: l = f.readlines() @@ -64,7 +64,7 @@ def load_layout_legacy(name, printing=True): line = l[x][:-1].split(":LPHK_BUTTON_SEP:") for y in range(9): info = line[y].split(":LPHK_ENTRY_SEP:") - + color = None if not info[0].isdigit(): split = info[0].split(",") @@ -72,7 +72,7 @@ def load_layout_legacy(name, printing=True): else: color = lp_colors.code_to_RGB(int(info[0])) script_text = info[1].replace(":LPHK_NEWLINE_REP:", "\n") - + layout["buttons"][-1].append({"color": color, "text": script_text}) if printing: print("[files] Loaded legacy layout " + name) @@ -82,12 +82,12 @@ def load_layout(name, popups=True, save_converted=True, printing=True): basename_list = os.path.basename(name).split(os.path.extsep) ext = basename_list[-1] title = os.path.extsep.join(basename_list[:-1]) - + if "." + ext == LEGACY_LAYOUT_EXT: # TODO: Error checking on resultant JSON layout = load_layout_legacy(name, printing=printing) - - + + if save_converted: name = os.path.dirname(name) + os.path.sep + title + LAYOUT_EXT if popups: @@ -104,54 +104,54 @@ def load_layout(name, popups=True, save_converted=True, printing=True): if popups: window.app.popup(window.app, "Error loading file!", window.app.info_image, "The layout is not in valid JSON format (the new .lpl extention).\n\nIf this was renamed from a .LPHKlayout file, please change\nthe extention back to .LPHKlayout and try loading again.", "OK") raise - + return layout def save_lp_to_layout(name): layout = dict() - + has_subs = False - + layout["buttons"] = [] for x in range(9): layout["buttons"].append([]) for y in range(9): color = lp_colors.curr_colors[x][y] script_text = scripts.buttons[x][y].script_str - + layout["buttons"][-1].append({"color": color, "text": script_text}) - + for x in scripts.VALID_COMMANDS: # for all the commands that exist if x.startswith(SUBROUTINE_PREFIX): # if this command is a subroutine if not has_subs: layout["subroutines"] = [] # only add the key if required has_subs = True cmd = scripts.VALID_COMMANDS[x] # get the command - layout["subroutines"] += [cmd.routine] # add the command to the list (name is embedded in the subroutine) + layout["subroutines"] += [cmd.routine] # add the command to the list (name is embedded in the subroutine) - if has_subs: # file version depends on the existance of subroutines + if has_subs: # file version depends on the existance of subroutines layout["version"] = FILE_VERSION_SUBS else: layout["version"] = FILE_VERSION - + save_layout(layout=layout, name=name) def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): global curr_layout global in_error global layout_changed_since_load - + converted_to_rg = False - + scripts.Unbind_all() scripts.Unload_all() # remove all existing subroutines when you load a new layout window.app.draw_canvas() - + if preload == None: layout = load_layout(name, popups=popups, save_converted=save_converted) else: layout = preload - + # load subroutines before buttons so you don't get errors on buttons using them if "subroutines" in layout: # were subroutines saved? for sub in layout["subroutines"]: # for all the subroutines that were saved @@ -162,12 +162,12 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): button = layout["buttons"][x][y] color = button["color"] script_text = button["text"] - + if window.lp_mode == "Mk1": if color[2] != 0: color = lp_colors.RGB_to_RG(color) converted_to_rg = True - + if script_text != "": script_validation = None try: @@ -193,7 +193,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): lp_colors.update_all() window.app.draw_canvas() - + curr_layout = name if converted_to_rg: if popups: @@ -208,7 +208,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): def load_subroutines_to_lp(name, popups=True, preload=None): with open(name, 'r') as in_subs: subs = in_subs.read().split('\n===\n') - + for i, sub in enumerate(subs): load_subroutine(sub.splitlines(), i+1, name) @@ -216,7 +216,7 @@ def load_subroutines_to_lp(name, popups=True, preload=None): def load_subroutine(sub, sub_n, fname): import commands_subroutines ok, name, params = commands_subroutines.Add_Function(sub, sub_n, fname) # Attempt to load the command - + if ok: pass # @@@ there must be more to do! :-) else: diff --git a/global_vars.py b/global_vars.py new file mode 100644 index 0000000..64af7a2 --- /dev/null +++ b/global_vars.py @@ -0,0 +1,5 @@ +# Variables used all over the place. +# You must use "import global_vars" and refer to "global_vars.xxx" + +# Arguments +ARGS = dict() diff --git a/kb.py b/kb.py index bf32d8c..bd3deb1 100644 --- a/kb.py +++ b/kb.py @@ -42,7 +42,7 @@ def release_all(): for key in pressed.copy(): # for each key recorded as pressed (in no particular order) release(key) # release the key - + # tap a key def tap(key): if type(key) == str: # if it's a str diff --git a/launchpad_fake.py b/launchpad_fake.py new file mode 100644 index 0000000..a9de1bd --- /dev/null +++ b/launchpad_fake.py @@ -0,0 +1,50 @@ +# A fake connector to allow operation without a launchpad connected + +class Launchpad(): + def __init__(self): + pass + + def ButtonFlush(self): + pass + + def LedCtrlBpm(self, x): + pass + + def ButtonStateXY(self): + return [] + + def LedCtrlXYByRGB(self, x, y, z): + pass + + def LedCtrlXY(self, x, y, z, t): + pass + + def LedCtrlXYByCode(self, x, y, z): + pass + + +launchpad = Launchpad() + + +class Launchpad_Fake_Connector(): + def __init__(self): + pass + + def get_launchpad(self): + global launchpad + return launchpad + + def connect(self, lp): + return True + + def get_mode(self, lp): + return "*ALONE*" + + def get_display_name(self, lp): + return "Standalone Mode" + + def disconnect(self, lp_object): + pass + + +launchpad_fake_connector = Launchpad_Fake_Connector() diff --git a/logger.py b/logger.py index a988196..6e7d7e3 100644 --- a/logger.py +++ b/logger.py @@ -1,8 +1,8 @@ # Logger module by Ella J. (nimaid) -# +# # This module provides basic logging of STDOUT and STDERR to a text file # Usage: -# +# # import logger # logger.start("/path/to/my/log.txt") # ... @@ -18,12 +18,12 @@ def __init__(self, file_path): self._file = open(self.path, "w") self._stdout_logger = self._LoggerStdout(self._file) self._stderr_logger = self._LoggerStderr(self._file) - + def __del__(self): self._stdout_logger.__del__() self._stderr_logger.__del__() self._file.close() - + class _LoggerStdout: def __init__(self, file_in): self._file = file_in @@ -37,7 +37,7 @@ def write(self, data): self._file.flush() def flush(self): self._file.flush() - + class _LoggerStderr: def __init__(self, file_in): self._file = file_in diff --git a/lp_colors.py b/lp_colors.py index 9775ca9..091081e 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -101,7 +101,7 @@ def updateXY(x, y): #print("Update colors for (" + str(x) + ", " + str(y) + "), is_running = " + str(is_running)) if is_running: # if the button is running - set_color = scripts.COLOR_PRIMED # set the desired colour + set_color = scripts.COLOR_PRIMED # set the desired colour color_modes[x][y] = "flash" # and mode elif (x, y) in [l[1:] for l in scripts.to_run]: # is it waiting to run? if is_func_key: diff --git a/lp_events.py b/lp_events.py index 6305d33..bec5cec 100644 --- a/lp_events.py +++ b/lp_events.py @@ -33,14 +33,14 @@ def run(lp_object): pressed[x][y] = False else: # I presume this is "button pressed" pressed[x][y] = True - press_funcs[x][y](x, y) # do whatever you need to do with a pressed button + press_funcs[x][y](x, y) # do whatever you need to do with a pressed button lp_colors.updateXY(x, y) # and update the button colour else: # but if there's no event pending break # break out of the loop init(lp_object) # and schedule this button to run after the delay timer.start() -# "start" an object by initialising it then running it. +# "start" an object by initialising it then running it. def start(lp_object): lp_colors.init(lp_object) # assign this to a global (what?!) init(lp_object) # create the timer object for this lp_object (button) diff --git a/param_convs.py b/param_convs.py new file mode 100644 index 0000000..b92017d --- /dev/null +++ b/param_convs.py @@ -0,0 +1,29 @@ +# defines conversion routines for parameters + +# int conversion with sensible None handling +def _int(x): + if x == None or (isinstance(x, str) and x.strip() == ""): + return 0 + else: + return int(x) + + +# float conversion with sensible None handling +def _float(x): + if x == None or (isinstance(x, str) and x.strip() == ""): + return 0.0 + else: + return float(x) + + +# string conversion with sensible None handling +def _str(x): + if x == None: + return "" + else: + return x + + +# no conversion +def _None(x): + return x \ No newline at end of file diff --git a/scripts.py b/scripts.py index baae7d4..b004077 100644 --- a/scripts.py +++ b/scripts.py @@ -13,7 +13,7 @@ # HEADERS is likewise empty until added (all headers, not just async ones) -HEADERS = dict() +HEADERS = dict() # GLOBALS is likewise empty until global variables get created @@ -33,7 +33,7 @@ def Add_command( if a_command.name in HEADERS: # if this was previously a header, now it isn't HEADERS.pop(a_command.name) - + if a_command.name in VALID_COMMANDS: # if it already exists p = VALID_COMMANDS[a_command.name] # get it else: # otherwise @@ -45,8 +45,8 @@ def Add_command( HEADERS[a_command.name] = a_command.is_async # add it return p # return any replaced command - - + + # Remove a command. This could be useful in handling subroutines (@@@ UNTESTED) def Remove_command( @@ -55,11 +55,11 @@ def Remove_command( if command_name in HEADERS: # if this was previously a header HEADERS.pop(command_name) # remove the header - + if command_name in VALID_COMMANDS: # if it already exists VALID_COMMANDS.pop(command_name) # remove the command - - + + # Create a new symbol table. This contains information required for the script to run # it includes the locations of labels, loop counters, etc. If we implement variables # this is where we would place them @@ -69,12 +69,12 @@ def New_symbol_table(): # symbol table is dictionary of objects symbols = { SYM_REPEATS: dict(), - SYM_ORIGINAL: dict(), + SYM_ORIGINAL: dict(), SYM_LABELS: dict(), SYM_MOUSE: tuple(), SYM_GLOBAL: [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) SYM_LOCAL: dict(), # local (to the script) variables (with no lock) - SYM_STACK: [] } # script stack (for RPN_EVAL) + SYM_STACK: [] } # script stack (for RPN_EVAL) return symbols @@ -87,7 +87,7 @@ def New_symbol_table(): # A button is a class containing all that's essential for a button. class Button(): def __init__( - self, + self, x, # The button column y, # The button row script_str, # The Script @@ -108,45 +108,45 @@ def __init__( self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? else: self.coords = "(SUB)" # subroutines don't have coordinates - - # The "root" is the button that is scheduled. This allows subroutines to check if the + + # The "root" is the button that is scheduled. This allows subroutines to check if the # initiating button has been killed. if root == None: # if we are not being called self.root = self # then we are the root else: # otherwise self.root = root # the caller is the root - - - # Do what is required to parse the script. Parsing does not output any information unless it is an error + + + # Do what is required to parse the script. Parsing does not output any information unless it is an error def Parse_script(self): if self.validated: # we don't want to repeat validation over and over return True - + if self.script_lines == None: # A little setup if the script lines are not created if isinstance(self.script_str, list): # Subroutines already have this as a list of lines self.script_lines = self.script_str # Copy the lines else: # But commands just have the raw stream from a file self.script_lines = self.script_str.split('\n') # Create the lines self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks - + self.symbols = New_symbol_table() # Create a shiny new symbol table self.is_async = False # default is NOT async err = True errors = 0 # no errors found - for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, + for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, # pass 2 symbol check & assocoated processing for idx,line in enumerate(self.script_lines): # gen line number and text if self.Is_ignorable_line(line): continue # don't process ignorable lines - + cmd_txt = self.Split_cmd_text(line) # get the name of the command if cmd_txt in VALID_COMMANDS: # if first element is a command command = VALID_COMMANDS[cmd_txt]# get the command itself split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately - + if type(split_line) == tuple: if err == True: err = split_line @@ -168,28 +168,28 @@ def Parse_script(self): print('Pass ' + str(pass_no) + ' complete for button (' + str(self.x+1) + ',' + str(self.y+1) + '). ' + str(errors) + ' errors detected.') break # errors prevent next pass - return err # success or failure + return err # success or failure - # copies parsed info from self to new_btn + # copies parsed info from self to new_btn def Copy_parsed(self, new_btn, name="SUB"): new_btn.script_lines = self.script_lines # Copy the lines new_btn.coords = "(" + name + ")" # set the name - + new_btn.symbols = New_symbol_table() new_btn.symbols[SYM_REPEATS] = self.symbols[SYM_REPEATS].copy() # copy the repeats new_btn.symbols[SYM_ORIGINAL] = self.symbols[SYM_ORIGINAL].copy() # and the original values new_btn.symbols[SYM_LABELS] = self.symbols[SYM_LABELS].copy() # and the position of labels - + new_btn.is_async = self.is_async # default is NOT async - + # check "self" for death notification - def Check_self_kill(self, killfunc=None): + def Check_self_kill(self, killfunc=None): if not self.thread: print ("expecting a thread in ", self.coords) return False - + if self.thread.kill.is_set(): print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") self.thread.kill.clear() @@ -239,7 +239,7 @@ def Is_ignorable_line(self, line): def Schedule_script(self): # @@@ may be worth checking to see if it's a subroutine. Because subroutines shouldn't use this global to_run - + if self.thread != None: if self.thread.is_alive(): # @@@ The following code creates a problem if a script is looking for a second keypress @@ -269,7 +269,7 @@ def Schedule_script(self): else: print("[scripts] " + self.coords + " A script is already running, scheduling...") to_run.append((self.x, self.y)) - + lp_colors.updateXY(self.x, self.y) @@ -289,8 +289,8 @@ def Run_next(self): def Run_script_and_run_next(self): self.Run_script() self.Run_next() - - + + def Line(self, idx): if self.script_lines and idx >=0 and idx < len(self.script_lines): return self.Fix_comment(self.script_lines[idx]) @@ -321,9 +321,9 @@ def Split_text(self, command, cmd_txt, line): def split1(line): # just strip off a single (non-quoted) parameter param = line.split()[0] # get the parameter line = line[len(param):].strip() # strip off the parameter - + return param, line # return the parameter and the rest of the line - + # grab a quoted string from the line passed. Handles embedded quotes def strip_quoted(line): l2 = line # a copy of the line we can edit @@ -343,9 +343,9 @@ def strip_quoted(line): else: # for non-quote characters out += l2[0] # we just pass them through to the output string l2 = l2[1:] # and strip them off. - + return False, out, line # if we fall through, that's an error (no closing quote) - + # for all other commands, split on spaces if isinstance(command, command_base.Command_Basic): pline = line # something we can alter @@ -361,7 +361,7 @@ def strip_quoted(line): av = avl[n] # then grab it else: # otherwise the last parameter must allow for multiple values av = avl[-1] # so take the last auto-validation - + desc = av[AV_TYPE][AVT_DESC] # get the description of the parameter type (not the description of the parameter!) if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Is this one that wants quoted strings? @@ -374,7 +374,7 @@ def strip_quoted(line): sline += ['"'+param] # we'll add it as the parameter value. Note we add a leading " to distinguish it from a variable else: return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote - else: # if we want a quoted string, but value doesn't start with a quote + else: # if we want a quoted string, but value doesn't start with a quote if av[AV_VAR_OK] != AVV_NO: # Are we allowed to pass a variable? param = pline.split()[0] # then that's OK, just strip off an un-quoted string pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) @@ -382,20 +382,20 @@ def strip_quoted(line): return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... else: sline += [param] # add it to the list of parameters if it's OK - else: + else: return ('Error starting quoted string for param#' + str(n+1), line) # This is generally a missing initial quote - + elif desc == PT_LINE[AVT_DESC]: # the rest of the line (regardless of spaces) sline += [line] # just grab the rest of the line pline = "" # and leave nothing behind - + else: # in all other cases param = pline.split(" ")[0] # just strip the first unquoted parameter (@@@ why not use strip1()???) sline += [param] pline = pline[len(param):].strip() - + return sline # return a list of command and parameters - + else: # without autovalidate we just split on spaces return line.split(" ") @@ -405,28 +405,28 @@ def strip_quoted(line): def Run_script(self): # @@@ maybe check we're not a subroutine (subroutines should not use this) lp_colors.updateXY(self.x, self.y) - + if self.Validate_script() != True: return - + print("[scripts] " + self.coords + " Now running script...") - + self.running = not self.is_async cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd_txt in VALID_COMMANDS: - command = VALID_COMMANDS[cmd_txt] - command.Run(self, -1, [cmd_txt]) - + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) + if len(self.script_lines) > 0: self.running = True def Main_logic(idx): # the main logic to run a line of a script if self.Check_kill(): # first check to see if we've been asked to die return idx + 1 # we just return the next line, @@@ returning -1 is better - + line = self.Line(idx) # get the line of the script - + # Handle completely blank lines if line == "": return idx + 1 @@ -437,13 +437,13 @@ def Main_logic(idx): # the main logic t # Now get the command object if cmd_txt in VALID_COMMANDS: # make sure it's a valid command command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command - + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters if type(split_line) == tuple: # bad news if we get a tuple rather than a list print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") - else: - # now run the command + else: + # now run the command return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out else: print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") @@ -456,31 +456,31 @@ def Main_logic(idx): # the main logic t idx = Main_logic(idx) # run the current line if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid run = False # then we're not going to keep running! - + if not self.is_async: # async commands don't just end self.running = False # they have to say they're not running - + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours - + print("[scripts] " + self.coords + " Script ended.") # and print (log?) that the script is complete - - # run a subroutine. This is a simplified version of running a script because the script takes care of being scheduled and killed + + # run a subroutine. This is a simplified version of running a script because the script takes care of being scheduled and killed # @@@ this is so close to run_script that it probably should be merged with it at some point -- after I know its working def Run_subroutine(self): # @@@ maybe check that we **are** a subroutine first. This is for subroutines ONLY if self.Validate_script() != True: # validates if not validated return - + print("[scripts] " + self.coords + " Now running subroutine ...") - + self.running = not self.is_async # @@@ not sure a async subroutine makes sense cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd_txt in VALID_COMMANDS: - command = VALID_COMMANDS[cmd_txt] - command.Run(self, -1, [cmd_txt]) - + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) + if len(self.script_lines) > 0: self.running = True @@ -489,7 +489,7 @@ def Main_logic(idx): # the main logic t return idx + 1 # we just return the next line, @@@ returning -1 is better line = self.Line(idx) # get the line of the script - + # Handle completely blank lines if line == "": return idx + 1 @@ -500,13 +500,13 @@ def Main_logic(idx): # the main logic t # Now get the command object if cmd_txt in VALID_COMMANDS: # make sure it's a valid command command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command - + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters if type(split_line) == tuple: # bad news if we get a tuple rather than a list print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") - else: - # now run the command + else: + # now run the command return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out else: print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") @@ -519,13 +519,13 @@ def Main_logic(idx): # the main logic t idx = Main_logic(idx) # run the current line if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid run = False # then we're not going to keep running! - + if not self.is_async: # async commands don't just end @@@ again, not sure this makes sense for subroutines self.running = False # they have to say they're not running - + print("[scripts] " + self.coords + " Subroutine ended.") # and print (log?) that the script is complete - + # validating a script consists of doing the checks that we do prior to running, but # we won't run it afterwards. def Validate_script(self): @@ -540,9 +540,9 @@ def Validate_script(self): cmd_txt = self.Split_cmd_text(self.script_lines[0]) self.is_async = cmd_txt in HEADERS and HEADERS[cmd_txt].is_async else: - self.symbols = None # otherwise destroy symbol table + self.symbols = None # otherwise destroy symbol table - return self.validated # and tell us the result + return self.validated # and tell us the result # define the buttons structure here. Note that subroutines will likely be a different sort of button, so this may change @@ -554,12 +554,12 @@ def Validate_script(self): def Bind(x, y, script_str, color): global to_run global buttons - + btn = Button(x, y, script_str) buttons[x][y] = btn if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button for index in indexes[::-1]: # and for each of them (in reverse order) temp = to_run.pop(index) # Remove them from the list return # @@@ Why do we return here? @@ -576,11 +576,11 @@ def Unbind(x, y): global buttons lp_events.unbind(x, y) # Clear any events associated with the button - + btn = Button(x, y, "") # create the new blank button if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button for index in indexes[::-1]: # and for each of them (in reverse order) temp = to_run.pop(index) # Remove them from the list buttons[x][y] = btn # Clear the button script @@ -588,32 +588,32 @@ def Unbind(x, y): if thread[x][y] != None: # If the button is actially executing thread[x][y].kill.set() # then kill it - + buttons[x][y] = btn # Clear the button script - + files.layout_changed_since_load = True # Mark the layout as changed -# swap details for two buttons +# swap details for two buttons def Swap(x1, y1, x2, y2): global text color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 - - script_1 = buttons[x1, y1].script_str # Script for btn #1 + + script_1 = buttons[x1, y1].script_str # Script for btn #1 script_2 = buttons[x2, y2].script_str # Script for btn #2 - + Unbind(x1, y1) # Unbind #1 if script_2 != "": # If there is a script #2... Bind(x1, y1, script_2, color_2) # ...bind it to #1 lp_colors.updateXY(x1, y1) # Update the colours for btn #1 - + Unbind(x2, y2) # Do the reverse for #2 if script_1 != "": Bind(x2, y2, script_1, color_1) lp_colors.updateXY(x2, y2) - + files.layout_changed_since_load = True # Flag that the layout has changed @@ -622,14 +622,14 @@ def Copy(x1, y1, x2, y2): global buttons color_1 = lp_colors.curr_colors[x1][y1] # Get colour of btn to be copied - + script_1 = buttons[x1, y1].script_str # Get script to be copied - + Unbind(x2, y2) # Unbind the destination if script_1 != "": # If we're copying a button with a script... Bind(x2, y2, script_1, color_1) # ...bind the details to the destination lp_colors.updateXY(x2, y2) # Update the colours - + files.layout_changed_since_load = True # Flag the layout as changed @@ -638,16 +638,16 @@ def Move(x1, y1, x2, y2): global buttons color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour - + script_1 = buttons[x1, y1].script_str # Get source button script - + Unbind(x1, y1) # Unbind *both* buttons Unbind(x2, y2) - + if script_1 != "": # If the source had a script... Bind(x2, y2, script_1, color_1) # ...bind it to the destination lp_colors.updateXY(x2, y2) # Update the destination colours - + files.layout_changed_since_load = True # And flag the layout as changed @@ -676,18 +676,18 @@ def kill_all(): btn.thread.kill.set() # ...kill it -# Unbind all keys. +# Unbind all keys. def Unbind_all(): lp_events.unbind_all() # Unbind all events text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank - + kill_all() # stop everything running files.curr_layout = None # There is no current layout files.layout_changed_since_load = False # So mark it as unchanged - -# Unload all subroutines. + +# Unload all subroutines. def Unload_all(): kill_all() # stop everything running @@ -695,10 +695,10 @@ def Unload_all(): for cmd in VALID_COMMANDS: # for all the commands that exist if cmd.startswith(SUBROUTINE_PREFIX):# if this command is a subroutine subs += [cmd] # add the command to the list - + for cmd in subs: # for each subroutine we've found Remove_command(cmd) # remove it files.layout_changed_since_load = True # mark layout as changed - + diff --git a/sound.py b/sound.py index 3e401f6..5a4c029 100644 --- a/sound.py +++ b/sound.py @@ -38,7 +38,7 @@ def play(filename, volume=100.0): sound.play() except: print("[sound] Could not play sound " + final_name) - + def stop(): try: @@ -52,4 +52,3 @@ def fadeout(delay): m.fadeout(delay) except: print("Could not fade out sound") - \ No newline at end of file diff --git a/variables.py b/variables.py index 3e4d68d..f31b963 100644 --- a/variables.py +++ b/variables.py @@ -1,8 +1,8 @@ from constants import * -import variables +import variables, param_convs import re -# operations needed to access variables +# operations needed to access variables # NOTE that any locking is the responsibility of the calling code! @@ -19,12 +19,12 @@ def pop(syms): return 0 # raise Exception("Stack empty") - + def push(syms, val): # put val on to the top of the stack syms[SYM_STACK].append(val) # Push a value onto the stack in the supplied symbol table - + # the top of the stack will also return 0 for an empty stack. Alternatively it could # return an error. def top(syms, i): @@ -38,30 +38,32 @@ def top(syms, i): def is_defined(name, vbls): # is the variable defined in the symbol library - return vbls and name.lower() in vbls + return vbls and str(name).lower() in vbls -# This returns 0 if the variable is not defined. An alternative is to return an error -def get(name, l_vbls, g_vbls): +# gets a variable using the default conversion of None if the variable is undefined. +def get(name, l_vbls, g_vbls, default=param_convs._None): # get a variable. look in one symbol table, then the next. # this allows an order to be defined to get local vars then global - name = name.lower() - + # the optional default allows a value other than None to be returned if the variable is undefined + name = str(name).lower() + if is_defined(name, l_vbls): # First look in the local symbol table (if defined) return l_vbls[name] if is_defined(name, g_vbls): # then the global one return g_vbls[name] - return 0 + return default(None) # return default value (rather than always 0) # raise Exception("Variable not found") def put(name, val, vbls): # store a value in a named variable in a specific variable list + print(name, val) #@@@ vbls[name.lower()] = val # if you try to grab an argument where no more exists, an error will result -def next_cmd(ret, cmds): +def next_cmd(ret, cmds): # pull the next value from the commands list and return incremented result try: v = cmds[ret] # we get the next element @@ -79,7 +81,7 @@ def valid_var_name(v): # return a properly formatted error message def error_msg(idx, name, desc, p, param, err): ret = "Line:" + str(idx+1) + " -" - + if name: ret += " '" + name + "'" if desc: @@ -95,32 +97,32 @@ def error_msg(idx, name, desc, p, param, err): ret += " " + err ret += "." - + return ret - - + + # check the number of parameters allowed -def Check_num_params(btn, cmd, idx, split_line): +def Check_num_params(btn, cmd, idx, split_line): # cmd.valid_num_params is an array of valid numbers of parameters # it will be None if you've taken control of handling the parameters yourself. # if you set it to [n, None] that means any number of parameters from n to infinity! - + if cmd.valid_num_params == None: # if this is undefined return True # anything is valid - + ln = len(cmd.valid_num_params) - n = len(split_line)-1 + n = len(split_line)-1 if ln == 2 and cmd.valid_num_params[1] == None: if n >= cmd.valid_num_params[0]: return True - elif n in cmd.valid_num_params: - return True - + elif n in cmd.valid_num_params: + return True + # create a properly formatted error message if len(cmd.valid_num_params) == 0: msg = "Has no valid number of parameters described. " return (error_msg(idx, cmd.name, msg, None, None, "Please correct the definition"), btn.Line(idx)) - + msg = "Incorrect number of parameters" if cmd.valid_num_params == [0]: return (error_msg(idx, cmd.name, msg, str(n), "supplied. None are permitted"), btn.Line(idx)) @@ -131,8 +133,8 @@ def Check_num_params(btn, cmd, idx, split_line): elif len(cmd.valid_num_params) == 2 and cmd.valid_num_params[1] == None: cnt += str(cmd.valid_num_params[0]) + " or more" else: - cnt += ", ".join([str(el) for el in cmd.valid_num_params[0:-1]]) + ", or " + str(cmd.valid_num_params[-1]) - + cnt += ", ".join([str(el) for el in cmd.valid_num_params[0:-1]]) + ", or " + str(cmd.valid_num_params[-1]) + return (error_msg(idx, cmd.name, msg, None, str(n), "supplied, " + cnt + " are required"), btn.Line(idx)) @@ -145,7 +147,7 @@ def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): return True else: return (error_msg(idx, name, desc, p, None, 'required ' + val[AV_TYPE][AVT_DESC] + ' parameter not present'), split_line[p]) - + try: temp = val[AV_TYPE][AVT_CONV](split_line[p]) except: @@ -157,7 +159,7 @@ def Check_generic_param(btn, cmd, idx, split_line, p, val, val_validation): if val[val_validation]: return val[val_validation](temp, idx, cmd.name, val[AV_DESCRIPTION], p, split_line[p]) - return True + return True # @@@ deprecated @@ -169,7 +171,7 @@ def Check_numeric_param(split_line, p, desc, idx, name, line, validation, option return True else: return (error_msg(idx, name, desc, p, None, 'required parameter not present'), btn.Line(idx)) - + try: temp = conv(split_line[p]) except: @@ -180,7 +182,7 @@ def Check_numeric_param(split_line, p, desc, idx, name, line, validation, option if validation: return validation(temp, idx, name, desc, p, split_line[p]) - return True + return True # get the value of a parameter @@ -189,42 +191,54 @@ def get_value(v, symbols): g_vars = symbols[SYM_GLOBAL] with g_vars[0]: # lock the globals while we do this v = get(v, symbols[SYM_LOCAL], g_vars[1]) - + return v def Validate_non_zero(v, idx, name, desc, p, param): - if v: - if float(v) != 0: - return True - else: - return error_msg(idx, name, desc, p, param, 'must not be zero') + # make sure we have something that can be made numeric! + try: + v = float(v) + except: + return error_msg(idx, name, desc, p, param, 'must be numeric') + + # then do the test + if float(v) != 0: + return True else: - return error_msg(idx, name, desc, p, param, 'must be an integer') + return error_msg(idx, name, desc, p, param, 'must not be zero') + - def Validate_gt_zero(v, idx, name, desc, p, param): - if v: - if v > 0: - return True - else: - return error_msg(idx, name, desc, p, param, 'must be greater than zero') + # make sure we have something that can be made numeric! + try: + v = float(v) + except: + return error_msg(idx, name, desc, p, param, 'must be numeric') + + # then do the test + if v > 0: + return True else: - return error_msg(idx, name, desc, p, param, 'must be an integer') - - + return error_msg(idx, name, desc, p, param, 'must be greater than zero') + + def Validate_ge_zero(v, idx, name, desc, p, param): - if v: - if v >= 0: - return True - else: - return error_msg(idx, name, desc, p, param, 'must not be less than zero') + # make sure we have something that can be made numeric! + try: + v = float(v) + except: + return error_msg(idx, name, desc, p, param, 'must be numeric') + + # then do the test + if v >= 0: + return True else: - return error_msg(idx, name, desc, p, param, 'must be an integer') - + return error_msg(idx, name, desc, p, param, 'must not be less than zero') + def Auto_store(v_name, value, symbols): - # automatically stores the variable in the "right" place + # automatically stores the variable in the "right" place with symbols[SYM_GLOBAL][0]: # lock the globals while we do this if is_defined(v_name, symbols[SYM_LOCAL]): # Is it local... put(v_name, value, symbols[SYM_LOCAL]) # ...then store it locally @@ -235,21 +249,21 @@ def Auto_store(v_name, value, symbols): def Local_store(v_name, value, symbols): - # stores the variable locally + # stores the variable locally put(v_name, value, symbols[SYM_LOCAL]) # and store it locally def Global_store(v_name, value, symbols): - # stores the variable globally + # stores the variable globally with symbols[SYM_GLOBAL][0]: # lock the globals while we do this put(v_name, value, symbols[SYM_GLOBAL][1]) # and store it globally def Auto_recall(v_name, symbols): - # automatically recalls the variable from the "right" place + # automatically recalls the variable from the "right" place with symbols[SYM_GLOBAL][0]: # lock the globals while we do this a = variables.get(v_name, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1]) # try local, then global - + return a @@ -257,7 +271,7 @@ def Local_recall(v_name, symbols): # automatically recalls the local variable a = variables.get(v_name, symbols[SYM_LOCAL], None) # get the value from the local vars return a - + def Global_recall(v_name, symbols): # automatically recalls the global variable diff --git a/window.py b/window.py index 1431f29..0cf9e1b 100644 --- a/window.py +++ b/window.py @@ -5,8 +5,12 @@ from functools import partial import webbrowser -import scripts, files, lp_colors, lp_events -from utils import launchpad_connector as lpcon +import scripts, files, lp_colors, lp_events, global_vars + +from utils import launchpad_connector +from launchpad_fake import launchpad_fake_connector +from constants import * + BUTTON_SIZE = 40 HS_SIZE = 200 @@ -29,16 +33,12 @@ MAIN_ICON = None -launchpad = None - root = None app = None root_destroyed = None restart = False lp_object = None -ARGS = dict() - load_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT, files.LEGACY_LAYOUT_EXT])] load_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT, files.LEGACY_SCRIPT_EXT])] load_subroutine_filetypes = [('LPHK subroutine files', [files.SUBROUTINE_EXT])] @@ -53,10 +53,30 @@ MENU_LAYOUT = "Layout" MENU_SUBROUTINES = "Subroutines" +MENU_LAUNCHPAD = "Launchpad" + +LPCON = None + + +# Are we in standalone mode? +def IsStandalone(): + return global_vars.ARGS['standalone'] + + +def lpcon(): + global LPCON -def init(lp_object_in, launchpad_in, path_in, prog_path_in, user_path_in, version_in, platform_in): + if LPCON == None: + if IsStandalone(): + LPCON = launchpad_fake_connector + else: + LPCON = launchpad_connector + + return LPCON + + +def init(lp_object_in, path_in, prog_path_in, user_path_in, version_in, platform_in): global lp_object - global launchpad global PATH global PROG_PATH global USER_PATH @@ -64,13 +84,12 @@ def init(lp_object_in, launchpad_in, path_in, prog_path_in, user_path_in, versio global PLATFORM global MAIN_ICON lp_object = lp_object_in - launchpad = launchpad_in PATH = path_in PROG_PATH = prog_path_in USER_PATH = user_path_in VERSION = version_in PLATFORM = platform_in - + if PLATFORM == "windows": MAIN_ICON = os.path.join(PATH, "resources", "LPHK.ico") else: @@ -84,7 +103,7 @@ def __init__(self, master=None): tk.Frame.__init__(self, master) self.master = master self.init_window() - + self.about_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/LPHK-banner.png")) self.info_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/info.png")) self.warning_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/warning.png")) @@ -99,7 +118,7 @@ def __init__(self, master=None): def init_window(self): global root - + self.master.title("LPHK - Novation Launchpad Macro Scripting System") self.pack(fill="both", expand=1) @@ -108,7 +127,10 @@ def init_window(self): self.m_Launchpad = tk.Menu(self.m, tearoff=False) self.m_Launchpad.add_command(label="Redetect (Restart)", command=self.redetect_lp) - self.m.add_cascade(label="Launchpad", menu=self.m_Launchpad) + self.m.add_cascade(label=MENU_LAUNCHPAD, menu=self.m_Launchpad) + + if IsStandalone(): + self.disable_menu(MENU_LAUNCHPAD) self.m_Layout = tk.Menu(self.m, tearoff=False) self.m_Layout.add_command(label="New Layout", command=self.unbind_lp) @@ -118,14 +140,14 @@ def init_window(self): self.m.add_cascade(label=MENU_LAYOUT, menu=self.m_Layout) self.disable_menu(MENU_LAYOUT) - + self.m_Subroutine = tk.Menu(self.m, tearoff=False) self.m_Subroutine.add_command(label="Load", command=self.load_subroutines) self.m_Subroutine.add_command(label="Clear", command=self.clear_subroutines) self.m.add_cascade(label=MENU_SUBROUTINES, menu=self.m_Subroutine) self.disable_menu(MENU_SUBROUTINES) - + self.m_Help = tk.Menu(self.m, tearoff=False) open_readme = lambda: webbrowser.open("https://github.com/nimaid/LPHK#lphk-launchpad-hotkey") self.m_Help.add_command(label="Open README...", command=open_readme) @@ -146,26 +168,26 @@ def init_window(self): self.c.bind("", self.click) self.c.grid(row=0, column=0, padx=round(c_gap/2), pady=round(c_gap/2)) - self.stat = tk.Label(self, text="No Launchpad Connected", bg=STAT_INACTIVE_COLOR, fg="#fff") + self.stat = tk.Label(self, text="No Launchpad Connected 1", bg=STAT_INACTIVE_COLOR, fg="#fff") #@@@ self.stat.grid(row=1, column=0, sticky=tk.EW) self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) - + def raise_above_all(self): self.master.attributes('-topmost', 1) self.master.attributes('-topmost', 0) - + def enable_menu(self, name): self.m.entryconfig(name, state="normal") def disable_menu(self, name): self.m.entryconfig(name, state="disabled") - + def connect_dummy(self): # WIP global lp_connected global lp_mode global lp_object - + lp_connected = True lp_mode = "Dummy" self.draw_canvas() @@ -177,35 +199,43 @@ def connect_lp(self): global lp_mode global lp_object - lp = lpcon.get_launchpad() + lp = lpcon().get_launchpad() if lp is -1: self.popup(self, "Connect to Unsupported Device", self.error_image, """The device you are attempting to use is not currently supported by LPHK, - and there are no plans to add support for it. - Please voice your feature requests on the Discord or on GitHub.""", +and there are no plans to add support for it. +Please voice your feature requests on the Discord or on GitHub.""", "OK") if lp is None: self.popup_choice(self, "No Launchpad Detected...", self.error_image, """Could not detect any connected Launchpads! - Disconnect and reconnect your USB cable, - then click 'Redetect Now'.""", +Disconnect and reconnect your USB cable, +then click 'Redetect Now'.""", [["Ignore", None], ["Redetect Now", self.redetect_lp]] ) return - if lpcon.connect(lp): + if IsStandalone(): + self.popup_choice(self, "LPHK Standalone mode", self.error_image, + """LPHK has been started in standalone mode and +will not try to connect to a Launchpad. Execute +buttons by right clicking on them.""", + [["OK", None]] + ) + + if lpcon().connect(lp): lp_connected = True lp_object = lp - lp_mode = lpcon.get_mode(lp) + lp_mode = lpcon().get_mode(lp) if lp_mode is "Pro": self.popup(self, "Connect to Launchpad Pro", self.error_image, """This is a BETA feature! The Pro is not fully supported yet,as the bottom and left rows are not mappable currently. - I (nimaid) do not have a Launchpad Pro to test with, so let me know if this does or does not work on the Discord! (https://discord.gg/mDCzB8X) - You must first put your Launchpad Pro in Live (Session) mode. To do this, press and holde the 'Setup' key, press the green pad in the - upper left corner, then release the 'Setup' key. Please only continue once this step is completed.""", +I (nimaid) do not have a Launchpad Pro to test with, so let me know if this does or does not work on the Discord! (https://discord.gg/mDCzB8X) +You must first put your Launchpad Pro in Live (Session) mode. To do this, press and holde the 'Setup' key, press the green pad in the +upper left corner, then release the 'Setup' key. Please only continue once this step is completed.""", "I am in Live mode.") lp_object.ButtonFlush() @@ -218,23 +248,25 @@ def connect_lp(self): self.draw_canvas() self.enable_menu(MENU_LAYOUT) self.enable_menu(MENU_SUBROUTINES) - self.stat["text"] = f"Connected to {lpcon.get_display_name(lp)}" - self.stat["bg"] = STAT_ACTIVE_COLOR + self.stat["text"] = f"Connected to {lpcon().get_display_name(lp)}" + self.stat["bg"] = STAT_ACTIVE_COLOR # load a layout on startup def load_initial_layout(self): - global ARGS - if ARGS['layout']: # did the user pass the option to load an initial layout? - files.load_layout_to_lp(ARGS['layout'].name) # Load it! + if global_vars.ARGS['layout']: # did the user pass the option to load an initial layout? + files.load_layout_to_lp(global_vars.ARGS['layout'].name) # Load it! def disconnect_lp(self): global lp_connected try: scripts.Unbind_all() lp_events.timer.cancel() - lpcon.disconnect(lp_object) + if not IsStandalone(): + lpcon().disconnect(lp_object) except: - self.redetect_lp() + if not IsStandalone(): + self.redetect_lp() + lp_connected = False self.clear_canvas() @@ -242,7 +274,7 @@ def disconnect_lp(self): self.disable_menu(MENU_LAYOUT) self.disable_menu(MENU_SUBROUTINES) - self.stat["text"] = "No Launchpad Connected" + self.stat["text"] = "No Launchpad Connected 2" # @@@ self.stat["bg"] = STAT_INACTIVE_COLOR def redetect_lp(self): @@ -296,11 +328,11 @@ def save_layout(self): else: files.save_lp_to_layout(files.curr_layout) files.load_layout_to_lp(files.curr_layout) - + def click(self, event): gap = int(BUTTON_SIZE // 4) - - + + column = min(8, int(event.x // (BUTTON_SIZE + gap))) row = min(8, int(event.y // (BUTTON_SIZE + gap))) @@ -315,7 +347,7 @@ def click(self, event): elif self.button_mode == "swap": self.button_mode = "copy" else: - self.button_mode = "edit" + self.button_mode = "edit" self.draw_canvas() else: if self.button_mode == "edit": @@ -330,7 +362,7 @@ def click(self, event): move_func = partial(scripts.move, self.last_clicked[0], self.last_clicked[1], column, row) swap_func = partial(scripts.swap, self.last_clicked[0], self.last_clicked[1], column, row) copy_func = partial(scripts.copy, self.last_clicked[0], self.last_clicked[1], column, row) - + if self.button_mode == "move": if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to move a button to an already\nbound button. What would you like to do?", [["Overwrite", move_func], ["Swap", swap_func], ["Cancel", None]]) @@ -345,7 +377,7 @@ def click(self, event): swap_func() self.last_clicked = None self.draw_canvas() - + def draw_button(self, column, row, color="#000000", shape="square"): gap = int(BUTTON_SIZE // 4) @@ -369,7 +401,7 @@ def draw_canvas(self): y_start = round((BUTTON_SIZE * self.last_clicked[1]) + (gap * self.last_clicked[1])) x_end = round(x_start + BUTTON_SIZE + gap) y_end = round(y_start + BUTTON_SIZE + gap) - + if (self.last_clicked[1] == 0) or (self.last_clicked[0] == 8): self.outline_box = self.c.create_oval(x_start + (gap // 2), y_start + (gap // 2), x_end - (gap // 2), y_end - (gap // 2), fill=SELECT_COLOR, outline="") else: @@ -379,7 +411,7 @@ def draw_canvas(self): if self.outline_box != None: self.c.delete(self.outline_box) self.outline_box = None - + if self.grid_drawn: for x in range(8): y = 0 @@ -392,7 +424,7 @@ def draw_canvas(self): for x in range(8): for y in range(1, 9): self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) - + self.c.itemconfig(self.grid_rects[8][0], text=self.button_mode.capitalize()) else: for x in range(8): @@ -406,12 +438,12 @@ def draw_canvas(self): for x in range(8): for y in range(1, 9): self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y)) - + gap = int(BUTTON_SIZE // 4) text_x = round((BUTTON_SIZE * 8) + (gap * 8) + (BUTTON_SIZE / 2) + (gap / 2)) text_y = round((BUTTON_SIZE / 2) + (gap / 2)) self.grid_rects[8][0] = self.c.create_text(text_x, text_y, text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) - + self.grid_drawn = True def clear_canvas(self): @@ -421,7 +453,7 @@ def clear_canvas(self): def script_entry_window(self, x, y, text_override=None, color_override=None): global color_to_set - + w = tk.Toplevel(self) w.winfo_toplevel().title("Editing Script for Button (" + str(x) + ", " + str(y) + ")") w.resizable(False, False) @@ -431,11 +463,11 @@ def script_entry_window(self, x, y, text_override=None, color_override=None): dummy = None #w.call('wm', 'iconphoto', w._w, tk.PhotoImage(file=MAIN_ICON)) else: - w.iconbitmap(MAIN_ICON) - + w.iconbitmap(MAIN_ICON) + def validate_func(): nonlocal x, y, t - + text_string = t.get(1.0, tk.END) try: btn = scripts.Button(x, y, text_string) @@ -448,6 +480,7 @@ def validate_func(): self.save_script(w, x, y, text_string) else: w.destroy() + w.protocol("WM_DELETE_WINDOW", validate_func) e_m = tk.Menu(w) @@ -457,7 +490,7 @@ def validate_func(): t = tk.scrolledtext.ScrolledText(w) t.grid(column=0, row=0, rowspan=3, padx=10, pady=10) - + if text_override == None: t.insert(tk.INSERT, scripts.buttons[x][y].script_str) else: @@ -470,21 +503,21 @@ def validate_func(): export_script_func = lambda: self.export_script(t, w) e_m_Script.add_command(label="Export script", command=export_script_func) e_m.add_cascade(label="Script", menu=e_m_Script) - + if color_override == None: colors_to_set[x][y] = lp_colors.getXY(x, y) else: colors_to_set[x][y] = color_override - + if type(colors_to_set[x][y]) == int: colors_to_set[x][y] = lp_colors.code_to_RGB(colors_to_set[x][y]) - + if all(c < 4 for c in colors_to_set[x][y]): if lp_mode == "Mk1": colors_to_set[x][y] = MK1_DEFAULT_COLOR else: colors_to_set[x][y] = DEFAULT_COLOR - + ask_color_func = lambda: self.ask_color(w, color_button, x, y, colors_to_set[x][y]) color_button = tk.Button(w, text="Select Color", command=ask_color_func) color_button.grid(column=1, row=0, padx=(0, 10), pady=(10, 50), sticky="nesw") @@ -519,60 +552,60 @@ def classic_askcolor(self, color=(255, 0, 0), title="Color Chooser"): #w.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) else: w.iconbitmap(MAIN_ICON) - + w.protocol("WM_DELETE_WINDOW", w.destroy) - + color = "" - + def return_color(col): nonlocal color color = col w.destroy() - + button_frame = tk.Frame(w) button_frame.grid(padx=(10, 0), pady=(10, 0)) - + def make_grid_button(column, row, color_hex, func=None, size=100): nonlocal w f = tk.Frame(button_frame, width=size, height=size) b = tk.Button(f, command=func) - + f.rowconfigure(0, weight = 1) f.columnconfigure(0, weight = 1) f.grid_propagate(0) - + f.grid(column=column, row=row) b.grid(padx=(0,10), pady=(0,10), sticky="nesw") b.config(bg=color_hex) - + def make_color_button(button_color, column, row, size=100): button_color_hex = "#%02x%02x%02x" % button_color - + b_func = lambda: return_color(button_color) make_grid_button(column, row, button_color_hex, b_func, size) - + for c in range(4): for r in range(4): if not (c == 0 and r == 3): red = int(c * (255 / 3)) green = int((3 - r) * (255 / 3)) - + make_color_button((red, green, 0), c, r, size=75) w.wait_visibility() w.grab_set() w.wait_window() - + if color: hex = "#%02x%02x%02x" % color return color, hex else: return None, None - + def ask_color(self, window, button, x, y, default_color): global colors_to_set - + if lp_mode == "Mk1": color = self.classic_askcolor(color=tuple(default_color), title="Select Color for Button (" + str(x) + ", " + str(y) + ")") else: @@ -620,14 +653,14 @@ def unbind_destroy(self, x, y, window): def save_script(self, window, x, y, script_text, open_editor = False, color=None): global colors_to_set - + script_text = script_text.strip() - + def open_editor_func(): nonlocal x, y if open_editor: self.script_entry_window(x, y, script_text, color) - + try: btn = scripts.Button(x, y, script_text) script_validate = btn.Parse_script() @@ -694,7 +727,7 @@ def run_end(): popup.wait_visibility() popup.grab_set() popup.wait_window() - + def popup_choice(self, window, title, image, text, choices): popup = tk.Toplevel(window) popup.resizable(False, False) @@ -706,7 +739,7 @@ def popup_choice(self, window, title, image, text, choices): popup.iconbitmap(MAIN_ICON) popup.wm_title(title) popup.tkraise(window) - + def run_end(func): popup.destroy() if func != None: @@ -722,7 +755,7 @@ def run_end(func): popup.wait_visibility() popup.grab_set() popup.wait_window() - + def modified_layout_save_prompt(self): if files.layout_changed_since_load == True: layout_empty = True @@ -731,7 +764,7 @@ def modified_layout_save_prompt(self): if btn.script_str != "": layout_empty = False break - + if not layout_empty: self.popup_choice(self, "Save Changes?", self.warning_image, "You have made changes to this layout.\nWould you like to save this layout before exiting?", [["Save", self.save_layout], ["Save As...", self.save_layout_as], ["Discard", None]]) @@ -740,11 +773,10 @@ def make(): global app global root_destroyed global redetect_before_start - global ARGS root = tk.Tk() # does the user want to start the form minimised? - if ARGS['minimised']: + if global_vars.ARGS['minimised']: root.iconify() root_destroyed = False @@ -765,7 +797,7 @@ def make(): def close(): - global root_destroyed, launchpad + global root_destroyed app.modified_layout_save_prompt() app.disconnect_lp() From 107303b40682f5acfc9e4de078e597b83b4fd23a Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:55:44 +0800 Subject: [PATCH 48/83] * Better handling of the conversion routines * Allow emulation of various launchpads --- LPHK.py | 2 +- commands_rpncalc.py | 3 +-- constants.py | 29 +++++++++++++++++------------ files.py | 2 +- launchpad_fake.py | 7 +++++-- lp_colors.py | 5 +++-- window.py | 10 +++++----- 7 files changed, 33 insertions(+), 25 deletions(-) diff --git a/LPHK.py b/LPHK.py index fc819a2..d1ec88f 100755 --- a/LPHK.py +++ b/LPHK.py @@ -109,7 +109,7 @@ def init(): help = "Start the application minimised", action="store_true") ap.add_argument( # option to start without connecting to a Launchpad "-s", "--standalone", - help = "Operate without connection to Launchpad", action="store_true") + help = "Operate without connection to Launchpad", type=str, choices=['Mk1', "Mk2", "Mini", "Pro"]) global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to diff --git a/commands_rpncalc.py b/commands_rpncalc.py index c705359..aeba148 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -1,6 +1,5 @@ -import command_base, lp_events, scripts, variables, sys +import command_base, lp_events, scripts, variables, sys, param_convs from constants import * -from param_convs import * LIB = "cmds_rpnc" # name of this library (for logging) diff --git a/constants.py b/constants.py index 10362e5..e4840b7 100644 --- a/constants.py +++ b/constants.py @@ -1,6 +1,6 @@ # Constants used all over the place. An excuse to use "from constants import *" -from param_convs import * +import param_convs # Get platform information PLATFORMS = [ {"search_string": "win", "name_string": "windows"}, @@ -64,17 +64,17 @@ AV_P2_VALIDATION = 5 # constants for parameter types -# desc conv special last var (special means additional auto-validation, last means MUST be last, var is the max AV_VAR allowed) -PT_INT = ("int", _int, False, False, AVVS_ALL) -PT_FLOAT = ("float", _float, False, False, AVVS_ALL) -PT_STR = ("str", _str, True, False, AVVS_ALL) # a quoted string -PT_STRS = ("strs", _str, True, True, AVVS_ALL) # 1 or more quoted strings -PT_LINE = ("line", _str, True, True, AVVS_NO) # the rest of the line following first preceeding whitespace -PT_TEXT = ("text", _str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED -PT_LABEL = ("label", _str, True, False, AVVS_NO) # Note that this is for a reference to a label, not the definition of a label! -PT_TARGET = ("target", _str, True, False, AVVS_NO) # Note that this is for the definition of a target (e.g. creating a label) -PT_KEY = ("key", _str, True, False, AVVS_NO) # This is a key literal -PT_BOOL = ("bool", _str, True, False, AVVS_ALL) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables +# desc conv special last var (special means additional auto-validation, last means MUST be last, var is the max AV_VAR allowed) +PT_INT = ("int", param_convs._int, False, False, AVVS_ALL) +PT_FLOAT = ("float", param_convs._float, False, False, AVVS_ALL) +PT_STR = ("str", param_convs._str, True, False, AVVS_ALL) # a quoted string +PT_STRS = ("strs", param_convs._str, True, True, AVVS_ALL) # 1 or more quoted strings +PT_LINE = ("line", param_convs._str, True, True, AVVS_NO) # the rest of the line following first preceeding whitespace +PT_TEXT = ("text", param_convs._str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED +PT_LABEL = ("label", param_convs._str, True, False, AVVS_NO) # Note that this is for a reference to a label, not the definition of a label! +PT_TARGET = ("target", param_convs._str, True, False, AVVS_NO) # Note that this is for the definition of a target (e.g. creating a label) +PT_KEY = ("key", param_convs._str, True, False, AVVS_NO) # This is a key literal +PT_BOOL = ("bool", param_convs._str, True, False, AVVS_ALL) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables # constants for auto_message AM_COUNT = 0 @@ -95,3 +95,8 @@ DELAY_EXIT_CHECK = 0.025 SUBROUTINE_PREFIX = "CALL:" + +# Launchpad constants + +LP_MK1 = "Mk1" +LP_PRO = "Pro" \ No newline at end of file diff --git a/files.py b/files.py index 413c8dd..1be07f9 100644 --- a/files.py +++ b/files.py @@ -163,7 +163,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): color = button["color"] script_text = button["text"] - if window.lp_mode == "Mk1": + if window.lp_mode == LP_MK1: if color[2] != 0: color = lp_colors.RGB_to_RG(color) converted_to_rg = True diff --git a/launchpad_fake.py b/launchpad_fake.py index a9de1bd..4aa2890 100644 --- a/launchpad_fake.py +++ b/launchpad_fake.py @@ -1,5 +1,8 @@ # A fake connector to allow operation without a launchpad connected +import global_vars + + class Launchpad(): def __init__(self): pass @@ -38,10 +41,10 @@ def connect(self, lp): return True def get_mode(self, lp): - return "*ALONE*" + return global_vars.ARGS['standalone'] def get_display_name(self, lp): - return "Standalone Mode" + return "Emulated Launchpad " + global_vars.ARGS['standalone'] def disconnect(self, lp_object): pass diff --git a/lp_colors.py b/lp_colors.py index 091081e..886d45d 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -3,6 +3,7 @@ import lp_events, scripts, window import colorsys +from constants import * lp_object = None # another suspiciously non-threadsafe way of doing things :-( @@ -113,7 +114,7 @@ def updateXY(x, y): set_color = curr_colors[x][y] # this button is not running (or not even asigned) color_modes[x][y] = "solid" # set the mode alone - if window.lp_mode == "Mk1": # how to actually set the colours of Mk:1 launchpads + if window.lp_mode == LP_MK1: # how to actually set the colours of Mk:1 launchpads if type(set_color) is int: set_color = code_to_RGB(set_color) lp_object.LedCtrlXY(x, y, set_color[0]//64, set_color[1]//64) @@ -149,7 +150,7 @@ def update_all(): def raw_clear(): for x in range(9): for y in range(9): - if window.lp_mode == "Mk1": + if window.lp_mode == LP_MK1: lp_object.LedCtrlXY(x, y, 0, 0) else: lp_object.LedCtrlXYByCode(x, y, 0) diff --git a/window.py b/window.py index 0cf9e1b..c9e19dc 100644 --- a/window.py +++ b/window.py @@ -60,7 +60,7 @@ # Are we in standalone mode? def IsStandalone(): - return global_vars.ARGS['standalone'] + return not (global_vars.ARGS['standalone'] == None) def lpcon(): @@ -230,7 +230,7 @@ def connect_lp(self): lp_object = lp lp_mode = lpcon().get_mode(lp) - if lp_mode is "Pro": + if lp_mode is LP_PRO: self.popup(self, "Connect to Launchpad Pro", self.error_image, """This is a BETA feature! The Pro is not fully supported yet,as the bottom and left rows are not mappable currently. I (nimaid) do not have a Launchpad Pro to test with, so let me know if this does or does not work on the Discord! (https://discord.gg/mDCzB8X) @@ -241,7 +241,7 @@ def connect_lp(self): lp_object.ButtonFlush() # special case? - if lp_mode is not "Mk1": + if lp_mode != LP_MK1: lp_object.LedCtrlBpm(INDICATOR_BPM) lp_events.start(lp_object) @@ -513,7 +513,7 @@ def validate_func(): colors_to_set[x][y] = lp_colors.code_to_RGB(colors_to_set[x][y]) if all(c < 4 for c in colors_to_set[x][y]): - if lp_mode == "Mk1": + if lp_mode == LP_MK1: colors_to_set[x][y] = MK1_DEFAULT_COLOR else: colors_to_set[x][y] = DEFAULT_COLOR @@ -606,7 +606,7 @@ def make_color_button(button_color, column, row, size=100): def ask_color(self, window, button, x, y, default_color): global colors_to_set - if lp_mode == "Mk1": + if lp_mode == LP_MK1: color = self.classic_askcolor(color=tuple(default_color), title="Select Color for Button (" + str(x) + ", " + str(y) + ")") else: color = tkcolorpicker.askcolor(color=tuple(default_color), parent=window, title="Select Color for Button (" + str(x) + ", " + str(y) + ")") From c497ab28bca190b99127cef0589da88514abb5bf Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 10 Mar 2021 06:43:29 +0800 Subject: [PATCH 49/83] Further work on standalone mode, and on correcting parameter passing. Code still contains a lot of debug stuff * LPHK.py values for Launchpad model now constants * LPHK.py New option -M or -mode to set initial mode (edit, move, swap, copy, or RUN!!!!) * variables.py Some new temporary debug stuff * window.py - Setting of initial mode from arguments * window.py Using constants instead of literals in more places * window.py Rearranging code to allow main form to be set up before displaying most popups * window.py Form now uses button release instead of button click. This means nothing is pressed when a command is executed * window.py Buttons now must be clicked ON rather than just near them. (circular buttons still have a square sensitive region) * window.py Local execution (via RUN) available but non-functionaL in non-standalone mode (more work to do here) * window.py Clicking on button in RUN mode queues a button down and then button up into events queue. This limits what you can do with buttons in this mode, but it's all I need, and is better than nothing * constants.py New constants, mostly for window.py, but also used in LPHK.py for arguments * command_base.py Some new routines to make handling multiple parameters easier * command_base.py work on correcting variable handling that was broken by subroutines (incomplete) * commands_test.py A new command module containing commands used for testing parameter passing. This is temporary only and will be removed from the project once the problem is solved * command_list.py Includes commands_test.py, but doesn't complain if it's not there. Also a temporary modification. --- LPHK.py | 7 +- command_base.py | 51 +++++---- command_list.py | 6 ++ commands_test.py | 257 ++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 11 +- launchpad_fake.py | 24 +++-- variables.py | 2 +- window.py | 81 +++++++++------ 8 files changed, 375 insertions(+), 64 deletions(-) create mode 100644 commands_test.py diff --git a/LPHK.py b/LPHK.py index d1ec88f..57dbdfa 100755 --- a/LPHK.py +++ b/LPHK.py @@ -109,7 +109,10 @@ def init(): help = "Start the application minimised", action="store_true") ap.add_argument( # option to start without connecting to a Launchpad "-s", "--standalone", - help = "Operate without connection to Launchpad", type=str, choices=['Mk1', "Mk2", "Mini", "Pro"]) + help = "Operate without connection to Launchpad", type=str, choices=[LP_MK1, LP_MK2, LP_MINI, LP_PRO]) + ap.add_argument( # option to start with launchpad window in a particular mode + "-M", "--mode", + help = "Launchpad mode", type=str, choices=[LM_EDIT, LM_MOVE, LM_SWAP, LM_COPY, LM_RUN], default=LM_EDIT) global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to @@ -120,7 +123,7 @@ def init(): files.init(USER_PATH) sound.init(USER_PATH) - + global LP LP = Launchpad() diff --git a/command_base.py b/command_base.py index 7748627..04ba2af 100644 --- a/command_base.py +++ b/command_base.py @@ -496,24 +496,24 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): if pass_no == 1: v = split_line[n] - print(v) #@@@ + print("V1", v) #@@@ if av[AV_VAR_OK] == AVV_YES: # if a variable is allowed - if valid_var_name(v): # if it's a variable + if variables.valid_var_name(v): # if it's a variable v = variables.get_value(split_line[n], btn.symbols) # get the value - print(v) #@@@ + print("V2", v) #@@@ if av[AV_VAR_OK] != AVV_REQD: # if it's not required (i.e. the variable name is not to be passed through) if av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: # if there is a type v = av[AV_TYPE][AVT_CONV](v) # convert the variable to that type - print(v) #@@@ + print("V2", v) #@@@ return v elif pass_no == 2: ok = ret if av[AV_P1_VALIDATION]: - print(btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ + print("S", btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ ok = av[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) if ok != True: print("[" + self.lib + "] " + btn.coords + " " + ok) @@ -522,46 +522,61 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): return ret + # How many parameters do we have? + def Param_count(self, btn): + return btn.symbols[SYM_PARAM_CNT] + + # Is there a parameter n? def Has_param(self, btn, n): + if self.Param_count(btn) < n: + return False + val = btn.symbols[SYM_PARAMS][n] return not (val is None) - # How many parameters do we have? - def Param_count(self, btn): - return btn.symbols[SYM_PARAM_CNT] - - # gets the value of the nth parameter (button is required for context). Other is default value if param does not exist def Get_param(self, btn, n, other=None): # handle the repeating last parameter avl = len(self.auto_validate) m = min(n, avl) - val = self.auto_validate[m-1] + av = self.auto_validate[m-1] - param = btn.symbols[SYM_PARAMS][n] - print(param) #@@@ + if self.Param_count(btn) > n: + param = None + print("P-none", self.Param_count(btn), n) + else: + param = btn.symbols[SYM_PARAMS][n] + print("P-n", n) + print("P", param) #@@@ if param == None: ret = other else: - if val[AV_VAR_OK] == AVV_REQD: + if av[AV_VAR_OK] == AVV_REQD: + print("RQ") #@@@ ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: - if type(param) == str and val[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': + print("NRQ") #@@@ + if type(param) == str and av[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': ret = param[1:] else: ret = param - return ret + return av[AV_TYPE][AVT_CONV](ret) # sets the value of the nth parameter (if it is a variable) def Set_param(self, btn, n, val): + if self.Param_count(btn) < n: + return + param = btn.symbols[SYM_PARAMS][n] - av = self.auto_validate[n-1] + avl = len(self.auto_validate) + m = min(n, avl) + av = self.auto_validate[m-1] if av[AV_VAR_OK] == AVV_REQD: - print(btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ + print("SP", btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable diff --git a/command_list.py b/command_list.py index 9dd7074..50172c1 100644 --- a/command_list.py +++ b/command_list.py @@ -15,6 +15,12 @@ commands_external, \ commands_subroutines +# @@@ a test command set for testing things! Will be removed for production +try: + import commands_test +except: + pass + # This library could be considered optional, but is not platform specific try: import commands_rpncalc diff --git a/commands_test.py b/commands_test.py new file mode 100644 index 0000000..193d3c5 --- /dev/null +++ b/commands_test.py @@ -0,0 +1,257 @@ +import command_base, commands_header, scripts, variables +from constants import * + +LIB = "cmds_test" # name of this library (for logging) + +class Test_XX(command_base.Command_Basic): + def Process(self, btn, idx, split_line): + print("============================") + print("=", self.name, split_line) + print("=", f"Symbols before = {btn.symbols}") + a = self.Get_param(btn, 1) + print("=", f"Param = '{a}'") + if a != None: + a += 1 + self.Set_param(btn, 1, a) + print("=", f"Symbols after = {btn.symbols}") + print("============================") + + +class Test_01(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_01", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_NO, PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_01()) # register the command + + +class Test_02(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_02", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_NO, PT_FLOAT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_02()) # register the command + + +class Test_03(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_03", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_NO, PT_STR, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_03()) # register the command + + +class Test_04(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_04", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_NO, PT_STRS, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_04()) # register the command + + +class Test_11(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_11", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_YES, PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_11()) # register the command + + +class Test_12(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_12", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_YES, PT_FLOAT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_12()) # register the command + + +class Test_13(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_13", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_YES, PT_STR, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_13()) # register the command + + +class Test_14(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_14", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_YES, PT_STRS, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_14()) # register the command + + +class Test_21(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_21", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_REQD,PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_21()) # register the command + + +class Test_22(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_22", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_REQD,PT_FLOAT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_22()) # register the command + + +class Test_23(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_23", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_REQD,PT_STR, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_23()) # register the command + + +class Test_24(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_24", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", True, AVV_REQD,PT_STRS, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + +scripts.Add_command(Test_24()) # register the command diff --git a/constants.py b/constants.py index e4840b7..970e490 100644 --- a/constants.py +++ b/constants.py @@ -97,6 +97,13 @@ SUBROUTINE_PREFIX = "CALL:" # Launchpad constants - LP_MK1 = "Mk1" -LP_PRO = "Pro" \ No newline at end of file +LP_MK2 = "Mk2" +LP_PRO = "Pro" +LP_MINI = "Mini" + +LM_EDIT = "edit" +LM_MOVE = "move" +LM_SWAP = "swap" +LM_COPY = "copy" +LM_RUN = "run" \ No newline at end of file diff --git a/launchpad_fake.py b/launchpad_fake.py index 4aa2890..c2a6620 100644 --- a/launchpad_fake.py +++ b/launchpad_fake.py @@ -2,6 +2,16 @@ import global_vars +FAKE_EVENT_QUEUE = [] + +def AddEvent(x): + FAKE_EVENT_QUEUE.append(x) + +def Pop(): + if FAKE_EVENT_QUEUE == []: + return [] + else: + return FAKE_EVENT_QUEUE.pop(0) class Launchpad(): def __init__(self): @@ -14,18 +24,18 @@ def LedCtrlBpm(self, x): pass def ButtonStateXY(self): - return [] - + return Pop() + def LedCtrlXYByRGB(self, x, y, z): pass - + def LedCtrlXY(self, x, y, z, t): pass - + def LedCtrlXYByCode(self, x, y, z): pass - - + + launchpad = Launchpad() @@ -39,7 +49,7 @@ def get_launchpad(self): def connect(self, lp): return True - + def get_mode(self, lp): return global_vars.ARGS['standalone'] diff --git a/variables.py b/variables.py index f31b963..94b3b12 100644 --- a/variables.py +++ b/variables.py @@ -58,7 +58,7 @@ def get(name, l_vbls, g_vbls, default=param_convs._None): def put(name, val, vbls): # store a value in a named variable in a specific variable list - print(name, val) #@@@ + print("NV", name, val) #@@@ vbls[name.lower()] = val diff --git a/window.py b/window.py index c9e19dc..d612c49 100644 --- a/window.py +++ b/window.py @@ -112,7 +112,7 @@ def __init__(self, master=None): self.scare_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/scare.png")) self.grid_drawn = False self.grid_rects = [[None for y in range(9)] for x in range(9)] - self.button_mode = "edit" + self.button_mode = global_vars.ARGS['mode'] self.last_clicked = None self.outline_box = None @@ -165,10 +165,10 @@ def init_window(self): c_size = (BUTTON_SIZE * 9) + (c_gap * 9) self.c = tk.Canvas(self, width=c_size, height=c_size) - self.c.bind("", self.click) + self.c.bind("", self.click) self.c.grid(row=0, column=0, padx=round(c_gap/2), pady=round(c_gap/2)) - self.stat = tk.Label(self, text="No Launchpad Connected 1", bg=STAT_INACTIVE_COLOR, fg="#fff") #@@@ + self.stat = tk.Label(self, text="No Launchpad Connected", bg=STAT_INACTIVE_COLOR, fg="#fff") self.stat.grid(row=1, column=0, sticky=tk.EW) self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) @@ -217,27 +217,11 @@ def connect_lp(self): ) return - if IsStandalone(): - self.popup_choice(self, "LPHK Standalone mode", self.error_image, - """LPHK has been started in standalone mode and -will not try to connect to a Launchpad. Execute -buttons by right clicking on them.""", - [["OK", None]] - ) - if lpcon().connect(lp): lp_connected = True lp_object = lp lp_mode = lpcon().get_mode(lp) - if lp_mode is LP_PRO: - self.popup(self, "Connect to Launchpad Pro", self.error_image, - """This is a BETA feature! The Pro is not fully supported yet,as the bottom and left rows are not mappable currently. -I (nimaid) do not have a Launchpad Pro to test with, so let me know if this does or does not work on the Discord! (https://discord.gg/mDCzB8X) -You must first put your Launchpad Pro in Live (Session) mode. To do this, press and holde the 'Setup' key, press the green pad in the -upper left corner, then release the 'Setup' key. Please only continue once this step is completed.""", - "I am in Live mode.") - lp_object.ButtonFlush() # special case? @@ -251,6 +235,21 @@ def connect_lp(self): self.stat["text"] = f"Connected to {lpcon().get_display_name(lp)}" self.stat["bg"] = STAT_ACTIVE_COLOR + if IsStandalone(): + self.popup_choice(self, "LPHK Standalone mode", self.error_image, + """LPHK has been started in standalone mode and +will not try to connect to a Launchpad. Execute +buttons by right clicking on them.""", + [["OK", None]] + ) + elif lp_mode is LP_PRO: + self.popup(self, "Connect to Launchpad Pro", self.error_image, + """This is a BETA feature! The Pro is not fully supported yet,as the bottom and left rows are not mappable currently. +I (nimaid) do not have a Launchpad Pro to test with, so let me know if this does or does not work on the Discord! (https://discord.gg/mDCzB8X) +You must first put your Launchpad Pro in Live (Session) mode. To do this, press and holde the 'Setup' key, press the green pad in the +upper left corner, then release the 'Setup' key. Please only continue once this step is completed.""", + "I am in Live mode.") + # load a layout on startup def load_initial_layout(self): if global_vars.ARGS['layout']: # did the user pass the option to load an initial layout? @@ -274,7 +273,7 @@ def disconnect_lp(self): self.disable_menu(MENU_LAYOUT) self.disable_menu(MENU_SUBROUTINES) - self.stat["text"] = "No Launchpad Connected 2" # @@@ + self.stat["text"] = "No Launchpad Connected" self.stat["bg"] = STAT_INACTIVE_COLOR def redetect_lp(self): @@ -305,7 +304,7 @@ def load_subroutines(self): title="Load subroutines", filetypes=load_subroutine_filetypes) # get the filename if name: - files.load_subroutines_to_lp(name) # and load routines if a file was selected + files.load_subroutines_to_lp(name) # and load routines if a file was selected # user requests clearing all subroutines def clear_subroutines(self): @@ -332,29 +331,43 @@ def save_layout(self): def click(self, event): gap = int(BUTTON_SIZE // 4) - column = min(8, int(event.x // (BUTTON_SIZE + gap))) row = min(8, int(event.y // (BUTTON_SIZE + gap))) + # ignore button clicks outside the button. + if event.x < (gap/2 + column * (BUTTON_SIZE + gap)) or event.x > (column+1) * (BUTTON_SIZE + gap) - gap/2 - 1: + return + if event.y < (gap/2 + row * (BUTTON_SIZE + gap)) or event.y > (row+1) * (BUTTON_SIZE + gap) - gap/2 - 1: + return + if self.grid_drawn: if(column, row) == (8, 0): - #mode change + #mode change self.last_clicked = None - if self.button_mode == "edit": - self.button_mode = "move" - elif self.button_mode == "move": - self.button_mode = "swap" - elif self.button_mode == "swap": - self.button_mode = "copy" + if self.button_mode == LM_EDIT: + self.button_mode = LM_MOVE + elif self.button_mode == LM_MOVE: + self.button_mode = LM_SWAP + elif self.button_mode == LM_SWAP: + self.button_mode = LM_COPY + elif self.button_mode == LM_COPY: + self.button_mode = LM_RUN else: - self.button_mode = "edit" + self.button_mode = LM_EDIT self.draw_canvas() else: - if self.button_mode == "edit": + if self.button_mode == LM_EDIT: self.last_clicked = (column, row) self.draw_canvas() self.script_entry_window(column, row) self.last_clicked = None + elif self.button_mode == LM_RUN: + # queue up a button press & release + if IsStandalone(): + from launchpad_fake import AddEvent + AddEvent([column, row, True]) + AddEvent([column, row, False]) + pass else: if self.last_clicked == None: self.last_clicked = (column, row) @@ -363,17 +376,17 @@ def click(self, event): swap_func = partial(scripts.swap, self.last_clicked[0], self.last_clicked[1], column, row) copy_func = partial(scripts.copy, self.last_clicked[0], self.last_clicked[1], column, row) - if self.button_mode == "move": + if self.button_mode == LM_MOVE: if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to move a button to an already\nbound button. What would you like to do?", [["Overwrite", move_func], ["Swap", swap_func], ["Cancel", None]]) else: move_func() - elif self.button_mode == "copy": + elif self.button_mode == LM_COPY: if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to copy a button to an already\nbound button. What would you like to do?", [["Overwrite", copy_func], ["Swap", swap_func], ["Cancel", None]]) else: copy_func() - elif self.button_mode == "swap": + elif self.button_mode == LM_SWAP: swap_func() self.last_clicked = None self.draw_canvas() From 03e96d22077c5b2ef12b5e9288b1b68d94026357 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 10 Mar 2021 17:15:45 +0800 Subject: [PATCH 50/83] Lots of bug fixes, but debug code remains (although largely commented out) * command_base.py Correction to Param Validation Count to allow for optional and multiple parameters * commands_control.py Addition of return command to return from subroutines (or exit a main procedure) * commands_control.py Modification to END and ABORT commands to exit the entire execution of a button (and not simply return) * commands_rpncalc.py Modification to "getting" of variables to be flexible with types * commands_subroutines.py depth of subroutine call must be set to type integer now * commands_test.py full(?) testing of most single parameter types * LPHK.py addition of -q/--quiet argument to supress informational dialogs (there's just 1 at the moment, the alert that you're using an emulated launchpad) * param_convs.py Modification of parameter type setting to flexibly handle whatever is thrown at it. * window.py implementation of --quiet to prevent a popup --- LPHK.py | 3 +++ command_base.py | 47 +++++++++++++++++++++++--------- commands_control.py | 35 +++++++++++++++++++++--- commands_rpncalc.py | 7 ++--- commands_subroutines.py | 3 ++- commands_test.py | 52 ++++++++++++++++++++++++++++++----- param_convs.py | 60 ++++++++++++++++++++++++++++++++--------- variables.py | 2 +- window.py | 9 ++++--- 9 files changed, 173 insertions(+), 45 deletions(-) diff --git a/LPHK.py b/LPHK.py index 57dbdfa..3f1019b 100755 --- a/LPHK.py +++ b/LPHK.py @@ -113,6 +113,9 @@ def init(): ap.add_argument( # option to start with launchpad window in a particular mode "-M", "--mode", help = "Launchpad mode", type=str, choices=[LM_EDIT, LM_MOVE, LM_SWAP, LM_COPY, LM_RUN], default=LM_EDIT) + ap.add_argument( # reimnplementation of debug (-d or --debug) + "-q", "--quiet", + help = "Disable information popups", action="store_true") global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to diff --git a/command_base.py b/command_base.py index 04ba2af..5b28f6d 100644 --- a/command_base.py +++ b/command_base.py @@ -229,7 +229,9 @@ def Partial_run_step_init(self, ret, btn, idx, split_line): # If you need more temporary data, you can override this, call the ancestor, and # create what you need. + #print(f"run_step_init '{split_line}', {self.Param_validation_count(len(split_line)-1)}")#@@@ btn.symbols[SYM_PARAMS] = [self.name] + [None] * self.Param_validation_count(len(split_line)-1) + #print(btn.symbols[SYM_PARAMS])#@@@ btn.symbols[SYM_PARAM_CNT] = 0 return ret @@ -363,11 +365,24 @@ def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. # This function improves efficiency. - vmp = self.valid_max_params - if (vmp == None) or (vmp < n_passed): + #print(f"vmp = {self.valid_max_params}, vnp = {self.valid_num_params}, n = {n_passed}")#@@@ + vmp = self.valid_max_params # what is the max number of parameters + if vmp == None: + vnp = self.valid_num_params # if there isn't a max, use the number of parameters + if vnp[0] == None: # if we really don't know + v = 0 # assume 0 + elif vnp[-1] == None: # if there is an unlimited maximum + v = vnp[-2] # use the max that we know + else: + return vnp[-1] # use the actual maximum + else: + v = vmp # vmp is preferred though + + #print(f"vmp = {vmp}, v = {v}, n = {n_passed}")#@@@ + if (v == None) or (v < n_passed): return n_passed else: - return vmp + return v def Validate_params(self, ret, btn, idx, split_line, val_validation): @@ -496,24 +511,25 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): if pass_no == 1: v = split_line[n] - print("V1", v) #@@@ + #print("V1", v) #@@@ if av[AV_VAR_OK] == AVV_YES: # if a variable is allowed if variables.valid_var_name(v): # if it's a variable v = variables.get_value(split_line[n], btn.symbols) # get the value - print("V2", v) #@@@ + #print("V2a", v) #@@@ if av[AV_VAR_OK] != AVV_REQD: # if it's not required (i.e. the variable name is not to be passed through) if av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: # if there is a type + #print("V2ba", v, type(v), av[AV_TYPE][AVT_CONV]) #@@@ v = av[AV_TYPE][AVT_CONV](v) # convert the variable to that type - print("V2", v) #@@@ + #print("V2bb", v) #@@@ return v elif pass_no == 2: ok = ret if av[AV_P1_VALIDATION]: - print("S", btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ + #print("S", btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ ok = av[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) if ok != True: print("[" + self.lib + "] " + btn.coords + " " + ok) @@ -545,24 +561,29 @@ def Get_param(self, btn, n, other=None): if self.Param_count(btn) > n: param = None - print("P-none", self.Param_count(btn), n) + #print("P-none", self.Param_count(btn), n)#@@@ else: + #print("P-n", self.Param_count(btn), n)#@@@ param = btn.symbols[SYM_PARAMS][n] - print("P-n", n) - print("P", param) #@@@ + #print("P", param) #@@@ if param == None: ret = other + #print(f"ret- {ret}") #@@@ else: if av[AV_VAR_OK] == AVV_REQD: - print("RQ") #@@@ + #print("RQ") #@@@ ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) + #print(f"ret0 {ret}") #@@@ else: - print("NRQ") #@@@ + #print("NRQ") #@@@ if type(param) == str and av[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': ret = param[1:] + #print(f"ret1 {ret}") #@@@ else: ret = param + #print(f"ret2 {ret}") #@@@ + #print(f"get_param '{ret}', {type(ret)} --> '{av[AV_TYPE][AVT_CONV](ret)}', {type(av[AV_TYPE][AVT_CONV](ret))}using '{av[AV_TYPE][AVT_CONV]}'") #@@@ return av[AV_TYPE][AVT_CONV](ret) @@ -576,7 +597,7 @@ def Set_param(self, btn, n, val): m = min(n, avl) av = self.auto_validate[m-1] if av[AV_VAR_OK] == AVV_REQD: - print("SP", btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ + #print("SP", btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable diff --git a/commands_control.py b/commands_control.py index fd03b1c..51ae0d6 100644 --- a/commands_control.py +++ b/commands_control.py @@ -523,6 +523,31 @@ def Run( scripts.Add_command(Control_Reset_Repeats()) # register the command +# ################################################## +# ### CLASS Control_Return ### +# ################################################## + +# class that defines the RETURN command +# +# This differs from END and ABORT (that will abort the execution of a button) in that will returnfrom a +# subroutine without exiting +class Control_Return(command_base.Command_Text_Basic): + def __init__( + self, + ): + + super().__init__("RETURN", # the name of the command as you have to enter it in the code + LIB, + "SCRIPT RETURNS" ) + + + def Process(self, btn, idx, split_line): + return -1 + + +scripts.Add_command(Control_Return()) # register the command + + # ################################################## # ### CLASS Control_End ### # ################################################## @@ -532,17 +557,19 @@ def Run( # This command simply ends the current script. I'm going to be working on subroutines, so this is a good # start. The parameters to this command are simply the message it will print. # This is really like a comment that returns the next line as -1 -class Control_End(command_base.Command_Text_Basic): +class Control_End(Control_Return): def __init__( self, ): - super().__init__("END", # the name of the command as you have to enter it in the code - LIB, - "SCRIPT ENDED" ) + super().__init__() + + self.name = "END" + self.info_msg = "SCRIPT ENDED" def Process(self, btn, idx, split_line): + btn.root.thread.kill.set() return -1 diff --git a/commands_rpncalc.py b/commands_rpncalc.py index aeba148..1a27d2c 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -123,6 +123,7 @@ def Process(self, btn, idx, split_line): i = i + o_ret except: print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") + raise #@@@ break else: # if invalid, report it print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") @@ -504,7 +505,7 @@ def rcl(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - a = variables.get(v, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1], _int) # as an integer + a = variables.get(v, symbols[SYM_LOCAL], symbols[SYM_GLOBAL][1], param_convs._any) # as an integer variables.push(symbols, a) return ret @@ -514,7 +515,7 @@ def rcl_l(self, symbols, cmd, cmds): # recalls a local variable (not overly useful, but avoids ambiguity) ret = 1 ret, v = variables.next_cmd(ret, cmds) - a = variables.get(v, symbols[SYM_LOCAL], None, _int) # as an integer + a = variables.get(v, symbols[SYM_LOCAL], None, param_convs._any) # as an integer variables.push(symbols, a) return ret @@ -525,7 +526,7 @@ def rcl_g(self, symbols, cmd, cmds): ret = 1 ret, v = variables.next_cmd(ret, cmds) with symbols[SYM_GLOBAL][0]: # lock the globals while we do this - a = variables.get(v, None, symbols[SYM_GLOBAL][1], _int) # grab the value from the global vars as an integer + a = variables.get(v, None, symbols[SYM_GLOBAL][1], param_convs._any)# grab the value from the global vars as an integer variables.push(symbols, a) # and push onto the stack return ret diff --git a/commands_subroutines.py b/commands_subroutines.py index 3b75680..bea4512 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -1,4 +1,4 @@ -import command_base, commands_header, scripts, variables +import command_base, commands_header, scripts, variables, param_convs from constants import * LIB = "cmds_subr" # name of this library (for logging) @@ -87,6 +87,7 @@ def Process(self, btn, idx, split_line): variables.Local_store('sub__np', self.Param_count(btn), sub_btn.symbols) # number of parameters passed d = variables.Local_recall('sub__d',btn.symbols) # get current call depth + d = param_convs._int(d) # create an integer from it variables.Local_store('sub__d', d+1, sub_btn.symbols) # and pass that + 1 #@@@ will fail with multiple parameters at the end! diff --git a/commands_test.py b/commands_test.py index 193d3c5..dfe6aa4 100644 --- a/commands_test.py +++ b/commands_test.py @@ -4,17 +4,33 @@ LIB = "cmds_test" # name of this library (for logging) class Test_XX(command_base.Command_Basic): + def clean(self, s): # remove stuff that changes (memory addresses) + p = s.find(" at 0x") + if p >=0: + s = self.clean(s[:p] + s[p+22:]) + return s + + def Process(self, btn, idx, split_line): - print("============================") + print("============================START") print("=", self.name, split_line) - print("=", f"Symbols before = {btn.symbols}") + print("=", self.clean(f"{self.auto_validate}")) + before = self.clean(f"{btn.symbols}") + print("=", f"Symbols before = {before}") a = self.Get_param(btn, 1) - print("=", f"Param = '{a}'") + print("=", f"Param = '{a}', {type(a)}") if a != None: - a += 1 + print("=", f"adding = '{self.one}', {type(self.one)}") + a += self.one self.Set_param(btn, 1, a) - print("=", f"Symbols after = {btn.symbols}") - print("============================") + print("=", f"Modified param = '{a}', {type(a)}") + after = self.clean(f"{btn.symbols}") + print("=", f"Symbols after = {after}") + if before == after: + print("= No change to stack") + else: + print("= STACK CHANGED") + print("============================END") class Test_01(Test_XX): @@ -33,6 +49,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_01()) # register the command @@ -53,6 +71,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_02()) # register the command @@ -73,6 +93,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_03()) # register the command @@ -93,6 +115,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_04()) # register the command @@ -113,6 +137,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_11()) # register the command @@ -133,6 +159,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_12()) # register the command @@ -153,6 +181,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_13()) # register the command @@ -173,6 +203,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_14()) # register the command @@ -193,6 +225,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_21()) # register the command @@ -213,6 +247,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = 1 + scripts.Add_command(Test_22()) # register the command @@ -233,6 +269,8 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_23()) # register the command @@ -253,5 +291,7 @@ def __init__( (1, " Param {1}"), ) ) + self.one = "1" + scripts.Add_command(Test_24()) # register the command diff --git a/param_convs.py b/param_convs.py index b92017d..49297eb 100644 --- a/param_convs.py +++ b/param_convs.py @@ -2,28 +2,62 @@ # int conversion with sensible None handling def _int(x): - if x == None or (isinstance(x, str) and x.strip() == ""): - return 0 - else: - return int(x) + if x == None or (isinstance(x, str) and x.strip() == ""): + return 0 + elif isinstance(x, str): + x = x.strip() + try: + return int(x) + except: + try: + return int(float(x)) + except: + return 0 + else: + return int(x) # float conversion with sensible None handling def _float(x): - if x == None or (isinstance(x, str) and x.strip() == ""): - return 0.0 - else: - return float(x) + if x == None or (isinstance(x, str) and x.strip() == ""): + return 0.0 + elif isinstance(x, str): + x = x.strip() + try: + return float(x) + except: + return 0 + else: + return float(x) # string conversion with sensible None handling def _str(x): - if x == None: - return "" - else: - return x + if x == None: + return "" + else: + return str(x) # no conversion def _None(x): - return x \ No newline at end of file + return x + + +# int conversion with sensible any handling (converts to what we can convert it to) +def _any(x): + if x == None: + return 0 + elif isinstance(x, str): + x = x.strip() + try: + return int(x) + except: + try: + return float(x) + except: + return 0 + else: + return str(x) + + diff --git a/variables.py b/variables.py index 94b3b12..b1af8cc 100644 --- a/variables.py +++ b/variables.py @@ -58,7 +58,7 @@ def get(name, l_vbls, g_vbls, default=param_convs._None): def put(name, val, vbls): # store a value in a named variable in a specific variable list - print("NV", name, val) #@@@ + #print("NV", name,run val) #@@@ vbls[name.lower()] = val diff --git a/window.py b/window.py index d612c49..697a52a 100644 --- a/window.py +++ b/window.py @@ -236,12 +236,13 @@ def connect_lp(self): self.stat["bg"] = STAT_ACTIVE_COLOR if IsStandalone(): - self.popup_choice(self, "LPHK Standalone mode", self.error_image, - """LPHK has been started in standalone mode and + if not global_vars.ARGS['quiet']: + self.popup_choice(self, "LPHK Standalone mode", self.error_image, + """LPHK has been started in standalone mode and will not try to connect to a Launchpad. Execute buttons by right clicking on them.""", - [["OK", None]] - ) + [["OK", None]] + ) elif lp_mode is LP_PRO: self.popup(self, "Connect to Launchpad Pro", self.error_image, """This is a BETA feature! The Pro is not fully supported yet,as the bottom and left rows are not mappable currently. From 779fb232df0a706528ce031796f993ee54bffec9 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 10 Mar 2021 20:03:27 +0800 Subject: [PATCH 51/83] More bugs fixed, and the start of dialogs * command_base.py Bug fixed where a command requires 0 parameters * command_list.py Now dumps the stack if an error occurs while loading commands_test.py * commands_test.py Added a command to test a simple dialog box * window.py updated a dialog box to give the correct information aBOUT RUN MODE * dialog.py New module to handle dialogs. * dialog.py Creates a sync_queue task that can maintain a threadsafe queue that understands buttons being killed * dialog.py Implements a method to pull a request off the queue if it has not yet been actioned * dialog.py A standard function that puts a request on the queue and waits for either it to return or the button to be killed. If the button is killed it tries to pull the request. * dialog.py A function to queue a request for a simple info dialog box * dialog.py NOTE: no code is yet implemented to execute a request for a dialog, but buttons that request one can still be correctly killed. --- command_base.py | 2 +- command_list.py | 1 + commands_test.py | 25 +++++++++ dialog.py | 135 +++++++++++++++++++++++++++++++++++++++++++++++ window.py | 2 +- 5 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 dialog.py diff --git a/command_base.py b/command_base.py index 5b28f6d..9859273 100644 --- a/command_base.py +++ b/command_base.py @@ -369,7 +369,7 @@ def Param_validation_count(self, n_passed): vmp = self.valid_max_params # what is the max number of parameters if vmp == None: vnp = self.valid_num_params # if there isn't a max, use the number of parameters - if vnp[0] == None: # if we really don't know + if vnp == None or vnp[0] == None: # if we really don't know v = 0 # assume 0 elif vnp[-1] == None: # if there is an unlimited maximum v = vnp[-2] # use the max that we know diff --git a/command_list.py b/command_list.py index 50172c1..6843779 100644 --- a/command_list.py +++ b/command_list.py @@ -19,6 +19,7 @@ try: import commands_test except: + traceback.print_exc() pass # This library could be considered optional, but is not platform specific diff --git a/commands_test.py b/commands_test.py index dfe6aa4..428c85b 100644 --- a/commands_test.py +++ b/commands_test.py @@ -295,3 +295,28 @@ def __init__( scripts.Add_command(Test_24()) # register the command + + +# class that defines the Delay command (a target of GOTO's etc) +class Test_Dialog(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TEST_DIALOG", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (0, " Dialog Test"), + ) ) + + + def Process(self, btn, idx, split_line): + import dialog + dialog.CommentBox(btn, "this is a test") + + +scripts.Add_command(Test_Dialog()) # register the command diff --git a/dialog.py b/dialog.py new file mode 100644 index 0000000..5d0deb9 --- /dev/null +++ b/dialog.py @@ -0,0 +1,135 @@ +# a routine that allows a single script at a time to access dialogs +import threading +from constants import * + + +DLG_INFO = 1 # a simple info box + + +class SyncQueue(): + def __init__(self): + self.queue = [] # the queue + self.lock = threading.Lock() # the lock to protect it + self.msg_id = 1 # and the id to return for each push to allow pull + + + # Acquire a lock + def acquire(self, btn=None): + if btn == None: # if there's no button + while not self.lock.acquire(True, -1): # wait forever for a lock + pass + else: + unlocked = False # we start locked + while not unlocked: # and until unlocked + btn.Safe_sleep(DELAY_EXIT_CHECK) # we take a short nap + if btn.Check_kill(): # and make sure we're not dead + return False # returning False if we are + + unlocked = self.lock.acquire(False) # but the main job is to attempt to acquire the lock without blocking) + + return True # if we're here, we have a lock + + + # push a value onto the queue. If a button is passed, do it in a way that + # doesn't stall things + def push(self, x, btn=None): + ok = self.acquire(btn) # try to get a lock + + if not ok: # error return for death notification + return -1 + + self.msg_id += 1 # increment the message id + m = self.msg_id # and store our local message id + + try: + self.queue.append((m, x)) # this is what we're here to do! + + except: + return -1 # unlikely, but something bad happened + + finally: + self.lock.release() # Always release the lock + + return m # and return msg_id on success + + + # pop a value off the queue. If a button is passed, do it in a way that + # doesn't stall things + def pop(self, x, btn=None): + ok = self.acquire(btn) # try to get a lock + + if not ok: # error return for death notification + return (False, None) + + try: + if len(self.queue) == 0: # if the queue is empty + return (True, None) # return success, but a value of None + return (True, self.queue.pop(x)) # otherwise return the head of the queue + + except: + return (False, None) # unlikely, but something bad happened + + finally: + self.lock.release() # Always release the lock + + + # removes a message off the queue. This is always allowed to stall + def pull(self, msg_id): + ok = self.acquire(None) # try to get a lock + + if not ok: # error return for weird situations (should never happen) + return False + + try: + for idx, (m_id, val) in enumerate(self.queue): # for all items in the queue + if m_id > msg_id: # if it's not on the queue + return False # it must bebeing handled + if m_id == msg_id: # found it! + del self.queue[idx] # remove it + return True # and that is success + + return False # error if we don't find it at all + + finally: + self.lock.release() # Always release the lock + + +DIALOG_QUEUE = SyncQueue() # create a queue object to synchronise requests for dialogs + + +# request any type of dialog +def Sync_Request(btn, m_type, args): + waiting = True # we're waiting (by default) + info = None # and we have nothing returned + + def EndWait(p_info): # callback routine to end the wait + nonlocal info + info = p_info # here is what we get back + + nonlocal waiting + waiting = False # and the flag telling us the wait is over + + msg = DIALOG_QUEUE.push((m_type, btn, EndWait, args), btn) # push the request for the dialog + if msg < 0: # in case of error + return (False, info) # we have failed + + while waiting: # while we're waiting + btn.Safe_sleep(DELAY_EXIT_CHECK) # we take a short nap + if btn.Check_kill(): # and make sure we're not dead + if DIALOG_QUEUE.pull(msg): # can we pull the request before it gets actioned? + return (False, info) # yep, return immediately + else: + while waiting: # otherwise just keep waiting + btn.Safe_sleep(DELAY_EXIT_CHECK) + return (False, info) # and return false when the dialog ends + + return (True, info) # a normal return + + +# request a simple comment box +def CommentBox(btn, message): + return Sync_Request(btn, DLG_INFO, message) # the arguments are simply the message + + + + \ No newline at end of file diff --git a/window.py b/window.py index 697a52a..d8ec3a2 100644 --- a/window.py +++ b/window.py @@ -240,7 +240,7 @@ def connect_lp(self): self.popup_choice(self, "LPHK Standalone mode", self.error_image, """LPHK has been started in standalone mode and will not try to connect to a Launchpad. Execute -buttons by right clicking on them.""", +buttons by clicking on them in 'Run' mode.""", [["OK", None]] ) elif lp_mode is LP_PRO: From b11875d989089cf5e2309008e3bb9f826ba7dfd7 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:18:52 +0800 Subject: [PATCH 52/83] Modifications to improve documentation * command_base.py name passed to Command_Basic can be followed by ", description" * command_base.py Command_Text_Basic has validation params defined for documentation purposes * command_base.py Restriction on headers needing to be on the first line only removed (because now we allow multiple headers) * commands_control.py Basic documentation added * commands_mouse.py Basic documentation added * commands_subroutines New class Header_Subroutine added to implement base behaviour and enforce not async * commands_subroutines.py class Header_Subroutine (@SUB) renamed to Header_Sub_name and inherits the new Header_Subtoutine. This also implements code to report an error if it is used in anything other than a Subroutine * commands_subroutines.py New class Header_Sub_Desc (@DESC) to allow description to be added to a subroutine or Button * commands_subroutines.py New class Header_Sub_Name (@NAME) to allow a button to be named (future use) * commands_subroutines.py New class Header_Sub_Doc (@DOC) to allow multi-line documentation to be added to buttons or subroutines * commands_subroutines.py passes name of subroutine to integral button object for easier access * commands_test.py Added basic documentation * commands_test.py Added commands to test dumping of commands (with and without debug) TEST_DUMP and Test_Dump_Debug * scripts.py removal of HEADERS - everything is now held in VALID_COMMANDS This simplifies Add_command and other methods * scripts.py New method Dump_commands that outputs a list of all headers, commands, subroutines, and buttons with name, description, parameters, documentation, and ancestory (for debug) * scripts.py Add desc and doc to button and method Set_name to recalculate the coords if the name is changed. * scripts.py While parsing script, produce more informative error messages for buttons and subroutines * scripts.py Modification to Validate_script to use VALID_COMMANDS and still identify headers.. --- command_base.py | 20 +++- commands_control.py | 28 +++--- commands_mouse.py | 18 ++-- commands_subroutines.py | 144 ++++++++++++++++++++++++++--- commands_test.py | 74 ++++++++++++--- scripts.py | 199 +++++++++++++++++++++++++++++++++------- 6 files changed, 396 insertions(+), 87 deletions(-) diff --git a/command_base.py b/command_base.py index 9859273..54354b6 100644 --- a/command_base.py +++ b/command_base.py @@ -20,7 +20,14 @@ def __init__( # and we rely on the parameters to the methods to contain things # unique to each one! Local variables are fine, self.anything is BAD - self.name = name # the literal name of our command + p = name.find(',') # is there a comma in the name? + if p > 0: # yes! must have a description + self.name = name[:p].strip() # extract the name part + self.desc = name[p+1:].strip() # and the description part + else: + self.name = name # the literal name of our command + self.desc = '' # no description + self.lib = lib # the library we're part of self.auto_validate = auto_validate # any auto-validation, if defined self.auto_message = auto_message # format for any messages we need @@ -377,7 +384,7 @@ def Param_validation_count(self, n_passed): return vnp[-1] # use the actual maximum else: v = vmp # vmp is preferred though - + #print(f"vmp = {vmp}, v = {v}, n = {n_passed}")#@@@ if (v == None) or (v < n_passed): return n_passed @@ -615,7 +622,10 @@ def __init__( super().__init__(name, # the name of the command as you have to enter it in the code lib, - (), + ( + # Desc Opt Var type p1_val p2_val + ("Param", False, AVV_NO, PT_TEXT, None, None), + ), () ) # this command does not have a standard list of fields, so we need to do some stuff manually @@ -658,8 +668,8 @@ def Validate( pass_no ): - if idx != 0: - return ("ERROR on line " + btn.Line(idx) + ". " + self.name + " must only appear on line 1.", -1) + #if idx != 0: + # return ("ERROR on line " + btn.Line(idx) + ". " + self.name + " must only appear on line 1.", -1) return (None, 0) diff --git a/commands_control.py b/commands_control.py index 51ae0d6..eb3881b 100644 --- a/commands_control.py +++ b/commands_control.py @@ -15,7 +15,7 @@ def __init__( self, ): - super().__init__("-", # the name of the command as you have to enter it in the code + super().__init__("-, Comment", LIB, "-" ) @@ -34,7 +34,7 @@ def __init__( ): super().__init__( - "LABEL", # the name of the command as you have to enter it in the code + "LABEL, Target for jumps (goto, loops, etc)", LIB, ( # Desc Opt Var type p1_val p2_val @@ -186,7 +186,7 @@ def __init__( ): super().__init__( - "GOTO_LABEL", # the name of the command as you have to enter it in the code + "GOTO_LABEL, Unconditional jump to label", LIB, ( # Desc Opt Var type p1_val p2_val @@ -212,7 +212,7 @@ def __init__( ): super().__init__( - "IF_PRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + "IF_PRESSED_GOTO_LABEL, Jump to label if initiating button still pressed", LIB, ( # desc opt var type p1_val p2_val @@ -241,7 +241,7 @@ def __init__( ): super().__init__( - "IF_UNPRESSED_GOTO_LABEL", # the name of the command as you have to enter it in the code + "IF_UNPRESSED_GOTO_LABEL, Jump to label if initiating button is NOT pressed", LIB, ( # desc opt var type p1_val p2_val @@ -270,7 +270,7 @@ def __init__( ): super().__init__( - "REPEAT_LABEL", # the name of the command as you have to enter it in the code + "REPEAT_LABEL, Jump to label a fixed number of times", LIB, ( # desc opt var type p1_val p2_val @@ -307,7 +307,7 @@ def __init__( ): super().__init__( - "REPEAT", # the name of the command as you have to enter it in the code + "REPEAT, Repeat a block of code a fixed number of times (auto reset -- can be nested)", LIB, ( # desc opt var type p1_val p2_val @@ -342,7 +342,7 @@ def __init__( ): super().__init__( - "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + "IF_PRESSED_REPEAT_LABEL, Jump to a label a fixed number of times IF initiating button still pressed", LIB, ( # desc opt var type p1_val p2_val @@ -379,7 +379,7 @@ def __init__( ): super().__init__( - "IF_PRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + "IF_PRESSED_REPEAT, Repeat a block of code a fixed number of times IF originating button still pressed (auto reset -- can be nested)", LIB, ( # desc opt var type p1_val p2_val @@ -414,7 +414,7 @@ def __init__( ): super().__init__( - "IF_UNPRESSED_REPEAT_LABEL", # the name of the command as you have to enter it in the code + "IF_UNPRESSED_REPEAT_LABEL, Jump to a label a fixed number of times IF initiating button released", LIB, ( # desc opt var type p1_val p2_val @@ -451,7 +451,7 @@ def __init__( ): super().__init__( - "IF_UNPRESSED_REPEAT", # the name of the command as you have to enter it in the code + "IF_UNPRESSED_REPEAT, Repeat a block of code a fixed number of times IF originating button is released (auto reset -- can be nested)", LIB, ( # desc opt var type p1_val p2_val @@ -488,7 +488,7 @@ def __init__( self, ): - super().__init__("RESET_REPEATS") # the name of the command as you have to enter it in the code + super().__init__("RESET_REPEATS, Resets all repeats to their initial values") def Validate( @@ -536,7 +536,7 @@ def __init__( self, ): - super().__init__("RETURN", # the name of the command as you have to enter it in the code + super().__init__("RETURN, Returns from a subroutine or exits a script", LIB, "SCRIPT RETURNS" ) @@ -565,6 +565,7 @@ def __init__( super().__init__() self.name = "END" + self.desc = "Ends an execution unconditionally (including if called from a subroutine)" self.info_msg = "SCRIPT ENDED" @@ -591,6 +592,7 @@ def __init__( super().__init__() self.name = "ABORT" + self.desc = "Aborts an execution unconditionally (including if called from a subroutine)" self.info_msg = "SCRIPT ABORTED" diff --git a/commands_mouse.py b/commands_mouse.py index 1b22d66..2142e78 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -13,7 +13,7 @@ def __init__( self, ): - super().__init__("M_MOVE", # the name of the command as you have to enter it in the code + super().__init__("M_MOVE, Relative mouse movement", LIB, # the name of this module ( # description of parameters # Desc Opt Var type p1_val p2_val (trailing comma is important) @@ -46,7 +46,7 @@ def __init__( self, ): - super().__init__("M_SET", # the name of the command as you have to enter it in the code + super().__init__("M_SET, Set mouse position", LIB, ( # Desc Opt Var type p1_val p2_val @@ -79,7 +79,7 @@ def __init__( self, ): - super().__init__("M_SCROLL", # the name of the command as you have to enter it in the code + super().__init__("M_SCROLL, Scroll using mouse", LIB, ( # Desc Opt Var type p1_val p2_val @@ -113,7 +113,7 @@ def __init__( self, ): - super().__init__("M_LINE", # the name of the command as you have to enter it in the code + super().__init__("M_LINE, Move the mouse along a line", LIB, ( # Desc Opt Var type p1_val p2_val @@ -169,7 +169,7 @@ def __init__( self, ): - super().__init__("M_LINE_MOVE", # the name of the command as you have to enter it in the code + super().__init__("M_LINE_MOVE, Relative mouse movement", LIB, ( # Desc Opt Var type p1_val p2_val @@ -222,7 +222,7 @@ def __init__( self, ): - super().__init__("M_LINE_SET", # the name of the command as you have to enter it in the code + super().__init__("M_LINE_SET, Absolute mouse movement", LIB, ( # Desc Opt Var type p1_val p2_val @@ -271,7 +271,7 @@ def __init__( self, ): - super().__init__("M_RECALL_LINE", # the name of the command as you have to enter it in the code + super().__init__("M_RECALL_LINE, Return the mouse to the stored position", LIB, ( # Desc Opt Var type p1_val p2_val @@ -328,7 +328,7 @@ def __init__( self, ): - super().__init__("M_STORE", # the name of the command as you have to enter it in the code + super().__init__("M_STORE, Store the mouse position", LIB, ( # Desc Opt Var type p1_val p2_val @@ -365,7 +365,7 @@ def __init__( self, ): - super().__init__("M_RECALL", # the name of the command as you have to enter it in the code + super().__init__("M_RECALL, Return the mouse to the stored position", ( # no variables defined, so none are allowed ), diff --git a/commands_subroutines.py b/commands_subroutines.py index bea4512..667cc06 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -12,15 +12,40 @@ # ### CLASS Header_Subroutine ### # ################################################## +# This forms the basis of the subroutine headers. It should not be instansiated by itself! +class Header_Subroutine(command_base.Command_Header): + def __init__( + self, + name + ): + + super().__init__(name, + False) # subroutines are not async! + + + # Dummy run routine. Simply passes execution to the next line + def Run( + self, + btn, + idx: int, # The current line number + split_line # The current line, split + ): + + return idx+1 + + +# ################################################## +# ### CLASS Header_Sub_Name ### +# ################################################## + # This is a dummy header. It is interpreted for real when a subroutine is loaded, # but is ignored in the normal running of commands -class Header_Subroutine(command_base.Command_Header): +class Header_Sub_Name(Header_Subroutine): def __init__( self, ): - super().__init__("@SUB", # the name of the header as you have to enter it in the code - False) # You also define if the header causes the script to be asynchronous + super().__init__("@SUB, Defines a subroutine name and parameters") # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) @@ -32,24 +57,113 @@ def Validate( pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if btn.is_button == True: - return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is not permitted in a button.", btn.Line(idx)) + if pass_no == 1: + if btn.is_button: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a subroutine.", btn.Line(idx)) return True - # Dummy run routine. Simply passes execution to the next line - def Run( +scripts.Add_command(Header_Sub_Name()) # register the header + + +# ################################################## +# ### CLASS Header_Sub_Desc ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Sub_Desc(Header_Subroutine): + def __init__( + self, + ): + + super().__init__("@DESC, Defines a subroutine description") + + + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) + def Validate( self, btn, idx: int, # The current line number - split_line # The current line, split + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - return idx+1 + if pass_no == 1: + btn.desc = ' '.join(split_line[1:]) + + return True + + +scripts.Add_command(Header_Sub_Desc()) # register the header + + +# ################################################## +# ### CLASS Header_Sub_Name ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Sub_Name(Header_Subroutine): + def __init__( + self, + ): + + super().__init__("@NAME, (re)names a subroutine description") + + + # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if btn.is_button: + btn.Set_name(' '.join(split_line[1:])) + else: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a button.", btn.Line(idx)) + + return True + + +scripts.Add_command(Header_Sub_Name()) # register the header + + +# ################################################## +# ### CLASS Header_Sub_Doc ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Sub_Doc(Header_Subroutine): + def __init__( + self, + ): + + super().__init__("@DOC, Adds a line to the subroutine documentation") + + + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + btn.doc += [' '.join(split_line[1:])] + + return True -scripts.Add_command(Header_Subroutine()) # register the header +scripts.Add_command(Header_Sub_Doc()) # register the header # ################################################## @@ -75,18 +189,18 @@ def __init__( ) ) self.routine = Lines # the routine to execute - self.btn = scripts.Button(-1, -1, self.routine) # we retain this so we only have to validate it once. executions use a deep-ish copy + self.btn = scripts.Button(-1, -1, self.routine, None, Name) # we retain this so we only have to validate it once. executions use a deep-ish copy # process for a subroutine handles parameter passing and then passes off the process to the script in a "dummy" button def Process(self, btn, idx, split_line): - sub_btn = scripts.Button(-1, -1, self.routine, btn.root) # create a new button and pass the script to it + sub_btn = scripts.Button(-1, -1, self.routine, btn.root, self.name) # create a new button and pass the script to it self.btn.Copy_parsed(sub_btn, self.name) # copy the info created when parsed variables.Local_store('sub__np', self.Param_count(btn), sub_btn.symbols) # number of parameters passed - d = variables.Local_recall('sub__d',btn.symbols) # get current call depth + d = variables.Local_recall('sub__d', btn.symbols) # get current call depth d = param_convs._int(d) # create an integer from it variables.Local_store('sub__d', d+1, sub_btn.symbols) # and pass that + 1 @@ -105,10 +219,10 @@ def Process(self, btn, idx, split_line): self.Set_param(btn, n+1, pn) # and store it - # This is not the parse routine called for validation! + # This is not the parse routine called for validation! @@@ not used??? def Parse_Sub(self): try: - script_validate = self.btn.Parse_script() + script_validate = self.btn.Parse_script() #@@@ does not raise an error except: self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") raise diff --git a/commands_test.py b/commands_test.py index 428c85b..b300598 100644 --- a/commands_test.py +++ b/commands_test.py @@ -38,7 +38,7 @@ def __init__( self, ): - super().__init__("TEST_01", # the name of the command as you have to enter it in the code + super().__init__("TEST_01, Test for single optional integer constant parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -60,7 +60,7 @@ def __init__( self, ): - super().__init__("TEST_02", # the name of the command as you have to enter it in the code + super().__init__("TEST_02, Test for single optional float constant parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -82,7 +82,7 @@ def __init__( self, ): - super().__init__("TEST_03", # the name of the command as you have to enter it in the code + super().__init__("TEST_03, Test for single optional string constant parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -104,7 +104,7 @@ def __init__( self, ): - super().__init__("TEST_04", # the name of the command as you have to enter it in the code + super().__init__("TEST_04, Test for single optional multi-string constant parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -126,7 +126,7 @@ def __init__( self, ): - super().__init__("TEST_11", # the name of the command as you have to enter it in the code + super().__init__("TEST_11, Test for single optional integer constant/variable parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -148,7 +148,7 @@ def __init__( self, ): - super().__init__("TEST_12", # the name of the command as you have to enter it in the code + super().__init__("TEST_12, Test for single optional float constant/variable parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -170,7 +170,7 @@ def __init__( self, ): - super().__init__("TEST_13", # the name of the command as you have to enter it in the code + super().__init__("TEST_13, Test for single optional string constant/variable parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -192,7 +192,7 @@ def __init__( self, ): - super().__init__("TEST_14", # the name of the command as you have to enter it in the code + super().__init__("TEST_14, Test for single optional multi-string constant/variable parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -214,7 +214,7 @@ def __init__( self, ): - super().__init__("TEST_21", # the name of the command as you have to enter it in the code + super().__init__("TEST_21, Test for single optional integer by-ref parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -236,7 +236,7 @@ def __init__( self, ): - super().__init__("TEST_22", # the name of the command as you have to enter it in the code + super().__init__("TEST_22, Test for single optional float by-ref parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -258,7 +258,7 @@ def __init__( self, ): - super().__init__("TEST_23", # the name of the command as you have to enter it in the code + super().__init__("TEST_23, Test for single optional string by-ref parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -280,7 +280,7 @@ def __init__( self, ): - super().__init__("TEST_24", # the name of the command as you have to enter it in the code + super().__init__("TEST_24, Test for single optional multi-string by-ref parameter", LIB, ( # Desc Opt Var type p1_val p2_val @@ -303,7 +303,7 @@ def __init__( self, ): - super().__init__("TEST_DIALOG", # the name of the command as you have to enter it in the code + super().__init__("TEST_DIALOG, Test to display a simple dialog", LIB, ( # Desc Opt Var type p1_val p2_val @@ -320,3 +320,51 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Test_Dialog()) # register the command + + +# class that dumps all known commands +class Test_Dump(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TEST_DUMP, Dump all headers and commands", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (0, " Dump headers and commands"), + ) ) + + + def Process(self, btn, idx, split_line): + scripts.Dump_commands() + + +scripts.Add_command(Test_Dump()) # register the command + + +# class that dumps all known commands +class Test_Dump_Debug(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TEST_DUMP_DEBUG, Dump all headers and commands with ancestory", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (0, " Dump headers and commands"), + ) ) + + + def Process(self, btn, idx, split_line): + scripts.Dump_commands(True) + + +scripts.Add_command(Test_Dump_Debug()) # register the command diff --git a/scripts.py b/scripts.py index b004077..6246b22 100644 --- a/scripts.py +++ b/scripts.py @@ -11,53 +11,170 @@ VALID_COMMANDS = dict() -# HEADERS is likewise empty until added (all headers, not just async ones) - -HEADERS = dict() - - # GLOBALS is likewise empty until global variables get created GLOBALS = dict() # the globals themselvs GLOBAL_LOCK = threading.Lock() # a lock for the globals to prevent simultaneous access - - # Add a new command. This removes any existing command of the same name from the VALID_COMMANDS # and returns it as the result def Add_command( - a_command: command_base.Command_Basic # the command to add + a_command: command_base.Command_Basic # the command or header to add ): - if a_command.name in HEADERS: # if this was previously a header, now it isn't - HEADERS.pop(a_command.name) - - if a_command.name in VALID_COMMANDS: # if it already exists - p = VALID_COMMANDS[a_command.name] # get it - else: # otherwise - p = None # the return value will be None + if a_command.name in VALID_COMMANDS: # or if it was a command + p = VALID_COMMANDS.pop(a_command.name) # pop that too + else: # otherwise + p = None # the return value will be None (we're not replacing anything) - VALID_COMMANDS[a_command.name] = a_command # add the new command + VALID_COMMANDS[a_command.name] = a_command # add the new command - if a_command is command_base.Command_Header: # is this a header? - HEADERS[a_command.name] = a_command.is_async # add it + return p # return any replaced command - return p # return any replaced command - -# Remove a command. This could be useful in handling subroutines (@@@ UNTESTED) +# Remove a command. This could be useful in handling subroutines def Remove_command( command_name # the command to remove ): - if command_name in HEADERS: # if this was previously a header - HEADERS.pop(command_name) # remove the header + if command_name in VALID_COMMANDS: # check command + p = VALID_COMMANDS.pop(command_name) # remove the command + else: + p = None # nothing to remove - if command_name in VALID_COMMANDS: # if it already exists - VALID_COMMANDS.pop(command_name) # remove the command + return p # the thing we removed + + +# display info on all commands and headers + +def Dump_commands(debug=False): + def get_name(c): + if isinstance(c, command_base.Command_Basic): + return c.name + elif isinstance(c, Button): + return c.coords + else: + return "ERROR" + + def get_desc(c): + ret = '' + if isinstance(c, command_base.Command_Basic): + if hasattr(c, 'desc') and not callable(c.desc): + ret = c.desc + if hasattr(c, 'btn') and not callable(c.btn) and c.btn: + if c.btn.desc != "": + ret = c.btn.desc + elif isinstance(c, Button): + ret = c.desc + else: + ret = "ERROR" + + return ret + + def dump_name(c_type, c): + print(f" {c_type} \"{get_name(c)}\"", end="") + desc = get_desc(c) + if desc == "": + print() + else: + print(f" - {desc}") + + def get_doc(c): + ret = [] + if isinstance(c, command_base.Command_Basic): + if hasattr(c, 'doc') and not callable(c.doc): + ret = c.doc + if hasattr(c, 'btn') and not callable(c.btn) and c.btn: + if c.btn.doc != []: + ret = c.btn.doc + elif isinstance(c, Button): + ret = c.doc + else: + ret = ["ERROR"] + + return ret + + def dump_doc(c): + doc = get_doc(c) + if doc != []: + print(" Notes") + for n in doc: + print(f" {n}") + + def dump_ancestory(c): + print(" Ancestory") + print(f" {type(c)}") + a = type(c).__bases__[0] + while a != object: + print(f" {a}") + a = a.__bases__[0] + + def dump_params(c): + if isinstance(c, command_base.Command_Basic): + print(" Parameters") + if c.auto_validate == None: + print(" Parameters undocumented (Auto-validation is not defined)") + elif len(c.auto_validate) == 0: + print(" No parameters") + else: + for v in c.auto_validate: + print(f" {v[AV_DESCRIPTION]} - {v[AV_TYPE][AVT_DESC]}", end="") + + if v[AV_OPTIONAL]: + print(" (opt),", end="") + else: + print(" (reqd),", end="") + + if v[AV_VAR_OK] == AVV_NO: + print(" constant only") + elif v[AV_VAR_OK] == AVV_YES: + print(" variable permitted") + elif v[AV_VAR_OK] == AVV_REQD: + print(" variable required (possible return value)") + else: + print(" UNKNOWN VALUE") + + def dump(c_type, c, debug): + dump_name(c_type, c) + dump_doc(c) + if debug: + dump_ancestory(c) + dump_params(c) + + print() + + import commands_subroutines + + print("HEADERS") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], command_base.Command_Header): + dump("Header", VALID_COMMANDS[cmd], debug) + + print("COMMANDS") + print() + for cmd in VALID_COMMANDS: + if not (isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine) or \ + isinstance(VALID_COMMANDS[cmd], command_base.Command_Header)): + dump("Command", VALID_COMMANDS[cmd], debug) + + print("SUBROUTINES") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): + dump("Subroutine", VALID_COMMANDS[cmd], debug) + + print("BUTTONS") + print() + global buttons + for x in range(8): + for y in range(1, 9): + btn = buttons[x][y] + if btn.script_str != "": + dump("Button", btn, debug) # Create a new symbol table. This contains information required for the script to run @@ -91,23 +208,25 @@ def __init__( x, # The button column y, # The button row script_str, # The Script - root = None # Who called us + root = None, # Who called us + name = '' # name of this button (subroutine) ): self.x = x self.y = y self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine self.script_str = script_str # The script + + self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? + self.desc = "" + self.doc = [] + self.validated = False # Has the script been validated? self.symbols = None # The symbol table for the button self.script_lines = None # the lines of the script self.thread = None # the thread associated with this button self.running = False # is the script running? self.is_async = False # async execution flag - if self.is_button: - self.coords = "(" + str(self.x) + ", " + str(self.y) + ")" # let's just do this the once eh? - else: - self.coords = "(SUB)" # subroutines don't have coordinates # The "root" is the button that is scheduled. This allows subroutines to check if the # initiating button has been killed. @@ -117,6 +236,17 @@ def __init__( self.root = root # the caller is the root + # let us set/change the name of a button + def Set_name(self, name): + self.name = name + self.coords = '' + + if self.is_button: + self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? + if name: + self.coords = " ".join([self.name, self.coords]) # subroutines don't have coordinates + + # Do what is required to parse the script. Parsing does not output any information unless it is an error def Parse_script(self): if self.validated: # we don't want to repeat validation over and over @@ -165,7 +295,10 @@ def Parse_script(self): errors += 1 # and 1 more error if err != True: - print('Pass ' + str(pass_no) + ' complete for button (' + str(self.x+1) + ',' + str(self.y+1) + '). ' + str(errors) + ' errors detected.') + if self.is_button: + print('Pass ' + str(pass_no) + ' complete for button ' + self.coords + '. ' + str(errors) + ' errors detected.') + else: + print('Pass ' + str(pass_no) + ' complete for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') break # errors prevent next pass return err # success or failure @@ -538,7 +671,9 @@ def Validate_script(self): if len(self.script_lines) > 0: # look for async header and set flag cmd_txt = self.Split_cmd_text(self.script_lines[0]) - self.is_async = cmd_txt in HEADERS and HEADERS[cmd_txt].is_async + self.is_async = cmd_txt in VALID_COMMANDS and \ + isinstance(VALID_COMMANDS[cmd_txt], command_base.Command_Header) and \ + VALID_COMMANDS[cmd_txt].is_async else: self.symbols = None # otherwise destroy symbol table From 33c5c7c02897b668caa272ff9de35c7812b71ef2 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 11 Mar 2021 20:00:14 +0800 Subject: [PATCH 53/83] * Improved documentation * Version number changed to 0.3.2 --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++----- VERSION | 2 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f37c8e1..23bcc42 100644 --- a/README.md +++ b/README.md @@ -137,11 +137,26 @@ I have specifically chosen to do my best to develop this using as many cross pla * `.desktop` shortcuts are coming soon! ## How do I use it? (Post-Install) [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) -* Before starting the program, make sure your Launchpad Classic/Mini/S or MkII is connected to the computer. +* Command line options + * usage: lphk.py [-h] [-d] [-l LAYOUT] [-m] [-s {Mk1,Mk2,Mini,Pro}] [-M {edit,move,swap,copy,run}] [-q] + * -h, --help + * Show this help message and exit + * -d, --debug + * Turn on debugging mode + * -l LAYOUT, --layout + * LAYOUT Load an initial layout + * -m, --minimised + * Start the application minimised + * -s {Mk1,Mk2,Mini,Pro}, --standalone {Mk1,Mk2,Mini,Pro} + * Operate without connection to Launchpad + * -M {edit,move,swap,copy,run}, --mode {edit,move,swap,copy,run} + * Launchpad mode + * -q, --quiet + * Disable information popups* Click `Launchpad > Connect to Launchpad...`. +* Before starting the program, if you are planning to use a Launchpad, ensure it (Launchpad Classic/Mini/S or MkII) is connected to the computer. * If you have a Launchpad Pro, there is currently beta support for it. Please put it in `Live` mode by following the instructions in the pop-up when trying to connect in the next step. For more info, see the [User Manual](https://d2xhy469pqj8rc.cloudfront.net/sites/default/files/novation/downloads/10581/launchpad-pro-gsg-en.pdf). -* Click `Launchpad > Connect to Launchpad...`. * If the connection is successful, the grid will appear, and the status bar at the bottom will turn green. -* The current mode is displayed in the upper right, in the gap between the circular buttons. Clicking this text will change the mode. There are four modes: +* The current mode is displayed in the upper right, in the gap between the circular buttons. Clicking this text will change the mode. There are five modes: * "Edit" mode: Click on a button to open the Script Edit window for that button. * All scripts are saved in the `.lpl` (LaunchPad Layout) files, but the editor also has the ability to import/export single `.lps` (LaunchPad Script) files. * For examples, you can click `Import Script` and look through the `user_scripts/examples/` folder. @@ -158,6 +173,10 @@ I have specifically chosen to do my best to develop this using as many cross pla * The selected button will remain unchanged. * The second button will have the selected button's old script and color bound to it. * If the second button is already bound, you will get a dialog box with options. + * "Run" mode: Click on a button to execute it. + * The button is executes when you release the mouse button. + * A release of the button is queued immediately after the button press. + * This is currentl;y only functional with an emulated launchpad * Go to `Layout > Save layout as...` to save your current layout for future use, colors and all. * Go to `Layout > Load layout...` to load an existing layout. Examples are in `user_layouts/examples/`. @@ -190,16 +209,52 @@ RELEASE (argument 1) ``` If this is used, all other lines in the file must either be blank lines or comments. +#### The `@LOAD_LAYOUT` Header [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +This is a method of loading a new layout. The header is followed by the name of the layout. +``` +@LOAD_LAYOUT c:\layouts\newlayout.lpl +``` +#### The `@SUB` Header [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +This defines a subroutine name and parameters. +``` +@SUB SUB1 a% b% @result% +``` +This defines a subroutine that will be called using CALL:SUB1. It requires 3 parameters. The first 2 can be either integer constants or variables. The last must be a variable because a result can be returned in it. +Within the subroutine, refer to the parameters as `a`, `b`, and `result`. +If a parameter name is preceeded with `@`, the parameter is passed by reference and a variable MUST be specified in the calling code +If a parameter name is preceeded with `-`, the parameter is optional +If a parameter name is followed with `%`, the parameter is integer +If a parameter name is followed with `#`, the parameter is floating point +If a parameter name is followed with `$`, the parameter is a string +If a parameter name is followed with `!`, the parameter is a boolean +#### The `@DESC` Header [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +This defines efines a one line description of a subroutine or button. +``` +@DESC Starts the music +``` +#### The "@NAME" Header [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +The sets the name of a button. Currently this does nothing outside the automatically generated documentation, but could in the future be used to place a label, hint text, etc on the form showing the launchpad. +``` +@NAME Fast Press +``` +This sets the name to `Fast Press`. Note that names are not limited to a single word, but in general should be terse. +#### The "@DOC" Header [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) +This adds lines of documentation to a subroutine or button script. +``` +@DOC This is the first line of documentation +@DOC And this is the second line +``` +This adds 2 lines of documentation that will appear in the automatically generated documentation ### Commands List [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text file with newlines separating commands. #### Utility [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) * `ABORT` - * Terminates the script immediately, logging any message after the command. This has the same functionality as END, however it carries with it the notion that the termination was abnormal. + * Terminates the script immediately, logging any message after the command. This has the same functionality as END, however it carries with it the notion that the termination was abnormal. This will stop execution of a script immediately, even if called from within a subroutine. * `DELAY` * Delays the script for (argument 1) seconds. * `END` - * Terminates the script immediately, logging any message after the command. This has the same functionality as ABORT, however it indicates a normal termination. + * Terminates the script immediately, logging any message after the command. This has the same functionality as ABORT, however it indicates a normal termination. This will stop execution of a script immediately, even if called from within a subroutine. * `GOTO_LABEL` * Goto label (argument 1). * `IF_PRESSED_GOTO_LABEL` @@ -224,6 +279,8 @@ Commands follow the format: `COMMAND arg1 arg2 ...`. Scripts are just a text fil * Works the same as the REPEAT_LABEL command, except the number of times the loop is executed is defined by argument 2. In addition, the loop counter is reset automatically allowing loops to be nested. * `RESET_REPEATS` * Reset the counter on all repeats. (no arguments) +* `RETURN` + * Returns from a subroutine. If not in a subroutine, this will terminate the script. * `SOUND` * Play a sound named (argument 1) inside the `user_sounds/` folder. * Supports `.wav`, `.flac`, and `.ogg` only. diff --git a/VERSION b/VERSION index 9325c3c..9fc80f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 \ No newline at end of file +0.3.2 \ No newline at end of file From 2472f8518fc8fccb51a789773ddeeae17c331808 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 11 Mar 2021 21:22:47 +0800 Subject: [PATCH 54/83] Some tidying up of code, improvements to documentation, and work on the script dialogs * command_base.py initialise doc to blank * commands_scrape.py implement as in the documentation * commands_test.py call newer dump command_base.py * constants.py Add constants for dump command * dialog.py more work on the idle processing of popups * NewCommands.md Document a bit more of the command creation Process * scripts.py Make the dump command more flexible --- NewCommands.md | 16 +++++++--- command_base.py | 3 +- commands_control.py | 2 +- commands_scrape.py | 6 +++- commands_test.py | 2 +- constants.py | 11 ++++++- dialog.py | 78 +++++++++++++++++++++++++++++++++++++-------- global_vars.py | 2 +- scripts.py | 72 +++++++++++++++++++++-------------------- 9 files changed, 134 insertions(+), 58 deletions(-) diff --git a/NewCommands.md b/NewCommands.md index 3fa21f9..9d0e3ee 100644 --- a/NewCommands.md +++ b/NewCommands.md @@ -55,7 +55,7 @@ This has a shorter list of requirements, but it's more coding, and requires more * Required basic understanding of stages of execution of a command -#### An example - Decoding an old version of the MOUSE_SCROLL command +#### An example - Decoding S_FDIST command The `S_FDIST` command looks like this: ```python @@ -69,7 +69,7 @@ class Scrape_Fingerprint_Distance(command_base.Command_Basic): self, ): - super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + super().__init__("S_FDIST, Calculate the distance between 2 fingerprints", LIB, ( # Desc Opt Var type p1_val p2_val @@ -82,6 +82,10 @@ class Scrape_Fingerprint_Distance(command_base.Command_Basic): (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) + self.doc = ["This command calculates the hamming distance between 2 fingerprints.", \ + "This can be used to determine how similar 2 images are. The larger", \ + "the hamming distance, the more different the images are."] + def Process(self, btn, idx, split_line): f1 = self.Get_param(btn, 1) # get the fingerprints @@ -130,7 +134,7 @@ The initialization of a command class serves to define the name of the command. self, ): - super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + super().__init__("S_FDIST, Calculate the distance between 2 fingerprints", LIB, ( # Desc Opt Var type p1_val p2_val @@ -142,9 +146,13 @@ The initialization of a command class serves to define the name of the command. # num params, format string (trailing comma is important) (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) + + self.doc = ["This command calculates the hamming distance between 2 fingerprints.", \ + "This can be used to determine how similar 2 images are. The larger", \ + "the hamming distance, the more different the images are."] ``` -The 5th line defines the name of the command. Note that command names are case sensitive, so the name should be in all uppercase to be consistent. +The 5th line defines the name of the command. Note that command names are case sensitive, so the name should be in all uppercase to be consistent. The name can be optionally followed by a comma and a description of the command. This description is used as part of the auto-documentation of commands Line 6 passes the name of the current library (lib) to the the object. The current library will be used to define where the command originates from in some of the low level reporting functions. diff --git a/command_base.py b/command_base.py index 54354b6..ed21143 100644 --- a/command_base.py +++ b/command_base.py @@ -27,6 +27,7 @@ def __init__( else: self.name = name # the literal name of our command self.desc = '' # no description + self.doc = [] # no multi-line documentation self.lib = lib # the library we're part of self.auto_validate = auto_validate # any auto-validation, if defined @@ -668,7 +669,7 @@ def Validate( pass_no ): - #if idx != 0: + #if idx != 0: # return ("ERROR on line " + btn.Line(idx) + ". " + self.name + " must only appear on line 1.", -1) return (None, 0) diff --git a/commands_control.py b/commands_control.py index eb3881b..0151658 100644 --- a/commands_control.py +++ b/commands_control.py @@ -529,7 +529,7 @@ def Run( # class that defines the RETURN command # -# This differs from END and ABORT (that will abort the execution of a button) in that will returnfrom a +# This differs from END and ABORT (that will abort the execution of a button) in that will returnfrom a # subroutine without exiting class Control_Return(command_base.Command_Text_Basic): def __init__( diff --git a/commands_scrape.py b/commands_scrape.py index 49f4875..45c214d 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -189,7 +189,7 @@ def __init__( self, ): - super().__init__("S_FDIST", # the name of the command as you have to enter it in the code + super().__init__("S_FDIST, Calculate the distance between 2 fingerprints", LIB, ( # Desc Opt Var type p1_val p2_val @@ -202,6 +202,10 @@ def __init__( (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) + self.doc = ["This command calculates the hamming distance between 2 fingerprints.", \ + "This can be used to determine how similar 2 images are. The larger", \ + "the hamming distance, the more different the images are."] + def Process(self, btn, idx, split_line): f1 = self.Get_param(btn, 1) # get the fingerprints diff --git a/commands_test.py b/commands_test.py index b300598..6bc13f1 100644 --- a/commands_test.py +++ b/commands_test.py @@ -364,7 +364,7 @@ def __init__( def Process(self, btn, idx, split_line): - scripts.Dump_commands(True) + scripts.Dump_commands(DS_NORMAL + [D_DEBUG]) scripts.Add_command(Test_Dump_Debug()) # register the command diff --git a/constants.py b/constants.py index 970e490..01dd9fe 100644 --- a/constants.py +++ b/constants.py @@ -106,4 +106,13 @@ LM_MOVE = "move" LM_SWAP = "swap" LM_COPY = "copy" -LM_RUN = "run" \ No newline at end of file +LM_RUN = "run" + +# Dump constants +D_HEADERS = 1 +D_COMMANDS = 2 +D_SUBROUTINES = 3 +D_BUTTONS = 4 +D_DEBUG = 5 + +DS_NORMAL = [D_HEADERS, D_COMMANDS, D_SUBROUTINES, D_BUTTONS] \ No newline at end of file diff --git a/dialog.py b/dialog.py index 5d0deb9..605049d 100644 --- a/dialog.py +++ b/dialog.py @@ -19,13 +19,13 @@ def acquire(self, btn=None): while not self.lock.acquire(True, -1): # wait forever for a lock pass else: - unlocked = False # we start locked - while not unlocked: # and until unlocked + locked = False # we start unlocked + while not locked: # and while unlocked btn.Safe_sleep(DELAY_EXIT_CHECK) # we take a short nap if btn.Check_kill(): # and make sure we're not dead return False # returning False if we are - unlocked = self.lock.acquire(False) # but the main job is to attempt to acquire the lock without blocking) + locked = self.lock.acquire(False) # but the main job is to attempt to acquire the lock without blocking) return True # if we're here, we have a lock @@ -37,16 +37,16 @@ def push(self, x, btn=None): if not ok: # error return for death notification return -1 - + self.msg_id += 1 # increment the message id m = self.msg_id # and store our local message id - + try: self.queue.append((m, x)) # this is what we're here to do! except: return -1 # unlikely, but something bad happened - + finally: self.lock.release() # Always release the lock @@ -60,7 +60,7 @@ def pop(self, x, btn=None): if not ok: # error return for death notification return (False, None) - + try: if len(self.queue) == 0: # if the queue is empty return (True, None) # return success, but a value of None @@ -79,7 +79,7 @@ def pull(self, msg_id): if not ok: # error return for weird situations (should never happen) return False - + try: for idx, (m_id, val) in enumerate(self.queue): # for all items in the queue if m_id > msg_id: # if it's not on the queue @@ -87,7 +87,7 @@ def pull(self, msg_id): if m_id == msg_id: # found it! del self.queue[idx] # remove it return True # and that is success - + return False # error if we don't find it at all finally: @@ -101,18 +101,18 @@ def pull(self, msg_id): def Sync_Request(btn, m_type, args): waiting = True # we're waiting (by default) info = None # and we have nothing returned - + def EndWait(p_info): # callback routine to end the wait nonlocal info info = p_info # here is what we get back nonlocal waiting waiting = False # and the flag telling us the wait is over - + msg = DIALOG_QUEUE.push((m_type, btn, EndWait, args), btn) # push the request for the dialog if msg < 0: # in case of error return (False, info) # we have failed - + while waiting: # while we're waiting btn.Safe_sleep(DELAY_EXIT_CHECK) # we take a short nap if btn.Check_kill(): # and make sure we're not dead @@ -131,5 +131,55 @@ def CommentBox(btn, message): return Sync_Request(btn, DLG_INFO, message) # the arguments are simply the message - - \ No newline at end of file +DIALOG_LOCK = threading.Lock() # lock to be used to access dialog variables +DIALOG_ACTIVE = False # true if we're showing (or preparing to show) a dialog +DIALOG_BUTTON = None # the button object in charge of the dialog +DIALOG_REQUEST = None # information about the request + +# routine that must be called periodically to initiate and kill dialogs +def IdleProcess(): + global DIALOG_LOCK + global DIALOG_ACTIVE + global DIALOG_BUTTON + + if DIALOG_LOCK.acquire(False): # try to acquire a lock + try: # then do what we need to do + if DIALOG_ACTIVE: # if there is a dialog + + if DIALOG_BUTTON.root.thread.kill.is_set(): # has the controlling button been killed? + CloseDialog() # act to close the dialog + + else: # there is no dialog open + + ok, request = DIALOG_QUEUE.pop() # pop a request off the queue + if ok: # if there is a request + OpenDialog(request) # open the dialog + + finally: + DIALOG_LOCK.release() # ensure lock is released before exiting + + +# Close any open dialog +def CloseDialog(): + global DIALOG_ACTIVE + global DIALOG_BUTTON + global DIALOG_REQUEST + + # do what is required to close the window + + DIALOG_ACTIVE = False # no dialog open + DIALOG_BUTTON = None # no button + DIALOG_REQUEST[2]((False, None)) # and return failure, and no information as return info + + +# Open a new dialog +def OpenDialog(request): + global DIALOG_ACTIVE + global DIALOG_BUTTON + global DIALOG_REQUEST + + DIALOG_ACTIVE = True # a dialog is open + DIALOG_BUTTON = request[1] # it's for this button + DIALOG_REQUEST = request[0] # and this is the request + + # do what is needed to actually open the window diff --git a/global_vars.py b/global_vars.py index 64af7a2..53c262e 100644 --- a/global_vars.py +++ b/global_vars.py @@ -1,4 +1,4 @@ -# Variables used all over the place. +# Variables used all over the place. # You must use "import global_vars" and refer to "global_vars.xxx" # Arguments diff --git a/scripts.py b/scripts.py index 6246b22..56aab09 100644 --- a/scripts.py +++ b/scripts.py @@ -50,7 +50,7 @@ def Remove_command( # display info on all commands and headers -def Dump_commands(debug=False): +def Dump_commands(style=DS_NORMAL): def get_name(c): if isinstance(c, command_base.Command_Basic): return c.name @@ -136,11 +136,11 @@ def dump_params(c): print(" variable required (possible return value)") else: print(" UNKNOWN VALUE") - - def dump(c_type, c, debug): + + def dump(c_type, c, style): dump_name(c_type, c) dump_doc(c) - if debug: + if D_DEBUG in style: dump_ancestory(c) dump_params(c) @@ -148,33 +148,37 @@ def dump(c_type, c, debug): import commands_subroutines - print("HEADERS") - print() - for cmd in VALID_COMMANDS: - if isinstance(VALID_COMMANDS[cmd], command_base.Command_Header): - dump("Header", VALID_COMMANDS[cmd], debug) - - print("COMMANDS") - print() - for cmd in VALID_COMMANDS: - if not (isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine) or \ - isinstance(VALID_COMMANDS[cmd], command_base.Command_Header)): - dump("Command", VALID_COMMANDS[cmd], debug) - - print("SUBROUTINES") - print() - for cmd in VALID_COMMANDS: - if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): - dump("Subroutine", VALID_COMMANDS[cmd], debug) - - print("BUTTONS") - print() - global buttons - for x in range(8): - for y in range(1, 9): - btn = buttons[x][y] - if btn.script_str != "": - dump("Button", btn, debug) + if D_HEADERS in style: + print("HEADERS") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], command_base.Command_Header): + dump("Header", VALID_COMMANDS[cmd], style) + + if D_COMMANDS in style: + print("COMMANDS") + print() + for cmd in VALID_COMMANDS: + if not (isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine) or \ + isinstance(VALID_COMMANDS[cmd], command_base.Command_Header)): + dump("Command", VALID_COMMANDS[cmd], style) + + if D_SUBROUTINES in style: + print("SUBROUTINES") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): + dump("Subroutine", VALID_COMMANDS[cmd], style) + + if D_BUTTONS in style: + print("BUTTONS") + print() + global buttons + for x in range(8): + for y in range(1, 9): + btn = buttons[x][y] + if btn.script_str != "": + dump("Button", btn, style) # Create a new symbol table. This contains information required for the script to run @@ -240,13 +244,13 @@ def __init__( def Set_name(self, name): self.name = name self.coords = '' - + if self.is_button: self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? if name: self.coords = " ".join([self.name, self.coords]) # subroutines don't have coordinates - - + + # Do what is required to parse the script. Parsing does not output any information unless it is an error def Parse_script(self): if self.validated: # we don't want to repeat validation over and over From 119c96cb86f6363e861582996adc8564759ab160 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 13 Mar 2021 08:37:51 +0800 Subject: [PATCH 55/83] Fixed parameter passing, Form on screen has same colours as Launchpad, and dialog boxes! * command_base.py 1 character fix to parameter passing bug! and many debug statements removed * command_list.py Now includes dialog commands * commands_dialog.py A rudimentary command for dialogs * commands_test.py new testing for multiple parameters * dialog.py Allows a single dialog box at a time, other threads wanting to display a dialog effectively go to sleep * launchpad_fake.py fake launchpad needs a close method * LPHK.py unbinding also happens with a fake launchpad * scripts.py Buttons have a blank name by default * scripts.py Changing the running status of a button causes the canvas to redraw (to change button colours) * scripts.py The killed flag in a thread is not reset immediately it it notices (because dialogs need to check it too) * scripts.py some bugs in Move, Copy, and Swap fixed * window.py New idle process that runs every 20ms keeps an eye on dialogs * window.py inability to call Move, Copy, and Swap fixed * window.py draw_canvas uses get_colour() to return button colour, or red if running * window.py draw_canvas has text_x() and text_x(y) functions to calculate text position for any button * window.py Square buttons now have a text overlay (tries to use button name, but fails?) * window.py RUN mode highlighted with yellow text on red background so you don't miss it * window.py idle loop started 120 ms after program starts --- LPHK.py | 2 +- command_base.py | 13 +----- command_list.py | 3 +- commands_dialog.py | 49 ++++++++++++++++++++ commands_test.py | 29 +++++++++++- dialog.py | 108 ++++++++++++++++++++++++++++++++++++++------- launchpad_fake.py | 3 ++ lp_colors.py | 2 +- scripts.py | 47 ++++++++++++-------- window.py | 78 ++++++++++++++++++++++++-------- 10 files changed, 265 insertions(+), 69 deletions(-) create mode 100644 commands_dialog.py diff --git a/LPHK.py b/LPHK.py index 3f1019b..b2dd8ae 100755 --- a/LPHK.py +++ b/LPHK.py @@ -141,7 +141,7 @@ def shutdown(): if scripts.buttons[x][y].thread != None: scripts.buttons[x][y].thread.kill.set() # request to kill any running threads - if window.lp_connected: + if window.lp_connected or window.IsStandalone: scripts.Unbind_all() # unbind all the buttons lp_events.timer.cancel() # cancel all the timers if LP != None and LP != -1: diff --git a/command_base.py b/command_base.py index ed21143..ca3dc1f 100644 --- a/command_base.py +++ b/command_base.py @@ -567,31 +567,21 @@ def Get_param(self, btn, n, other=None): m = min(n, avl) av = self.auto_validate[m-1] - if self.Param_count(btn) > n: + if self.Param_count(btn) < n: param = None - #print("P-none", self.Param_count(btn), n)#@@@ else: - #print("P-n", self.Param_count(btn), n)#@@@ param = btn.symbols[SYM_PARAMS][n] - #print("P", param) #@@@ if param == None: ret = other - #print(f"ret- {ret}") #@@@ else: if av[AV_VAR_OK] == AVV_REQD: - #print("RQ") #@@@ ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) - #print(f"ret0 {ret}") #@@@ else: - #print("NRQ") #@@@ if type(param) == str and av[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': ret = param[1:] - #print(f"ret1 {ret}") #@@@ else: ret = param - #print(f"ret2 {ret}") #@@@ - #print(f"get_param '{ret}', {type(ret)} --> '{av[AV_TYPE][AVT_CONV](ret)}', {type(av[AV_TYPE][AVT_CONV](ret))}using '{av[AV_TYPE][AVT_CONV]}'") #@@@ return av[AV_TYPE][AVT_CONV](ret) @@ -605,7 +595,6 @@ def Set_param(self, btn, n, val): m = min(n, avl) av = self.auto_validate[m-1] if av[AV_VAR_OK] == AVV_REQD: - #print("SP", btn.symbols[SYM_PARAMS][n], val, btn.symbols) #@@@ variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable diff --git a/command_list.py b/command_list.py index 6843779..55ac48c 100644 --- a/command_list.py +++ b/command_list.py @@ -13,7 +13,8 @@ commands_mouse, \ commands_pause, \ commands_external, \ - commands_subroutines + commands_subroutines, \ + commands_dialog # @@@ a test command set for testing things! Will be removed for production try: diff --git a/commands_dialog.py b/commands_dialog.py new file mode 100644 index 0000000..d691d0a --- /dev/null +++ b/commands_dialog.py @@ -0,0 +1,49 @@ +import command_base, commands_header, scripts, dialog +from constants import * + +LIB = "cmds_dlgs" # name of this library (for logging) + +# class that defines the Delay command (a target of GOTO's etc) +class Dialog_OK(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DIALOG_OK, A simple OK/Cancel dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), + ("Return", True, AVV_REQD,PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Dialog OK/Cancel '{1}' - abort on cancel"), + (2, " Dialog OK/Cancel '{1}'"), + ) ) + + self.doc = ["A simple dialog with a title and OK/Cancel buttons. Closing the window", \ + "is treated the same as cancel. If a return variable is specified, it will" \ + "contain 1 for OK, and 0 for cancel. If no variable is passed for the "\ + "return value, a cancel will result in a button abort."] + + + def Process(self, btn, idx, split_line): + #print("Title-->", self.Get_param(btn, 1))#@@@ + ret = dialog.CommentBox(btn, self.Get_param(btn, 1)) # Call the dialog and get the return value + try: + rval = ret[1][1] # this will get the return value if everything worked + + except: + rval = dialog.DR_ABORT # otherwise we'll substitute an abort code + + if rval == dialog.DR_OK: # if we got OK + self.Set_param(btn, 2, dialog.DR_OK) # set the return value to OK (if we can) + elif self.Param_count(btn) == 2: # otherwise, if there were 2 parameters + self.Set_param(btn, 2, dialog.DR_CANCEL) # return cancel + else: # if only 1 parameter and no return parameter + btn.root.thread.kill.set() # then kill the thread + return -1 + + +scripts.Add_command(Dialog_OK()) # register the command diff --git a/commands_test.py b/commands_test.py index 6bc13f1..8af5b7a 100644 --- a/commands_test.py +++ b/commands_test.py @@ -19,6 +19,9 @@ def Process(self, btn, idx, split_line): print("=", f"Symbols before = {before}") a = self.Get_param(btn, 1) print("=", f"Param = '{a}', {type(a)}") + for i in range(2,self.Param_count(btn)+1): + _a = self.Get_param(btn, i) + print("=", f"Param = '{_a}', {type(_a)}") if a != None: print("=", f"adding = '{self.one}', {type(self.one)}") a += self.one @@ -297,6 +300,29 @@ def __init__( scripts.Add_command(Test_24()) # register the command +class Test_101(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_101, Test for single required string followed by an optional integer", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Param_1", False, AVV_YES, PT_STR, None, None), + ("Param_2", True, AVV_REQD,PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (1, " Param {1}"), + ) ) + + self.one = "1" + + +scripts.Add_command(Test_101()) # register the command + + # class that defines the Delay command (a target of GOTO's etc) class Test_Dialog(command_base.Command_Basic): def __init__( @@ -316,7 +342,8 @@ def __init__( def Process(self, btn, idx, split_line): import dialog - dialog.CommentBox(btn, "this is a test") + ret = dialog.CommentBox(btn, "this is a test") + #print(ret)#@@@ scripts.Add_command(Test_Dialog()) # register the command diff --git a/dialog.py b/dialog.py index 605049d..4ad5da1 100644 --- a/dialog.py +++ b/dialog.py @@ -1,9 +1,62 @@ # a routine that allows a single script at a time to access dialogs -import threading +import threading, tkinter as tk from constants import * - -DLG_INFO = 1 # a simple info box +DR_ABORT = -1 # returned when aborted for any reason +DR_CANCEL = 0 # Cancel return +DR_OK = 1 # OK return + +DLG_INFO = 1 # a simple titled box with OK + +M_REF = 0 # reference number of message +M_REQ = 1 # request of message + +R_TYPE = 0 # dialog type requested +R_BUTTON = 1 # button that called dialog +R_CALLBACK = 2 # callback function +R_TITLE = 3 # dialog title + +class Dialog(tk.Toplevel): + + def __init__(self, parent, title = None): + tk.Toplevel.__init__(self, parent) + self.transient(parent) + if title: + self.title(title) + self.parent = parent + self.result = None + body = tk.Frame(self) + + b1 = tk.Button(self, text="OK", command=self.btn_OK) + b1.place(x=0, y=0) + b2 = tk.Button(self, text="Cancel", command=self.btn_Cancel) + b2.place(x=100, y=0) + #register validators + #self.validatePosInt = (body.register(self.OnValidatePosInt), + # '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') + + #self.initial_focus = self.body(body) #this calls the body function which is overridden, and which draws the dialog + #body.grid() + #self.buttonbox() + #self.grab_set() + #if not self.initial_focus: + # self.initial_focus = self + #self.protocol("WM_DELETE_WINDOW", self.cancel) + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + #self.initial_focus.focus_set() + + + def btn_OK(self): + global DIALOG_RETURN + DIALOG_RETURN = DR_OK + self.destroy() + + + def btn_Cancel(self): + global DIALOG_RETURN + DIALOG_RETURN = DR_CANCEL + self.destroy() class SyncQueue(): @@ -55,7 +108,7 @@ def push(self, x, btn=None): # pop a value off the queue. If a button is passed, do it in a way that # doesn't stall things - def pop(self, x, btn=None): + def pop(self, btn=None): ok = self.acquire(btn) # try to get a lock if not ok: # error return for death notification @@ -64,7 +117,7 @@ def pop(self, x, btn=None): try: if len(self.queue) == 0: # if the queue is empty return (True, None) # return success, but a value of None - return (True, self.queue.pop(x)) # otherwise return the head of the queue + return (True, self.queue.pop()) # otherwise return the head of the queue except: return (False, None) # unlikely, but something bad happened @@ -135,15 +188,27 @@ def CommentBox(btn, message): DIALOG_ACTIVE = False # true if we're showing (or preparing to show) a dialog DIALOG_BUTTON = None # the button object in charge of the dialog DIALOG_REQUEST = None # information about the request +DIALOG_OBJECT = None # the dialog +DIALOG_RETURN = DR_ABORT # return from the dialog + # routine that must be called periodically to initiate and kill dialogs -def IdleProcess(): +def IdleProcess(parent): global DIALOG_LOCK global DIALOG_ACTIVE global DIALOG_BUTTON + global DIALOG_OBJECT if DIALOG_LOCK.acquire(False): # try to acquire a lock try: # then do what we need to do + # Determine if dialog has closed + if DIALOG_ACTIVE: + try: + DIALOG_OBJECT.state() # Raises an exception if the window is closed + except: + DIALOG_ACTIVE = False + CloseDialog() # act to close the dialog + if DIALOG_ACTIVE: # if there is a dialog if DIALOG_BUTTON.root.thread.kill.is_set(): # has the controlling button been killed? @@ -151,12 +216,12 @@ def IdleProcess(): else: # there is no dialog open - ok, request = DIALOG_QUEUE.pop() # pop a request off the queue - if ok: # if there is a request - OpenDialog(request) # open the dialog + ok, msg = DIALOG_QUEUE.pop() # pop a request off the queue + if ok and msg != None: # if there is a request + OpenDialog(parent, msg[M_REQ]) # open the dialog finally: - DIALOG_LOCK.release() # ensure lock is released before exiting + DIALOG_LOCK.release() # ensure lock is released before exiting # Close any open dialog @@ -164,22 +229,33 @@ def CloseDialog(): global DIALOG_ACTIVE global DIALOG_BUTTON global DIALOG_REQUEST - + global DIALOG_OBJECT + global DIALOG_RETURN + # do what is required to close the window + if DIALOG_OBJECT: + DIALOG_OBJECT.destroy() + DIALOG_REQUEST[R_CALLBACK]((True, DIALOG_RETURN)) # and return success, and the return info from the dialog + DIALOG_OBJECT = None DIALOG_ACTIVE = False # no dialog open DIALOG_BUTTON = None # no button - DIALOG_REQUEST[2]((False, None)) # and return failure, and no information as return info + DIALOG_REQUEST = None # no request # Open a new dialog -def OpenDialog(request): +def OpenDialog(parent, request): global DIALOG_ACTIVE global DIALOG_BUTTON global DIALOG_REQUEST + global DIALOG_OBJECT + global DIALOG_RETURN DIALOG_ACTIVE = True # a dialog is open - DIALOG_BUTTON = request[1] # it's for this button - DIALOG_REQUEST = request[0] # and this is the request + DIALOG_BUTTON = request[R_BUTTON] # it's for this button + DIALOG_REQUEST = request # and this is the request + DIALOG_RETURN = None # the default return + + # do what is needed to actually open the window + DIALOG_OBJECT = Dialog(parent, request[3]) - # do what is needed to actually open the window diff --git a/launchpad_fake.py b/launchpad_fake.py index c2a6620..b548908 100644 --- a/launchpad_fake.py +++ b/launchpad_fake.py @@ -35,6 +35,9 @@ def LedCtrlXY(self, x, y, z, t): def LedCtrlXYByCode(self, x, y, z): pass + def Close(self): + pass + launchpad = Launchpad() diff --git a/lp_colors.py b/lp_colors.py index 886d45d..b03accd 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -85,7 +85,7 @@ def getXY_RGB(x, y): return color_string def luminance(r, g, b): - return ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255.0 + return ((0.299 * r) + (0.587 * g) + (0.114 * b)) / 255.0 # update the colour of a button def updateXY(x, y): diff --git a/scripts.py b/scripts.py index 56aab09..8df5435 100644 --- a/scripts.py +++ b/scripts.py @@ -221,6 +221,7 @@ def __init__( self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine self.script_str = script_str # The script + self.name = "" self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? self.desc = "" self.doc = [] @@ -229,7 +230,7 @@ def __init__( self.symbols = None # The symbol table for the button self.script_lines = None # the lines of the script self.thread = None # the thread associated with this button - self.running = False # is the script running? + self._running = False # is the script running? self.is_async = False # async execution flag # The "root" is the button that is scheduled. This allows subroutines to check if the @@ -241,14 +242,24 @@ def __init__( # let us set/change the name of a button - def Set_name(self, name): + def Set_name(self, name): self.name = name self.coords = '' if self.is_button: self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? - if name: + if name != "": self.coords = " ".join([self.name, self.coords]) # subroutines don't have coordinates + print(self.coords)#@@@ + + + def running(self, set_to=None): + if type(set_to) == bool and set_to != self._running: + self._running = set_to + from window import app + app.draw_canvas() # redraw the canvas when the button run status is changed + + return self._running # Do what is required to parse the script. Parsing does not output any information unless it is an error @@ -329,9 +340,9 @@ def Check_self_kill(self, killfunc=None): if self.thread.kill.is_set(): print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") - self.thread.kill.clear() + #self.thread.kill.clear() if not self.is_async: - self.running = False + self.running(False) if killfunc: killfunc() threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() @@ -398,7 +409,7 @@ def Schedule_script(self): self.thread = threading.Thread(target=Run_script, args=()) self.thread.kill = threading.Event() self.thread.start() - elif not self.running: + elif not self.running(): print("[scripts] " + self.coords + " No script running, starting script in background...") self.thread = threading.Thread(target=self.Run_script_and_run_next, args=()) self.thread.kill = threading.Event() @@ -548,7 +559,7 @@ def Run_script(self): print("[scripts] " + self.coords + " Now running script...") - self.running = not self.is_async + self.running(not self.is_async) cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd_txt in VALID_COMMANDS: @@ -556,7 +567,7 @@ def Run_script(self): command.Run(self, -1, [cmd_txt]) if len(self.script_lines) > 0: - self.running = True + self.running(True) def Main_logic(idx): # the main logic to run a line of a script if self.Check_kill(): # first check to see if we've been asked to die @@ -595,7 +606,7 @@ def Main_logic(idx): # the main logic t run = False # then we're not going to keep running! if not self.is_async: # async commands don't just end - self.running = False # they have to say they're not running + self.running(False) # they have to say they're not running threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours @@ -611,7 +622,7 @@ def Run_subroutine(self): print("[scripts] " + self.coords + " Now running subroutine ...") - self.running = not self.is_async # @@@ not sure a async subroutine makes sense + self.running(not self.is_async) # @@@ not sure a async subroutine makes sense cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters if cmd_txt in VALID_COMMANDS: @@ -619,7 +630,7 @@ def Run_subroutine(self): command.Run(self, -1, [cmd_txt]) if len(self.script_lines) > 0: - self.running = True + self.running(True) def Main_logic(idx): # the main logic to run a line of a script if self.Check_kill(): # first check on our death notification @@ -658,7 +669,7 @@ def Main_logic(idx): # the main logic t run = False # then we're not going to keep running! if not self.is_async: # async commands don't just end @@@ again, not sure this makes sense for subroutines - self.running = False # they have to say they're not running + self.running(False) # they have to say they're not running print("[scripts] " + self.coords + " Subroutine ended.") # and print (log?) that the script is complete @@ -723,10 +734,10 @@ def Unbind(x, y): for index in indexes[::-1]: # and for each of them (in reverse order) temp = to_run.pop(index) # Remove them from the list buttons[x][y] = btn # Clear the button script - return # WHY do we return here? + #return # WHY do we return here? - if thread[x][y] != None: # If the button is actially executing - thread[x][y].kill.set() # then kill it + if btn.thread != None: # If the button is actially executing + thread.kill.set() # then kill it buttons[x][y] = btn # Clear the button script @@ -740,8 +751,8 @@ def Swap(x1, y1, x2, y2): color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 - script_1 = buttons[x1, y1].script_str # Script for btn #1 - script_2 = buttons[x2, y2].script_str # Script for btn #2 + script_1 = buttons[x1][y1].script_str # Script for btn #1 + script_2 = buttons[x2][y2].script_str # Script for btn #2 Unbind(x1, y1) # Unbind #1 if script_2 != "": # If there is a script #2... @@ -778,7 +789,7 @@ def Move(x1, y1, x2, y2): color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour - script_1 = buttons[x1, y1].script_str # Get source button script + script_1 = buttons[x1][y1].script_str # Get source button script Unbind(x1, y1) # Unbind *both* buttons Unbind(x2, y2) diff --git a/window.py b/window.py index d8ec3a2..2d754fd 100644 --- a/window.py +++ b/window.py @@ -256,6 +256,16 @@ def load_initial_layout(self): if global_vars.ARGS['layout']: # did the user pass the option to load an initial layout? files.load_layout_to_lp(global_vars.ARGS['layout'].name) # Load it! + # load a layout on startup + def idle(self): + from dialog import IdleProcess + try: + IdleProcess(self) + finally: + pass + + app.after(20, app.idle) + def disconnect_lp(self): global lp_connected try: @@ -373,9 +383,9 @@ def click(self, event): if self.last_clicked == None: self.last_clicked = (column, row) else: - move_func = partial(scripts.move, self.last_clicked[0], self.last_clicked[1], column, row) - swap_func = partial(scripts.swap, self.last_clicked[0], self.last_clicked[1], column, row) - copy_func = partial(scripts.copy, self.last_clicked[0], self.last_clicked[1], column, row) + move_func = partial(scripts.Move, self.last_clicked[0], self.last_clicked[1], column, row) + swap_func = partial(scripts.Swap, self.last_clicked[0], self.last_clicked[1], column, row) + copy_func = partial(scripts.Copy, self.last_clicked[0], self.last_clicked[1], column, row) if self.button_mode == LM_MOVE: if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): @@ -407,10 +417,22 @@ def draw_button(self, column, row, color="#000000", shape="square"): return self.c.create_oval(x_start + shrink, y_start + shrink, x_end - shrink, y_end - shrink, fill=color, outline="") def draw_canvas(self): + def get_colour(x, y): + if scripts.buttons[x][y].running(): + return "#FF0000" + return lp_colors.getXY_RGB(x, y) + + gap = int(BUTTON_SIZE // 4) + + def text_x(x): + return round((BUTTON_SIZE * x) + (gap * x) + (BUTTON_SIZE / 2) + (gap / 2)) + + def text_y(y): + return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) + + if self.last_clicked != None: if self.outline_box == None: - gap = int(BUTTON_SIZE // 4) - x_start = round((BUTTON_SIZE * self.last_clicked[0]) + (gap * self.last_clicked[0])) y_start = round((BUTTON_SIZE * self.last_clicked[1]) + (gap * self.last_clicked[1])) x_end = round(x_start + BUTTON_SIZE + gap) @@ -429,34 +451,51 @@ def draw_canvas(self): if self.grid_drawn: for x in range(8): y = 0 - self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) + self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) for y in range(1, 9): x = 8 - self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) + self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) for x in range(8): for y in range(1, 9): - self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) - - self.c.itemconfig(self.grid_rects[8][0], text=self.button_mode.capitalize()) + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=scripts.buttons[x][y].name) + if scripts.buttons[x][y].name != "": #@@@ + print(x, y, scripts.buttons[x][y].name)#@@@ + + if self.button_mode == LM_RUN: + self.c.itemconfig(self.grid_rects[8][0][0], fill="red") + self.c.itemconfig(self.grid_rects[8][0][1], fill="yellow", text=self.button_mode.capitalize()) + else: + self.c.itemconfig(self.grid_rects[8][0][0], fill=self.c["background"]) + self.c.itemconfig(self.grid_rects[8][0][1], fill="black", text=self.button_mode.capitalize()) else: for x in range(8): y = 0 - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") + self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") for y in range(1, 9): x = 8 - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") + self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") for x in range(8): for y in range(1, 9): - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y)) - - gap = int(BUTTON_SIZE // 4) - text_x = round((BUTTON_SIZE * 8) + (gap * 8) + (BUTTON_SIZE / 2) + (gap / 2)) - text_y = round((BUTTON_SIZE / 2) + (gap / 2)) - self.grid_rects[8][0] = self.c.create_text(text_x, text_y, text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) + self.grid_rects[x][y] = ( \ + self.draw_button(x, y, color=get_colour(x, y)), \ + self.c.create_text(text_x(x), text_y(y), fill="black", text=scripts.buttons[x][y].name, font=("Courier", BUTTON_SIZE // 5, "normal")) \ + ) + + if self.button_mode == LM_RUN: + self.grid_rects[8][0] = ( \ + self.draw_button(8, 0, color="red"), \ + self.c.create_text(text_x(8), text_y(0), fill="yellow", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) \ + ) + else: + self.grid_rects[8][0] = ( \ + self.draw_button(8, 0, color=self.c["background"]), \ + self.c.create_text(text_x(8), text_y(0), fill="black", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) \ + ) self.grid_drawn = True @@ -806,7 +845,8 @@ def make(): app.after(100, app.connect_lp) app.after(110, app.load_initial_layout) # Load the initial layout if you have specified one - + app.after(120, app.idle) + app.mainloop() From 6ae928693f4154079035d1aa60e7e7fe38bb60fa Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 13 Mar 2021 15:40:01 +0800 Subject: [PATCH 56/83] Main improvement is the display of text on buttons provided by the @NAME header. However significant work has been done to make screen redrawing more efficient and to minimise the need for multiple validations of scripts. * lots of debug comments removed * command_base.py Split header class into 2. One has effects at runtime, the other does not * commands_header.py headers simplified using run/not run header classes * commands_header.py Headers defined initially for subroutines, but useful for buttons moved here * commands_rpncalc.py raising of error after evaluation exception removed * commands_subroutines.py general purpose headers moved to commands_header.py * commands_subroutines.py constant defined for subroutine header name because it is referred to explicitly * constants.py another subroutine-related constant and a comment * files.py Use of new Redraw() routine in place of direct calls to canvas redrawing to reduce multiple redraws * files.py Validated buttons passed to Bind wherever possible to make button names work * scripts.py Add calls to Redraw where required to replace calls to directly draw the canvas * scripts.py modified to allow a button (assumed validated) to be passed to it. If a script is passed, Bind calls ValidateScript to ensure documentation is updated. also the button is queued for redrawing * scripts.py Unbind queues a redraw for an unbound button * scripts.py Swap and Move modified to use the button rather than the script wherever possible * Window.py Definition of STAT_RUN_COLOR to be used for the status message in RUN mode * window.py Main_Window class has new variable _redraw that indicates is a redraw has been requested. By default it is False * window.py has new redraw method that is optionally passed x and y. depending on what is passed, you can queue an updae for a button, all the buttons, or even perform an immediate redraw. * window.py Uses Redraw (not self.redraw) to handle redraw requests * window.py MainWindow idle process now checks the self._redraw status and performs redraws as requested. * windows.py draw_canvas accepts x, y parameters to restrict the amount of redrawing performed * windows.py draw_canvas formats and displays button name on the square buttons * windows.py draw_canvas notes whether you are running, or just connected to launchpad * windows.py Redraw method calls the application's main window to pass through requests to redraw. --- command_base.py | 38 +++++++---- commands_header.py | 119 ++++++++++++++++++++++++++++----- commands_rpncalc.py | 1 - commands_subroutines.py | 134 ++----------------------------------- commands_test.py | 1 - constants.py | 2 + files.py | 8 +-- scripts.py | 61 +++++++++++------ variables.py | 1 - window.py | 142 ++++++++++++++++++++++++++++++++-------- 10 files changed, 290 insertions(+), 217 deletions(-) diff --git a/command_base.py b/command_base.py index ca3dc1f..91a4a85 100644 --- a/command_base.py +++ b/command_base.py @@ -237,9 +237,7 @@ def Partial_run_step_init(self, ret, btn, idx, split_line): # If you need more temporary data, you can override this, call the ancestor, and # create what you need. - #print(f"run_step_init '{split_line}', {self.Param_validation_count(len(split_line)-1)}")#@@@ btn.symbols[SYM_PARAMS] = [self.name] + [None] * self.Param_validation_count(len(split_line)-1) - #print(btn.symbols[SYM_PARAMS])#@@@ btn.symbols[SYM_PARAM_CNT] = 0 return ret @@ -373,7 +371,6 @@ def Param_validation_count(self, n_passed): # This routine determines how many parameters to check. In cases where there are unlimited parameters, # it will only recommend checking the number that exist. Otherwise, all parameters will be checked. # This function improves efficiency. - #print(f"vmp = {self.valid_max_params}, vnp = {self.valid_num_params}, n = {n_passed}")#@@@ vmp = self.valid_max_params # what is the max number of parameters if vmp == None: vnp = self.valid_num_params # if there isn't a max, use the number of parameters @@ -386,7 +383,6 @@ def Param_validation_count(self, n_passed): else: v = vmp # vmp is preferred though - #print(f"vmp = {vmp}, v = {v}, n = {n_passed}")#@@@ if (v == None) or (v < n_passed): return n_passed else: @@ -519,25 +515,20 @@ def Run_param_n(self, ret, btn, idx, split_line, pass_no, n): if pass_no == 1: v = split_line[n] - #print("V1", v) #@@@ if av[AV_VAR_OK] == AVV_YES: # if a variable is allowed if variables.valid_var_name(v): # if it's a variable v = variables.get_value(split_line[n], btn.symbols) # get the value - #print("V2a", v) #@@@ if av[AV_VAR_OK] != AVV_REQD: # if it's not required (i.e. the variable name is not to be passed through) if av[AV_TYPE] and av[AV_TYPE][AVT_CONV]: # if there is a type - #print("V2ba", v, type(v), av[AV_TYPE][AVT_CONV]) #@@@ v = av[AV_TYPE][AVT_CONV](v) # convert the variable to that type - #print("V2bb", v) #@@@ return v elif pass_no == 2: ok = ret if av[AV_P1_VALIDATION]: - #print("S", btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) #@@@ ok = av[AV_P1_VALIDATION](btn.symbols[SYM_PARAMS][n], idx, self.name, av[AV_DESCRIPTION], n, split_line[n]) if ok != True: print("[" + self.lib + "] " + btn.coords + " " + ok) @@ -633,16 +624,17 @@ def Partial_run_step_info(self, ret, btn, idx, split_line): # ################################################## -# ### CLASS Command_Header ### +# ### CLASS Command_Header_Run ### # ################################################## -# Command_Header is a class specifically defining a header command -class Command_Header(Command_Basic): +# Command_Header_Run is a class specifically defining a header command +# that has some action at runtime +class Command_Header_Run(Command_Basic): def __init__( self, name: str, # The name of the command (what you put in the script) - is_async: bool, # is this async? + is_async=False, # is this async? lib="LIB_UNSET", auto_validate=None ): @@ -664,3 +656,23 @@ def Validate( return (None, 0) +# ################################################## +# ### CLASS Command_Header ### +# ################################################## + +# Command_Header is a class specifically defining a a more typical header command +# that has no action at runtime +class Command_Header(Command_Header_Run): + + # Dummy run routine. Simply passes execution to the next line + def Run( + self, + btn, + idx: int, # The current line number + split_line # The current line, split + ): + + return idx+1 + + + diff --git a/commands_header.py b/commands_header.py index ceda43c..d5d8338 100644 --- a/commands_header.py +++ b/commands_header.py @@ -11,7 +11,7 @@ def __init__( ): super().__init__("@ASYNC", # the name of the header as you have to enter it in the code - True) # You also define if the header causes the script to be asynchronous + True) # This must be specified for async headers def Validate( self, @@ -31,16 +31,6 @@ def Validate( return True - def Run( - self, - btn, - idx: int, # The current line number - split_line # The current line, split - ): - - return idx+1 - - scripts.Add_command(Header_Async()) # register the header @@ -48,13 +38,12 @@ def Run( # ### CLASS Header_Simple ### # ################################################## -class Header_Simple(command_base.Command_Header): +class Header_Simple(command_base.Command_Header_Run): def __init__( self, ): - super().__init__("@SIMPLE", # the name of the header as you have to enter it in the code - False) # You also define if the header causes the script to be asynchronous + super().__init__("@SIMPLE") # the name of the header as you have to enter it in the code def Validate( self, @@ -118,13 +107,12 @@ def Run( # ################################################## # Loads a new layout. @@@ This should probably be rewritten in the newest style -class Header_Load_Layout(command_base.Command_Header): +class Header_Load_Layout(command_base.Command_Header_Run): def __init__( self, ): - super().__init__("@LOAD_LAYOUT", # the name of the header as you have to enter it in the code - False) # You also define if the header causes the script to be asynchronous + super().__init__("@LOAD_LAYOUT") # the name of the header as you have to enter it in the code def Validate( @@ -178,3 +166,100 @@ def Run( scripts.Add_command(Header_Load_Layout()) # register the header +# ################################################## +# ### CLASS Header_Name ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Name(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@NAME, Names a button") + + + # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if btn.is_button: + btn.Set_name(' '.join(split_line[1:])) + else: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a button.", btn.Line(idx)) + + return True + + +scripts.Add_command(Header_Name()) # register the header + + +# ################################################## +# ### CLASS Header_Desc ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Desc(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@DESC, Defines a description line") + + + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + btn.desc = ' '.join(split_line[1:]) + + return True + + +scripts.Add_command(Header_Desc()) # register the header + + +# ################################################## +# ### CLASS Header_Doc ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Doc(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@DOC, Adds a line to the documentation text") + + + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + btn.doc += [' '.join(split_line[1:])] + + return True + + +scripts.Add_command(Header_Doc()) # register the header \ No newline at end of file diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 1a27d2c..d6cc457 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -123,7 +123,6 @@ def Process(self, btn, idx, split_line): i = i + o_ret except: print("Error in evaluation: '" + str(sys.exc_info()[1]) + "' at operator #" + str(i) + " on Line:" + str(idx+1) + " '" + cmd + "'") - raise #@@@ break else: # if invalid, report it print("Line:" + str(idx+1) + " - invalid operator #" + str(i) + " '" + cmd + "'") diff --git a/commands_subroutines.py b/commands_subroutines.py index 667cc06..37a4787 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -8,44 +8,18 @@ # As such, it needs to define a new header, and a class that will be instansiated once for each # subroutine that is loaded. -# ################################################## -# ### CLASS Header_Subroutine ### -# ################################################## - -# This forms the basis of the subroutine headers. It should not be instansiated by itself! -class Header_Subroutine(command_base.Command_Header): - def __init__( - self, - name - ): - - super().__init__(name, - False) # subroutines are not async! - - - # Dummy run routine. Simply passes execution to the next line - def Run( - self, - btn, - idx: int, # The current line number - split_line # The current line, split - ): - - return idx+1 - - # ################################################## # ### CLASS Header_Sub_Name ### # ################################################## # This is a dummy header. It is interpreted for real when a subroutine is loaded, # but is ignored in the normal running of commands -class Header_Sub_Name(Header_Subroutine): +class Header_Sub_Name(command_base.Command_Header): def __init__( self, ): - super().__init__("@SUB, Defines a subroutine name and parameters") + super().__init__(SUBROUTINE_HEADER + ", Defines a subroutine name and parameters") # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) @@ -63,109 +37,9 @@ def Validate( return True - -scripts.Add_command(Header_Sub_Name()) # register the header - - -# ################################################## -# ### CLASS Header_Sub_Desc ### -# ################################################## - -# This is a dummy header. It is interpreted for real when a subroutine is loaded, -# but is ignored in the normal running of commands -class Header_Sub_Desc(Header_Subroutine): - def __init__( - self, - ): - - super().__init__("@DESC, Defines a subroutine description") - - - # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) - def Validate( - self, - btn, - idx: int, # The current line number - split_line, # The current line, split - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: - btn.desc = ' '.join(split_line[1:]) - - return True - - -scripts.Add_command(Header_Sub_Desc()) # register the header - - -# ################################################## -# ### CLASS Header_Sub_Name ### -# ################################################## - -# This is a dummy header. It is interpreted for real when a subroutine is loaded, -# but is ignored in the normal running of commands -class Header_Sub_Name(Header_Subroutine): - def __init__( - self, - ): - - super().__init__("@NAME, (re)names a subroutine description") - - - # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) - def Validate( - self, - btn, - idx: int, # The current line number - split_line, # The current line, split - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: - if btn.is_button: - btn.Set_name(' '.join(split_line[1:])) - else: - return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a button.", btn.Line(idx)) - - return True - - scripts.Add_command(Header_Sub_Name()) # register the header -# ################################################## -# ### CLASS Header_Sub_Doc ### -# ################################################## - -# This is a dummy header. It is interpreted for real when a subroutine is loaded, -# but is ignored in the normal running of commands -class Header_Sub_Doc(Header_Subroutine): - def __init__( - self, - ): - - super().__init__("@DOC, Adds a line to the subroutine documentation") - - - # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) - def Validate( - self, - btn, - idx: int, # The current line number - split_line, # The current line, split - pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) - ): - - if pass_no == 1: - btn.doc += [' '.join(split_line[1:])] - - return True - - -scripts.Add_command(Header_Sub_Doc()) # register the header - - # ################################################## # ### CLASS Subroutine_Define ### # ################################################## @@ -241,8 +115,8 @@ def Get_Name_And_Params(lines, sub_n, fname): line = line.strip() if line == '' or line[0] == '-': pass # ignore blank lines and comments - elif line.split()[0] != '@SUB': - return '', f'Error - Subroutine does not start with an @SUB header on line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num + elif line.split()[0] != SUBROUTINE_HEADER: + return '', f'Error - Subroutine does not start with an {SUBROUTINE_HEADER} header on line {lin_num+1} of subroutine {sub_n} in "{fname}"', lin_num else: found = True break diff --git a/commands_test.py b/commands_test.py index 8af5b7a..9140bed 100644 --- a/commands_test.py +++ b/commands_test.py @@ -343,7 +343,6 @@ def __init__( def Process(self, btn, idx, split_line): import dialog ret = dialog.CommentBox(btn, "this is a test") - #print(ret)#@@@ scripts.Add_command(Test_Dialog()) # register the command diff --git a/constants.py b/constants.py index 01dd9fe..a4e59df 100644 --- a/constants.py +++ b/constants.py @@ -94,7 +94,9 @@ EXIT_UPDATE_DELAY = 0.1 DELAY_EXIT_CHECK = 0.025 +# subroutine related SUBROUTINE_PREFIX = "CALL:" +SUBROUTINE_HEADER = "@SUB" # Launchpad constants LP_MK1 = "Mk1" diff --git a/files.py b/files.py index 1be07f9..33b7aaf 100644 --- a/files.py +++ b/files.py @@ -145,7 +145,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): scripts.Unbind_all() scripts.Unload_all() # remove all existing subroutines when you load a new layout - window.app.draw_canvas() + window.Redraw(True) if preload == None: layout = load_layout(name, popups=popups, save_converted=save_converted) @@ -182,17 +182,17 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): raise if script_validation != True: lp_colors.update_all() - window.app.draw_canvas() + window.Redraw(True) in_error = True window.app.save_script(window.app, x, y, script_text, open_editor = True, color = color) in_error = False else: - scripts.Bind(x, y, script_text, color) + scripts.Bind(x, y, btn, color) else: lp_colors.setXY(x, y, color) lp_colors.update_all() - window.app.draw_canvas() + window.Redraw(True) curr_layout = name if converted_to_rg: diff --git a/scripts.py b/scripts.py index 8df5435..8a4817a 100644 --- a/scripts.py +++ b/scripts.py @@ -3,6 +3,7 @@ from functools import partial import lp_events, lp_colors, kb, sound, ms, files, command_base, variables from constants import * +from window import Redraw # VALID_COMMAND is a dictionary of all commands available. @@ -221,7 +222,7 @@ def __init__( self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine self.script_str = script_str # The script - self.name = "" + self.name = None self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? self.desc = "" self.doc = [] @@ -242,22 +243,25 @@ def __init__( # let us set/change the name of a button - def Set_name(self, name): - self.name = name - self.coords = '' + def Set_name(self, name): + update = self.name != None # it is initialisation if the original contents is None + self.name = name # update the name - if self.is_button: + self.coords = '' # Start the process of updating the coords + + if self.is_button: # include actual coords if it is actually a button self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? - if name != "": - self.coords = " ".join([self.name, self.coords]) # subroutines don't have coordinates - print(self.coords)#@@@ + if self.name != "": # If it has a name, let's include that too + self.coords = " ".join([self.name, self.coords]) # remember that subroutines don't have coordinates + + if update and self.is_button: # no need to update the window on initialisation + Redraw(self.x, self.y) # and we only need to update this button def running(self, set_to=None): if type(set_to) == bool and set_to != self._running: self._running = set_to - from window import app - app.draw_canvas() # redraw the canvas when the button run status is changed + Redraw(self.x, self.y) # redraw the canvas when the button run status is changed return self._running @@ -700,12 +704,23 @@ def Validate_script(self): to_run = [] -# bind a button +# bind a button (Note that you can pass a validated button as script_str too) def Bind(x, y, script_str, color): global to_run global buttons - btn = Button(x, y, script_str) + if isinstance(script_str, Button): # if a button was passed + btn = script_str # then we can skipp the button creation + btn.x = x + btn.y = y + btn.Set_name(btn.name) # force recalc of coords + else: + btn = Button(x, y, script_str) + try: + btn.Validate_script() + except: + pass + buttons[x][y] = btn if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... @@ -718,6 +733,7 @@ def Bind(x, y, script_str, color): lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) files.layout_changed_since_load = True # Mark the layout as changed + Redraw(x, y) # unbind a button @@ -742,6 +758,7 @@ def Unbind(x, y): buttons[x][y] = btn # Clear the button script files.layout_changed_since_load = True # Mark the layout as changed + Redraw(x, y) # swap details for two buttons @@ -751,17 +768,17 @@ def Swap(x1, y1, x2, y2): color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 - script_1 = buttons[x1][y1].script_str # Script for btn #1 - script_2 = buttons[x2][y2].script_str # Script for btn #2 + btn_1 = buttons[x1][y1] # btn #1 + btn_2 = buttons[x2][y2] # btn #2 Unbind(x1, y1) # Unbind #1 - if script_2 != "": # If there is a script #2... - Bind(x1, y1, script_2, color_2) # ...bind it to #1 + if btn_2.script != "": # If there is a script #2... + Bind(x1, y1, btn_2, color_2) # ...bind it to #1 lp_colors.updateXY(x1, y1) # Update the colours for btn #1 Unbind(x2, y2) # Do the reverse for #2 - if script_1 != "": - Bind(x2, y2, script_1, color_1) + if btn_1.script != "": + Bind(x2, y2, btn_1, color_1) lp_colors.updateXY(x2, y2) files.layout_changed_since_load = True # Flag that the layout has changed @@ -786,16 +803,18 @@ def Copy(x1, y1, x2, y2): # move a button def Move(x1, y1, x2, y2): global buttons + if (x1, y1) == (x2, y2): + return color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour - script_1 = buttons[x1][y1].script_str # Get source button script + btn_1 = buttons[x1][y1] # Get source button script Unbind(x1, y1) # Unbind *both* buttons Unbind(x2, y2) - if script_1 != "": # If the source had a script... - Bind(x2, y2, script_1, color_1) # ...bind it to the destination + if btn_1.script != "": # If the source had a script... + Bind(x2, y2, btn_1, color_1) # ...bind it to the destination lp_colors.updateXY(x2, y2) # Update the destination colours files.layout_changed_since_load = True # And flag the layout as changed diff --git a/variables.py b/variables.py index b1af8cc..aa0aa6e 100644 --- a/variables.py +++ b/variables.py @@ -58,7 +58,6 @@ def get(name, l_vbls, g_vbls, default=param_convs._None): def put(name, val, vbls): # store a value in a named variable in a specific variable list - #print("NV", name,run val) #@@@ vbls[name.lower()] = val diff --git a/window.py b/window.py index 2d754fd..43aa79c 100644 --- a/window.py +++ b/window.py @@ -11,11 +11,11 @@ from launchpad_fake import launchpad_fake_connector from constants import * - BUTTON_SIZE = 40 HS_SIZE = 200 V_WIDTH = 50 STAT_ACTIVE_COLOR = "#080" +STAT_RUN_COLOR = "#D00" STAT_INACTIVE_COLOR = "#444" SELECT_COLOR = "#f00" DEFAULT_COLOR = [0, 0, 255] @@ -115,6 +115,7 @@ def __init__(self, master=None): self.button_mode = global_vars.ARGS['mode'] self.last_clicked = None self.outline_box = None + self._redraw = False def init_window(self): global root @@ -172,6 +173,24 @@ def init_window(self): self.stat.grid(row=1, column=0, sticky=tk.EW) self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) + def redraw(self, x, y): + if isinstance(x, bool): # if first parameter is a boolean + if x: + self.draw_canvas() # True means draw NOW! + self._redraw = not x # False means draw later (same as no parameters at all + return # and that's all we do for a boolean + + if self._redraw == True: # if _redraw is already True + return # we can't do more than that + + if x == None or y == None: # if either x or y are none (or not passed) + self._redraw = True # assume we want to redraw everything + return + + if self._redraw == False: # if nothing is queued up + self._redraw = set() # create a set of buttons to update + self._redraw.add(complex(x, y)) # and add the requested button to the set + def raise_above_all(self): self.master.attributes('-topmost', 1) self.master.attributes('-topmost', 0) @@ -190,7 +209,7 @@ def connect_dummy(self): lp_connected = True lp_mode = "Dummy" - self.draw_canvas() + Redraw() self.enable_menu(MENU_LAYOUT) self.enable_menu(MENU_SUBROUTINES) @@ -229,7 +248,7 @@ def connect_lp(self): lp_object.LedCtrlBpm(INDICATOR_BPM) lp_events.start(lp_object) - self.draw_canvas() + Redraw() self.enable_menu(MENU_LAYOUT) self.enable_menu(MENU_SUBROUTINES) self.stat["text"] = f"Connected to {lpcon().get_display_name(lp)}" @@ -256,15 +275,28 @@ def load_initial_layout(self): if global_vars.ARGS['layout']: # did the user pass the option to load an initial layout? files.load_layout_to_lp(global_vars.ARGS['layout'].name) # Load it! - # load a layout on startup + # process idle requests (dialogs and button redraws) def idle(self): from dialog import IdleProcess try: IdleProcess(self) - finally: + except: + pass + + try: + if self._redraw == True: + self._redraw = False + app.draw_canvas() + elif self._redraw != False: + r = self._redraw + self._redraw = False + for xy in r: + app.draw_canvas(xy.real,xy.imag) + + except: pass - - app.after(20, app.idle) + + app.after(20, app.idle) def disconnect_lp(self): global lp_connected @@ -297,7 +329,7 @@ def unbind_lp(self, prompt_save=True): self.modified_layout_save_prompt() scripts.Unbind_all() files.curr_layout = None - self.draw_canvas() + Redraw() def load_layout(self): self.modified_layout_save_prompt() @@ -365,11 +397,9 @@ def click(self, event): self.button_mode = LM_RUN else: self.button_mode = LM_EDIT - self.draw_canvas() else: if self.button_mode == LM_EDIT: self.last_clicked = (column, row) - self.draw_canvas() self.script_entry_window(column, row) self.last_clicked = None elif self.button_mode == LM_RUN: @@ -400,7 +430,8 @@ def click(self, event): elif self.button_mode == LM_SWAP: swap_func() self.last_clicked = None - self.draw_canvas() + + Redraw(column, row) def draw_button(self, column, row, color="#000000", shape="square"): gap = int(BUTTON_SIZE // 4) @@ -416,12 +447,12 @@ def draw_button(self, column, row, color="#000000", shape="square"): shrink = BUTTON_SIZE / 10 return self.c.create_oval(x_start + shrink, y_start + shrink, x_end - shrink, y_end - shrink, fill=color, outline="") - def draw_canvas(self): + def draw_canvas(self, bx=None, by=None): def get_colour(x, y): if scripts.buttons[x][y].running(): return "#FF0000" return lp_colors.getXY_RGB(x, y) - + gap = int(BUTTON_SIZE // 4) def text_x(x): @@ -429,8 +460,41 @@ def text_x(x): def text_y(y): return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) - - + + def fmt(t): + if len(t) <= 5: # if name is less than 5 characters + return t # return it unchanged + + tl = t.split() # more complex stuff needs to be split and processed + + n = 0 # split up any word > 5 characters + while n < len(tl): + if len(tl[n]) > 5: + tl = tl[:n] + [tl[n][:5]] + [tl[n][5:]] + tl[n+1:] + n += 1 + + n = 0 # combine ajacent short words + while n+1 < len(tl): + if len(tl[n]) + len(tl[n+1]) < 5: + tl[n] = tl[n] + ' ' + tl[n+1] + del tl[n+1] + else: + n += 1 + + tl = tl[0:3] # limit to 3 lines + + m = 0 # find the longest line + for x in tl: + m = max(m, len(x)) + + for i in range(len(tl)): # then add left padding to help formatting + l = len(tl[i]) + if l < m: + tl[i] = ' '*((m - l)//2) + tl[i] + + return "\n".join(tl) # and return them with line separations between them + + if self.last_clicked != None: if self.outline_box == None: x_start = round((BUTTON_SIZE * self.last_clicked[0]) + (gap * self.last_clicked[0])) @@ -449,27 +513,38 @@ def text_y(y): self.outline_box = None if self.grid_drawn: - for x in range(8): - y = 0 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + if by == None or (by == 0): + for x in range(8): + if bx == None or (bx == x): + y = 0 + self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) - for y in range(1, 9): - x = 8 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + if bx == None or (bx == 8): + for y in range(1, 9): + if bx == None or (by == y): + x = 8 + self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) for x in range(8): - for y in range(1, 9): - self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) - self.c.itemconfig(self.grid_rects[x][y][1], text=scripts.buttons[x][y].name) - if scripts.buttons[x][y].name != "": #@@@ - print(x, y, scripts.buttons[x][y].name)#@@@ + if bx == None or (bx == x): + for y in range(1, 9): + if by == None or (by == y): + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(scripts.buttons[x][y].name)) + global lp_object if self.button_mode == LM_RUN: self.c.itemconfig(self.grid_rects[8][0][0], fill="red") self.c.itemconfig(self.grid_rects[8][0][1], fill="yellow", text=self.button_mode.capitalize()) + if lp_connected: + app.stat["text"] = f"Running as {lpcon().get_display_name(lp_object)}" + app.stat["bg"] = STAT_RUN_COLOR else: self.c.itemconfig(self.grid_rects[8][0][0], fill=self.c["background"]) self.c.itemconfig(self.grid_rects[8][0][1], fill="black", text=self.button_mode.capitalize()) + if lp_connected: + app.stat["text"] = f"Connected to {lpcon().get_display_name(lp_object)}" + app.stat["bg"] = STAT_ACTIVE_COLOR else: for x in range(8): y = 0 @@ -701,7 +776,7 @@ def select_all(self, event): def unbind_destroy(self, x, y, window): scripts.Unbind(x, y) - self.draw_canvas() + Redraw(True) window.destroy() def save_script(self, window, x, y, script_text, open_editor = False, color=None): @@ -724,7 +799,7 @@ def open_editor_func(): if script_text != "": script_text = files.strip_lines(script_text) scripts.Bind(x, y, script_text, colors_to_set[x][y]) - self.draw_canvas() + Redraw(x, y) lp_colors.updateXY(x, y) window.destroy() else: @@ -821,6 +896,15 @@ def modified_layout_save_prompt(self): if not layout_empty: self.popup_choice(self, "Save Changes?", self.warning_image, "You have made changes to this layout.\nWould you like to save this layout before exiting?", [["Save", self.save_layout], ["Save As...", self.save_layout_as], ["Discard", None]]) +# queues up a redraw if the application exists +def Redraw(x=None, y=None): + global app + try: + if app != None: + app.redraw(x, y) + except: + raise + def make(): global root global app @@ -846,7 +930,7 @@ def make(): app.after(100, app.connect_lp) app.after(110, app.load_initial_layout) # Load the initial layout if you have specified one app.after(120, app.idle) - + app.mainloop() From bb8e06617f402f73b7507106d200ad4d5de99970 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 13 Mar 2021 17:02:11 +0800 Subject: [PATCH 57/83] Improvements to button text * LPHK.py new command line option -f to cause text to fit to the button * window.py button text colour (black/white) automatically selected to contrast with button colour * window.py if -f option is used, the font is adjusted to make the button text fit the button --- LPHK.py | 5 ++++- window.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/LPHK.py b/LPHK.py index b2dd8ae..f49ebfb 100755 --- a/LPHK.py +++ b/LPHK.py @@ -113,9 +113,12 @@ def init(): ap.add_argument( # option to start with launchpad window in a particular mode "-M", "--mode", help = "Launchpad mode", type=str, choices=[LM_EDIT, LM_MOVE, LM_SWAP, LM_COPY, LM_RUN], default=LM_EDIT) - ap.add_argument( # reimnplementation of debug (-d or --debug) + ap.add_argument( # turn of unnecessary verbosity "-q", "--quiet", help = "Disable information popups", action="store_true") + ap.add_argument( # make button text variable in size (default is small) + "-f", "--fit", + help = "Make short button text fit the button", action="store_true") global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to diff --git a/window.py b/window.py index 43aa79c..97afc5e 100644 --- a/window.py +++ b/window.py @@ -449,9 +449,27 @@ def draw_button(self, column, row, color="#000000", shape="square"): def draw_canvas(self, bx=None, by=None): def get_colour(x, y): - if scripts.buttons[x][y].running(): - return "#FF0000" - return lp_colors.getXY_RGB(x, y) + if scripts.buttons[x][y].running(): # if the button is running + return "#FF0000" # make the button red + return lp_colors.getXY_RGB(x, y) # otherwise, the normal colour + + def txt_col(x, y): + cn = get_colour(x, y) # get the colour of the button + c = self.winfo_rgb(cn) # convert to RGB value + b = 0.2126*c[0] + 0.587*c[1] + 0.0722*c[2] # calculate perceptual brightness + if b > 20000: # if it's fairly bright + return 'black' # text can be black + else: + return 'white' # otherwise it should be white + + def txt_font(x, y): + if global_vars.ARGS['fit']: # only do this of we're fitting text + t = scripts.buttons[x][y].name # get the text + l = len(t) # and its length + if l < 5 and l > 0: # if it's a reasonable size + return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit + return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size + gap = int(BUTTON_SIZE // 4) @@ -461,7 +479,9 @@ def text_x(x): def text_y(y): return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) - def fmt(t): + def fmt(x, y): + t = scripts.buttons[x][y].name # get the text + if len(t) <= 5: # if name is less than 5 characters return t # return it unchanged @@ -491,7 +511,7 @@ def fmt(t): l = len(tl[i]) if l < m: tl[i] = ' '*((m - l)//2) + tl[i] - + return "\n".join(tl) # and return them with line separations between them @@ -530,7 +550,7 @@ def fmt(t): for y in range(1, 9): if by == None or (by == y): self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) - self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(scripts.buttons[x][y].name)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) global lp_object if self.button_mode == LM_RUN: @@ -558,7 +578,7 @@ def fmt(t): for y in range(1, 9): self.grid_rects[x][y] = ( \ self.draw_button(x, y, color=get_colour(x, y)), \ - self.c.create_text(text_x(x), text_y(y), fill="black", text=scripts.buttons[x][y].name, font=("Courier", BUTTON_SIZE // 5, "normal")) \ + self.c.create_text(text_x(x), text_y(y), fill="black", text=scripts.buttons[x][y].name, font=("Courier", BUTTON_SIZE // 5, "bold")) \ ) if self.button_mode == LM_RUN: From 47cdfed74f613ba8010752fdf64e919df78f1f5e Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 13 Mar 2021 21:43:25 +0800 Subject: [PATCH 58/83] A pretty rugged workable version that has sufficient functionality for me to start writing the scripts I need to write!! The writing of complex scripts will give me an excellent opportunity to fix bugs and improve documentation. * commands_dialog.py Now implements an informational dialog and an OK/Cancel dialog.py * constants.py Some dialog constants moved here * dialog.py I have finally understood enough of TK to generate a fairly flexible dialog that remains non-modal * dialog.py Constants and methods renamed to allow for more generic interface * dialog.py implemented 2 message types corresponding to the 2 dialog types. --- commands_dialog.py | 49 +++++++++++++++++++++++++++++++--------- constants.py | 10 ++++++++- dialog.py | 56 ++++++++++++++++++++++++---------------------- 3 files changed, 77 insertions(+), 38 deletions(-) diff --git a/commands_dialog.py b/commands_dialog.py index d691d0a..e8bd824 100644 --- a/commands_dialog.py +++ b/commands_dialog.py @@ -3,34 +3,34 @@ LIB = "cmds_dlgs" # name of this library (for logging) -# class that defines the Delay command (a target of GOTO's etc) -class Dialog_OK(command_base.Command_Basic): +# class that defines the OK Cancel dialog command +class Dialog_Ok_Cancel(command_base.Command_Basic): def __init__( self, ): - super().__init__("DIALOG_OK, A simple OK/Cancel dialog", + super().__init__("DIALOG_OK_CANCEL, A simple OK/Cancel dialog", LIB, ( # Desc Opt Var type p1_val p2_val ("Title", False, AVV_NO, PT_STR, None, None), + ("Message", False, AVV_NO, PT_STR, None, None), ("Return", True, AVV_REQD,PT_INT, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) - (1, " Dialog OK/Cancel '{1}' - abort on cancel"), - (2, " Dialog OK/Cancel '{1}'"), + (2, " Dialog OK/Cancel '{1}' - abort on cancel"), + (3, " Dialog OK/Cancel '{1}'"), ) ) - self.doc = ["A simple dialog with a title and OK/Cancel buttons. Closing the window", \ - "is treated the same as cancel. If a return variable is specified, it will" \ + self.doc = ["A simple dialog with a title, message and OK/Cancel buttons. Closing the", \ + "window is treated the same as cancel. If a return variable is specified," \ "contain 1 for OK, and 0 for cancel. If no variable is passed for the "\ "return value, a cancel will result in a button abort."] def Process(self, btn, idx, split_line): - #print("Title-->", self.Get_param(btn, 1))#@@@ - ret = dialog.CommentBox(btn, self.Get_param(btn, 1)) # Call the dialog and get the return value + ret = dialog.QueuedDialog(btn, DLG_OK_CANCEL, (self.Get_param(btn, 1), self.Get_param(btn, 2))) # Call the dialog and get the return value try: rval = ret[1][1] # this will get the return value if everything worked @@ -46,4 +46,33 @@ def Process(self, btn, idx, split_line): return -1 -scripts.Add_command(Dialog_OK()) # register the command +scripts.Add_command(Dialog_Ok_Cancel()) # register the command + + +# class that defines an info dialog command +class Dialog_Info(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DIALOG_INFO, A simple informational dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), + ("Message", False, AVV_NO, PT_STR, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (2, " Info dialog '{1}'"), + ) ) + + self.doc = ["A simple dialog with a title, message and OK button. No return value" \ + "is required since the message only requires acknowledgement."] + + + def Process(self, btn, idx, split_line): + ret = dialog.QueuedDialog(btn, DLG_INFO, (self.Get_param(btn, 1), self.Get_param(btn, 2))) # Call the dialog + + +scripts.Add_command(Dialog_Info()) # register the command diff --git a/constants.py b/constants.py index a4e59df..959d1cb 100644 --- a/constants.py +++ b/constants.py @@ -117,4 +117,12 @@ D_BUTTONS = 4 D_DEBUG = 5 -DS_NORMAL = [D_HEADERS, D_COMMANDS, D_SUBROUTINES, D_BUTTONS] \ No newline at end of file +DS_NORMAL = [D_HEADERS, D_COMMANDS, D_SUBROUTINES, D_BUTTONS] + +# dialog constants +DR_ABORT = -1 # returned when aborted for any reason +DR_CANCEL = 0 # Cancel return +DR_OK = 1 # OK return + +DLG_INFO = 1 # a simple titled box with OK +DLG_OK_CANCEL = 2 # a simple titled box with OK and Cancel \ No newline at end of file diff --git a/dialog.py b/dialog.py index 4ad5da1..8bdae4b 100644 --- a/dialog.py +++ b/dialog.py @@ -2,49 +2,48 @@ import threading, tkinter as tk from constants import * -DR_ABORT = -1 # returned when aborted for any reason -DR_CANCEL = 0 # Cancel return -DR_OK = 1 # OK return - -DLG_INFO = 1 # a simple titled box with OK - M_REF = 0 # reference number of message M_REQ = 1 # request of message R_TYPE = 0 # dialog type requested R_BUTTON = 1 # button that called dialog R_CALLBACK = 2 # callback function -R_TITLE = 3 # dialog title +R_PARAM = 3 # dialog parameters (title, message) + class Dialog(tk.Toplevel): - def __init__(self, parent, title = None): + def __init__(self, parent, title=None, message=None, ok=False, cancel=False): + tk.Toplevel.__init__(self, parent) + self.transient(parent) + if title: self.title(title) + self.parent = parent self.result = None + body = tk.Frame(self) + body.pack(ipadx=2, ipady=2) + + if message: + msg = tk.Label(body, text=message, wraplength=350, justify="center") + msg.pack(padx=8, pady=8) + + foot = tk.Frame(body) + foot.pack(padx=4, pady=4) - b1 = tk.Button(self, text="OK", command=self.btn_OK) - b1.place(x=0, y=0) - b2 = tk.Button(self, text="Cancel", command=self.btn_Cancel) - b2.place(x=100, y=0) - #register validators - #self.validatePosInt = (body.register(self.OnValidatePosInt), - # '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') - - #self.initial_focus = self.body(body) #this calls the body function which is overridden, and which draws the dialog - #body.grid() - #self.buttonbox() - #self.grab_set() - #if not self.initial_focus: - # self.initial_focus = self - #self.protocol("WM_DELETE_WINDOW", self.cancel) + if ok: + b1 = tk.Button(foot, text="OK", width=8, command=self.btn_OK) + b1.pack(side='left', padx=5) + if cancel: + b2 = tk.Button(foot, text="Cancel", width=8, command=self.btn_Cancel) + b2.pack(side='left', padx=5) + #b1.focus_set() self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50)) - #self.initial_focus.focus_set() def btn_OK(self): @@ -180,8 +179,8 @@ def EndWait(p_info): # callback routine to # request a simple comment box -def CommentBox(btn, message): - return Sync_Request(btn, DLG_INFO, message) # the arguments are simply the message +def QueuedDialog(btn, dlg_type, message): + return Sync_Request(btn, dlg_type, message) # the arguments are simply the message DIALOG_LOCK = threading.Lock() # lock to be used to access dialog variables @@ -257,5 +256,8 @@ def OpenDialog(parent, request): DIALOG_RETURN = None # the default return # do what is needed to actually open the window - DIALOG_OBJECT = Dialog(parent, request[3]) + if request[R_TYPE] == DLG_INFO: + DIALOG_OBJECT = Dialog(parent, request[R_PARAM][0], request[R_PARAM][1], ok=True) + elif request[R_TYPE] == DLG_OK_CANCEL: + DIALOG_OBJECT = Dialog(parent, request[R_PARAM][0], request[R_PARAM][1], ok=True, cancel=True) From a24ac5b249e3c31eab3fbf7d78310ca4635a51dc Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 13 Mar 2021 21:45:40 +0800 Subject: [PATCH 59/83] The run script and the layout I have built up for testing. * run.bat how I run LPHK, with all the useful parameters * Returns.lpl A layout with a bit of everything --- run.bat | 3 + user_layouts/Returns.lpl | 698 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 701 insertions(+) create mode 100644 run.bat create mode 100644 user_layouts/Returns.lpl diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..9968e7f --- /dev/null +++ b/run.bat @@ -0,0 +1,3 @@ +cls +rem python LPHK.py -l user_layouts\Returns.lpl +python LPHK.py -l user_layouts\Returns.lpl -s Mk1 -M run -q -f diff --git a/user_layouts/Returns.lpl b/user_layouts/Returns.lpl new file mode 100644 index 0000000..4aac2ef --- /dev/null +++ b/user_layouts/Returns.lpl @@ -0,0 +1,698 @@ +{ + "buttons": [ + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "- Find the Logbook\n@NAME Find Note pad\nW_FIND_HWND \"Untitled - Notepad\" ml_hwnd n 1\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Norm Dlg\n@DESC Displays a dialog, then prints the returned value\nRPN_EVAL < a view\nDIALOG_INFO \"Information message\" \"Don't look behind you, there might be a clown!\"\nRPN_EVAL < a view\nDIALOG_OK_CANCEL \"Do you accept?\" \"You do not have any clowns standing behind you\" a\nRPN_EVAL < a view" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME To" + }, + { + "color": [ + 0, + 170, + 0 + ], + "text": "@NAME to" + }, + { + "color": [ + 0, + 85, + 0 + ], + "text": "@NAME the" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test\n@DESC Run a series of tests to prove parameter passing is working\n@DOC This test is intended as a regression tool to be used after modifying\n@DOC the parameter handling code. This series of tests passes almost every\n@DOC combination of parameters to test routines. The output can be difficult\n@DOC to make sense of, but the critical thing is that none of the lines of\n@DOC output between START and END should change. Changes indicate a new\n@DOC type of bad behaviour that needs to be rectified.\n@DOC\n@DOC I test this by having a reference testlog-base.log and running LPHK\n@DOC with the output redirected to testlog.log. I then use Beyond Compare\n@DOC to evaluate differences.\n@DOC\n@DOC Also note that this script requires the command_test.py module that is\n@DOC not intended to be included in production releases.\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_01\nTEST_01 1\nTEST_11\nTEST_11 1\nTEST_11 a\nTEST_11 aa\nTEST_11 b\nTEST_21\nTEST_21 a\nTEST_21 aa\nTEST_21 c\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_02\nTEST_02 1\nTEST_02 1.25\nTEST_12\nTEST_12 1\nTEST_12 1.25\nTEST_12 a\nTEST_12 aa\nTEST_12 d\nTEST_22\nTEST_22 a\nTEST_22 aa\nTEST_22 e\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_03\nTEST_03 \"1\"\nTEST_03 \"1.25\"\nTEST_13\nTEST_13 \"1\"\nTEST_13 \"1.25\"\nTEST_13 a\nTEST_13 aa\nTEST_13 f\nTEST_23\nTEST_23 a\nTEST_23 aa\nTEST_23 g\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_04\nTEST_04 \"1\"\nTEST_04 \"1.25\"\nTEST_14\nTEST_14 \"1\"\nTEST_14 \"1.25\"\nTEST_14 a\nTEST_14 aa\nTEST_14 h\nTEST_24\nTEST_24 a\nTEST_24 aa\nTEST_24 i\n\nTEST_11 g\nTEST_11 aa\nTEST_12 g\nTEST_12 aa\n\nTEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 85, + 255, + 0 + ], + "text": "@NAME be" + }, + { + "color": [ + 85, + 170, + 0 + ], + "text": "@NAME be," + }, + { + "color": [ + 85, + 85, + 0 + ], + "text": "@NAME question." + }, + { + "color": [ + 85, + 0, + 0 + ], + "text": "@NAME Yorick" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "TEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 170, + 255, + 0 + ], + "text": "@NAME or" + }, + { + "color": [ + 170, + 170, + 0 + ], + "text": "@NAME that" + }, + { + "color": [ + 170, + 85, + 0 + ], + "text": "@NAME Alas" + }, + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME I" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 5" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test Dlg 1\n@DESC Tests a useless dialog\nDIALOG_INFO \"Title 1\" \"Message 1\"" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 255, + 255, + 0 + ], + "text": "@NAME not" + }, + { + "color": [ + 255, + 170, + 0 + ], + "text": "@NAME is" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "@NAME poor" + }, + { + "color": [ + 255, + 0, + 0 + ], + "text": "@NAME knew him well" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 4" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n-@DOC Press this button and the other similar button. This should result in\n-@DOC only one dialog appearing at a time. Cancelling the button should\n-@DOC close an open dialog, or prevent a queued dialog from showing.\n-@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "ABORT\nEND" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 3" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:RETURN_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 2" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:END_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "- test\nDIALOG_OK_CANCEL \"Continue this LPHK script?\" \"Press OK to continue or Cancel to abort right now.\" a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME A 1\nCALL:ABORT_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Dump\n@DESC Dump all commands\n@DOC Lists all the heaqers, commands, subroutines, and buttons\n@DOC with whatever we can figure out about them\nTEST_DUMP_DEBUG" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ] + ], + "subroutines": [ + [ + "@SUB RETURN_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "RETURN", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ABORT_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "ABORT", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB END_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "END", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ADD_ONE @a%", + "@DESC Adds 1 to the integer parameter passed", + "@DOC Line 1 of documentation", + "@DOC Line 2 of documentation", + "RPN_EVAL view_l < a 1 + > a view_l" + ] + ], + "version": "0.2" +} \ No newline at end of file From c4c52108314e77dcc2c0e20cc5c71f8686c204bf Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 15 Mar 2021 18:50:29 +0800 Subject: [PATCH 60/83] Significant modifications and bug-fixes whilst using LPHK today. Apart from bugs, main improvements were to flow control and window handling. Note that a bug causes subroutines to report duplicate labels. I think this is due to validation being performed twice. * command_base.py Bug found in reporting of an error_msg * command_base.py Duplicate label reports label name * command_control.py new statements for IF/GOTO using variables or constants. EQ, NE, GE, LE GT, and LT * commands_dialog.py Bug fix in DLG_OK_CANCEL to correctly return result * commands_dialog.py New DIALOG_ERROR that just has a "cancel" button. * commands_keys.py New command TYPE that is similar to STRING except it accepts a series of constants and variables, appending them to make the typed string. * commands_rpn.py New command RPN_CLEAR that can be used to clear local, global, stack etc in one command also will allow the clearing of multiple named variables. (but there's a bug in that). This command is not properly validated. Also it needs a new datatype "word" that grabs a word from the command line without requiring it is quoted. * commands_win32 W_FIND_HWND now has a companion W_SIMILAR_HWND that only requires a match on the beginning of the window title * commands_win32.py New command W_MINIMISE_HWND can minimise just the HWND passed to it, or without an HWND will minimise all windows except LPHK. Note that this has a bug in minimising the Anaconda window LPHK starts from. Sometimes it won't maximise correctly again. * commands_win32.py New command W_RESTORE_HWND to restore a window from its minimised state. constants.py new constant for the Error dialog.py * dialog.py now replaces \n in strings with a newline so you can manually break lines in the dialogs. * dialog.py implemented new dialog box type * scripts.py dumps all buttons (some were missed previously) * scripts.py bug fixes in Swap and Move * variables.py implements a method to remove individual variables * window.py now also allows text on round buttons, but limited to 2 rows of 3 characters. Modified character sizing to fit the smaller buttons. --- command_base.py | 4 +- commands_control.py | 258 ++++++++++++++++++++++ commands_dialog.py | 58 ++++- commands_keys.py | 33 +++ commands_rpncalc.py | 62 +++++- commands_win32.py | 156 ++++++++++++- constants.py | 3 +- dialog.py | 3 + run.bat | 3 +- scripts.py | 10 +- user_layouts/{Returns.lpl => Testing.lpl} | 64 +++--- variables.py | 8 + window.py | 39 ++-- 13 files changed, 637 insertions(+), 64 deletions(-) rename user_layouts/{Returns.lpl => Testing.lpl} (89%) diff --git a/command_base.py b/command_base.py index 91a4a85..dd548ed 100644 --- a/command_base.py +++ b/command_base.py @@ -157,7 +157,7 @@ def Parse( return True if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: - ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.Line(idx)) + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + btn.Line(idx) + "' on pass " + str(pass_no), btn.Line(idx)) if ret[0]: print(ret[0]) @@ -449,7 +449,7 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only # check for duplicate label if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", btn.Line(idx)) + return ("Duplicate LABEL " + split_line[n], btn.Line(idx)) # add label to symbol table # Add the new label to the labels in the symbol table btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number diff --git a/commands_control.py b/commands_control.py index 0151658..bc73857 100644 --- a/commands_control.py +++ b/commands_control.py @@ -175,6 +175,78 @@ def Init_n_minus_1(self, btn, idx, split_line): # set repeats self.Reset(btn, idx) + def a_eq_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a == b: + return True + + except: + return False + + + def a_ne_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a != b: + return True + + except: + return True # this is an exception. If we can't compare they can't be equal! + + + def a_gt_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a > b: + return True + + except: + return False + + + def a_lt_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a < b: + return True + + except: + return False + + + def a_ge_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a >= b: + return True + + except: + return False + + + def a_le_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a <= b: + return True + + except: + return False + + # ################################################## # ### CLASS Control_Goto_Label ### # ################################################## @@ -201,6 +273,192 @@ def __init__( scripts.Add_command(Control_Goto_Label()) # register the command +# ################################################## +# ### CLASS IF_EQ_GOTO ### +# ################################################## + +# class that defines the IF_EQ_GOTO command +class Control_If_Eq_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_EQ_GOTO, Goto label, if parameters 2 and 3 are equal", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} == {3} Goto {1}"), + ), + "a == b", + self.a_eq_b + ) + + +scripts.Add_command(Control_If_Eq_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_NE_GOTO ### +# ################################################## + +# class that defines the IF_NE_GOTO command +class Control_If_Ne_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_NE_GOTO, Goto label, if parameters 2 and 3 are not equal", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} != {3} Goto {1}"), + ), + "a != b", + self.a_ne_b + ) + + +scripts.Add_command(Control_If_Ne_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_GT_GOTO ### +# ################################################## + +# class that defines the IF_GT_GOTO command +class Control_If_Gt_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_GT_GOTO, Goto label, if parameters 2 is greater than parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} > {3} Goto {1}"), + ), + "a > b", + self.a_gt_b + ) + + +scripts.Add_command(Control_If_Gt_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_GE_GOTO ### +# ################################################## + +# class that defines the IF_GE_GOTO command +class Control_If_Ge_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_GE_GOTO, Goto label, if parameters 2 is greater than or equal to parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} >= {3} Goto {1}"), + ), + "a >= b", + self.a_gt_b + ) + + +scripts.Add_command(Control_If_Ge_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_LT_GOTO ### +# ################################################## + +# class that defines the IF_LT_GOTO command +class Control_If_Lt_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_LT_GOTO, Goto label, if parameters 2 is less than parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} < {3} Goto {1}"), + ), + "a < b", + self.a_lt_b + ) + + +scripts.Add_command(Control_If_Lt_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_LE_GOTO ### +# ################################################## + +# class that defines the IF_LE_GOTO command +class Control_If_Le_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_LE_GOTO, Goto label, if parameters 2 is less than or equal to parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} <= {3} Goto {1}"), + ), + "a <= b", + self.a_le_b + ) + + +scripts.Add_command(Control_If_Le_Goto()) # register the command + + # ################################################## # ### CLASS Control_If_Pressed_Goto_Label ### # ################################################## diff --git a/commands_dialog.py b/commands_dialog.py index e8bd824..c0da941 100644 --- a/commands_dialog.py +++ b/commands_dialog.py @@ -3,6 +3,11 @@ LIB = "cmds_dlgs" # name of this library (for logging) + +# ################################################## +# ### CLASS DIALOG_OK_CANCEL ### +# ################################################## + # class that defines the OK Cancel dialog command class Dialog_Ok_Cancel(command_base.Command_Basic): def __init__( @@ -38,9 +43,9 @@ def Process(self, btn, idx, split_line): rval = dialog.DR_ABORT # otherwise we'll substitute an abort code if rval == dialog.DR_OK: # if we got OK - self.Set_param(btn, 2, dialog.DR_OK) # set the return value to OK (if we can) - elif self.Param_count(btn) == 2: # otherwise, if there were 2 parameters - self.Set_param(btn, 2, dialog.DR_CANCEL) # return cancel + self.Set_param(btn, 3, dialog.DR_OK) # set the return value to OK (if we can) + elif self.Param_count(btn) == 3: # otherwise, if there were 3 parameters + self.Set_param(btn, 3, dialog.DR_CANCEL) # return cancel else: # if only 1 parameter and no return parameter btn.root.thread.kill.set() # then kill the thread return -1 @@ -49,6 +54,10 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Dialog_Ok_Cancel()) # register the command +# ################################################## +# ### CLASS DIALOG_INFO ### +# ################################################## + # class that defines an info dialog command class Dialog_Info(command_base.Command_Basic): def __init__( @@ -68,7 +77,8 @@ def __init__( ) ) self.doc = ["A simple dialog with a title, message and OK button. No return value" \ - "is required since the message only requires acknowledgement."] + "is required since the message only requires acknowledgement. This" \ + "will never cause an abort"] def Process(self, btn, idx, split_line): @@ -76,3 +86,43 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Dialog_Info()) # register the command + +# ################################################## +# ### CLASS DIALOG_ERROR ### +# ################################################## + +# class that defines an error dialog command +class Dialog_Error(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DIALOG_ERROR, A simple error dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), + ("Message", False, AVV_NO, PT_STR, None, None), + ("Return", True, AVV_REQD,PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (2, " Error dialog '{1}'"), + (3, " Error dialog '{1}' returning {3}"), + ) ) + + self.doc = ["A simple dialog with a title, message and Cancel button. Typically" \ + "this should be called without a return variable to allow the script" \ + "to abort. If a return value is specified, the script will continue" \ + "after the dialog is dismissed."] + + + def Process(self, btn, idx, split_line): + ret = dialog.QueuedDialog(btn, DLG_ERROR, (self.Get_param(btn, 1), self.Get_param(btn, 2))) # Call the dialog + if self.Param_count(btn) == 2: + btn.root.thread.kill.set() # then kill the thread + return -1 + + + +scripts.Add_command(Dialog_Error()) # register the command diff --git a/commands_keys.py b/commands_keys.py index a85af2a..999ed07 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -217,3 +217,36 @@ def Run( scripts.Add_command(Keys_String()) # register the command + + +# ################################################## +# ### CLASS Keys_Type ### +# ################################################## + +# class that defines the TYPE command (type a string) +class Keys_Type(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TYPE, type the text that is the concatenation of all variables passed", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Value", False, AVV_YES, PT_STRS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Type {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + val = '' + for i in range(1, self.Param_count(btn)+1): # for each parameter (after the first) + val += str(self.Get_param(btn, i)) # append all the values (force to string) + kb.write(val) + + +scripts.Add_command(Keys_Type()) # register the command + diff --git a/commands_rpncalc.py b/commands_rpncalc.py index d6cc457..adcb6d7 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -177,7 +177,7 @@ def Register_operators(self): self.operators["!?L"] = (self.is_local_not_def, 1) # is local var not defined self.operators["?G"] = (self.is_global_def, 1) # is global var defined self.operators["!?G"] = (self.is_global_not_def, 1) # is global var not defined - self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc + self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc) def add(self, @@ -676,7 +676,7 @@ def __init__( self, ): - super().__init__("RPN_SET", # the name of the command as you have to enter it in the code + super().__init__("RPN_SET, Sets a string to the concatenation of all the variables passed to it", LIB, ( # Desc Opt Var type p1_val p2_val @@ -696,4 +696,60 @@ def Process(self, btn, idx, split_line): self.Set_param(btn, 1, val) # pass the combined string back -scripts.Add_command(Rpn_Set()) # register the command \ No newline at end of file +scripts.Add_command(Rpn_Set()) # register the command + + +# ################################################## +# ### CLASS RPN_CLEAR ### +# ################################################## + +# class that defines the RPN_CLEAR command -- clears variables @@@ needs validation!!! +class Rpn_Clear(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RPN_CLEAR, Clear variables or stack", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Function", False, AVV_NO, PT_STR, None, None), + ("Variable", True, AVV_REQD, PT_STRS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Clear {1}"), + (2, " Clear {1}: {2}"), + ) ) + + self.doc= ["If parameter 1 is:", + " 'GLOBALS' All the global variables are cleared", + " 'LOCALS' All the local variables are cleared", + " 'VARS' All variables are cleared", + " 'STACK' The stack is cleared", + " 'ALL' All variables and the stack are cleared", + " 'GLOBAL' v1 [v2 [v3...]] Named global variables v1... are deleted", + " 'LOCAL' v1 [v2 [v3...]] Named local variables v1... are deleted", + " 'VAR' v1 [v2 [v3...]] Named variables v1... are deleted"] + + + def Process(self, btn, idx, split_line): + f = (self.Get_param(btn, 1)).upper() + + if f in ["GLOBALS", "VARS", "ALL"]: + with btn.symbols[SYM_GLOBAL][0]: + btn.symbols[SYM_GLOBAL][1].clear() # clear all global variables + if f in ["LOCALS", "VARS", "ALL"]: + btn.symbols[SYM_LOCAL].clear() # clear all local variables + if f in ["STACK", "ALL"]: + btn.symbols[SYM_STACK].clear() # clear the stack + if f in ["GLOBAL", "VAR"]: + for i in range(2, self.Param_count(btn)+1): + with btn.symbols[SYM_GLOBAL][0]: + variables.undef(split_line[i], btn.symbols[SYM_GLOBAL][1]) + if f in ["LOCAL", "VAR"]: + for i in range(2, self.Param_count(btn)+1): + variables.undef(split_line[i], btn.symbols[SYM_LOCAL]) + + +scripts.Add_command(Rpn_Clear()) # register the command \ No newline at end of file diff --git a/commands_win32.py b/commands_win32.py index ac135cb..bb243be 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -28,6 +28,10 @@ def restore_window(self, hwnd, fg = False): return place[1], old_hwnd, hwnd # useful if you want to minimise it again + # minimise a window + def minimise_window(self, hwnd, fg = False): + win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) # minimise it + # resets windows to what they were before the restore def reset_window(self, old_state): state, old_hwnd, hwnd = old_state @@ -50,6 +54,7 @@ def callback (hwnd, hwnds): hwnds = [] win32gui.EnumWindows (callback, hwnds) + hwnds.sort() return hwnds @@ -267,7 +272,7 @@ def __init__( self, ): - super().__init__("W_FIND_HWND", # the name of the command as you have to enter it in the code + super().__init__("W_FIND_HWND, Returns the handle of the nth exactly matching window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -281,6 +286,11 @@ def __init__( (4, " Find {4}th window titled '{1}', returning handle in {2}. Report {3} total matches"), ) ) + self.doc = ["Searches for an exactly matching window `Title`. If multiple are found,", + "the windows are sorted by process id. The number of matching windows is", + "returned in `M`. If `N` or more are found, the nth window handle is", + "returned in `HWND`. -1 is returned if there is an error."] + def Process(self, btn, idx, split_line): def CheckWindow(hwnd, data): @@ -312,6 +322,150 @@ def CheckWindow(hwnd, data): scripts.Add_command(Win32_Find_Hwnd()) # register the command +# ################################################## +# ### CLASS W_SIMILAR_HWND ### +# ################################################## + +# class that defines the W_SIMILAR_HWND command - returns the nth matching window handle +class Win32_Similar_Hwnd(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_SIMILAR_HWND, Returns the handle of the nth similar window", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), # name to search for + ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND + ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M 0: # if it's a reasonable size - return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit - return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size + if round: + return ("Courier", int(0.75 * BUTTON_SIZE / l), "bold") # round buttons need smaller text + else: + return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit + return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size gap = int(BUTTON_SIZE // 4) @@ -479,29 +482,29 @@ def text_x(x): def text_y(y): return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) - def fmt(x, y): + def fmt(x, y, lines=3, cols=5): t = scripts.buttons[x][y].name # get the text - if len(t) <= 5: # if name is less than 5 characters + if len(t) <= cols: # if name is less than 5 characters return t # return it unchanged tl = t.split() # more complex stuff needs to be split and processed n = 0 # split up any word > 5 characters while n < len(tl): - if len(tl[n]) > 5: - tl = tl[:n] + [tl[n][:5]] + [tl[n][5:]] + tl[n+1:] + if len(tl[n]) > cols: + tl = tl[:n] + [tl[n][:cols]] + [tl[n][cols:]] + tl[n+1:] n += 1 n = 0 # combine ajacent short words while n+1 < len(tl): - if len(tl[n]) + len(tl[n+1]) < 5: + if len(tl[n]) + len(tl[n+1]) < cols: tl[n] = tl[n] + ' ' + tl[n+1] del tl[n+1] else: n += 1 - tl = tl[0:3] # limit to 3 lines + tl = tl[0:lines] # limit to 3 lines m = 0 # find the longest line for x in tl: @@ -537,13 +540,15 @@ def fmt(x, y): for x in range(8): if bx == None or (bx == x): y = 0 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) if bx == None or (bx == 8): for y in range(1, 9): if bx == None or (by == y): x = 8 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) for x in range(8): if bx == None or (bx == x): @@ -568,17 +573,23 @@ def fmt(x, y): else: for x in range(8): y = 0 - self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") + self.grid_rects[x][y] = ( \ + self.draw_button(x, y, color=get_colour(x, y), shape="circle"), + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ + ) for y in range(1, 9): x = 8 - self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") + self.grid_rects[x][y] = ( \ + self.draw_button(x, y, color=get_colour(x, y), shape="circle"), + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ + ) for x in range(8): for y in range(1, 9): self.grid_rects[x][y] = ( \ self.draw_button(x, y, color=get_colour(x, y)), \ - self.c.create_text(text_x(x), text_y(y), fill="black", text=scripts.buttons[x][y].name, font=("Courier", BUTTON_SIZE // 5, "bold")) \ + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) \ ) if self.button_mode == LM_RUN: From 340a74713130f484a9a4c1f7f6db810fefda7469 Mon Sep 17 00:00:00 2001 From: Steve the wonder dog Date: Mon, 15 Mar 2021 19:40:45 +0800 Subject: [PATCH 61/83] I hope this merge will be the last... --- command_base.py | 4 +- commands_control.py | 258 ++++ commands_dialog.py | 58 +- commands_keys.py | 33 + commands_rpncalc.py | 62 +- commands_win32.py | 156 ++- constants.py | 3 +- dialog.py | 3 + run.bat | 3 +- scripts.py | 10 +- user_layouts/{Returns.lpl => Testing.lpl} | 1394 ++++++++++----------- variables.py | 8 + window.py | 39 +- 13 files changed, 1302 insertions(+), 729 deletions(-) rename user_layouts/{Returns.lpl => Testing.lpl} (88%) diff --git a/command_base.py b/command_base.py index 91a4a85..dd548ed 100644 --- a/command_base.py +++ b/command_base.py @@ -157,7 +157,7 @@ def Parse( return True if ret == None or ((type(ret) == bool) and not ret) or len(ret) != 2: - ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + line + "' on pass " + str(pass_no), btn.Line(idx)) + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + btn.Line(idx) + "' on pass " + str(pass_no), btn.Line(idx)) if ret[0]: print(ret[0]) @@ -449,7 +449,7 @@ def Validate_param_n(self, ret, btn, idx, split_line, val_validation, n): if val[AV_TYPE] == PT_TARGET: # targets (label definitions) have pass 1 validation only # check for duplicate label if split_line[n] in btn.symbols[SYM_LABELS]: # Does the label already exist (that's bad)? - return ("Duplicate LABEL", btn.Line(idx)) + return ("Duplicate LABEL " + split_line[n], btn.Line(idx)) # add label to symbol table # Add the new label to the labels in the symbol table btn.symbols[SYM_LABELS][split_line[n]] = idx # key is label, data is line number diff --git a/commands_control.py b/commands_control.py index 0151658..bc73857 100644 --- a/commands_control.py +++ b/commands_control.py @@ -175,6 +175,78 @@ def Init_n_minus_1(self, btn, idx, split_line): # set repeats self.Reset(btn, idx) + def a_eq_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a == b: + return True + + except: + return False + + + def a_ne_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a != b: + return True + + except: + return True # this is an exception. If we can't compare they can't be equal! + + + def a_gt_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a > b: + return True + + except: + return False + + + def a_lt_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a < b: + return True + + except: + return False + + + def a_ge_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a >= b: + return True + + except: + return False + + + def a_le_b(self, btn): + a = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + try: + if a <= b: + return True + + except: + return False + + # ################################################## # ### CLASS Control_Goto_Label ### # ################################################## @@ -201,6 +273,192 @@ def __init__( scripts.Add_command(Control_Goto_Label()) # register the command +# ################################################## +# ### CLASS IF_EQ_GOTO ### +# ################################################## + +# class that defines the IF_EQ_GOTO command +class Control_If_Eq_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_EQ_GOTO, Goto label, if parameters 2 and 3 are equal", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} == {3} Goto {1}"), + ), + "a == b", + self.a_eq_b + ) + + +scripts.Add_command(Control_If_Eq_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_NE_GOTO ### +# ################################################## + +# class that defines the IF_NE_GOTO command +class Control_If_Ne_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_NE_GOTO, Goto label, if parameters 2 and 3 are not equal", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} != {3} Goto {1}"), + ), + "a != b", + self.a_ne_b + ) + + +scripts.Add_command(Control_If_Ne_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_GT_GOTO ### +# ################################################## + +# class that defines the IF_GT_GOTO command +class Control_If_Gt_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_GT_GOTO, Goto label, if parameters 2 is greater than parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} > {3} Goto {1}"), + ), + "a > b", + self.a_gt_b + ) + + +scripts.Add_command(Control_If_Gt_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_GE_GOTO ### +# ################################################## + +# class that defines the IF_GE_GOTO command +class Control_If_Ge_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_GE_GOTO, Goto label, if parameters 2 is greater than or equal to parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} >= {3} Goto {1}"), + ), + "a >= b", + self.a_gt_b + ) + + +scripts.Add_command(Control_If_Ge_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_LT_GOTO ### +# ################################################## + +# class that defines the IF_LT_GOTO command +class Control_If_Lt_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_LT_GOTO, Goto label, if parameters 2 is less than parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} < {3} Goto {1}"), + ), + "a < b", + self.a_lt_b + ) + + +scripts.Add_command(Control_If_Lt_Goto()) # register the command + + +# ################################################## +# ### CLASS IF_LE_GOTO ### +# ################################################## + +# class that defines the IF_LE_GOTO command +class Control_If_Le_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF_LE_GOTO, Goto label, if parameters 2 is less than or equal to parameter 3", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type + ("B", False, AVV_YES,PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} <= {3} Goto {1}"), + ), + "a <= b", + self.a_le_b + ) + + +scripts.Add_command(Control_If_Le_Goto()) # register the command + + # ################################################## # ### CLASS Control_If_Pressed_Goto_Label ### # ################################################## diff --git a/commands_dialog.py b/commands_dialog.py index e8bd824..c0da941 100644 --- a/commands_dialog.py +++ b/commands_dialog.py @@ -3,6 +3,11 @@ LIB = "cmds_dlgs" # name of this library (for logging) + +# ################################################## +# ### CLASS DIALOG_OK_CANCEL ### +# ################################################## + # class that defines the OK Cancel dialog command class Dialog_Ok_Cancel(command_base.Command_Basic): def __init__( @@ -38,9 +43,9 @@ def Process(self, btn, idx, split_line): rval = dialog.DR_ABORT # otherwise we'll substitute an abort code if rval == dialog.DR_OK: # if we got OK - self.Set_param(btn, 2, dialog.DR_OK) # set the return value to OK (if we can) - elif self.Param_count(btn) == 2: # otherwise, if there were 2 parameters - self.Set_param(btn, 2, dialog.DR_CANCEL) # return cancel + self.Set_param(btn, 3, dialog.DR_OK) # set the return value to OK (if we can) + elif self.Param_count(btn) == 3: # otherwise, if there were 3 parameters + self.Set_param(btn, 3, dialog.DR_CANCEL) # return cancel else: # if only 1 parameter and no return parameter btn.root.thread.kill.set() # then kill the thread return -1 @@ -49,6 +54,10 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Dialog_Ok_Cancel()) # register the command +# ################################################## +# ### CLASS DIALOG_INFO ### +# ################################################## + # class that defines an info dialog command class Dialog_Info(command_base.Command_Basic): def __init__( @@ -68,7 +77,8 @@ def __init__( ) ) self.doc = ["A simple dialog with a title, message and OK button. No return value" \ - "is required since the message only requires acknowledgement."] + "is required since the message only requires acknowledgement. This" \ + "will never cause an abort"] def Process(self, btn, idx, split_line): @@ -76,3 +86,43 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Dialog_Info()) # register the command + +# ################################################## +# ### CLASS DIALOG_ERROR ### +# ################################################## + +# class that defines an error dialog command +class Dialog_Error(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DIALOG_ERROR, A simple error dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), + ("Message", False, AVV_NO, PT_STR, None, None), + ("Return", True, AVV_REQD,PT_INT, None, None), + ), + ( # How to log runtime execution + # num params, format string (trailing comma is important) + (2, " Error dialog '{1}'"), + (3, " Error dialog '{1}' returning {3}"), + ) ) + + self.doc = ["A simple dialog with a title, message and Cancel button. Typically" \ + "this should be called without a return variable to allow the script" \ + "to abort. If a return value is specified, the script will continue" \ + "after the dialog is dismissed."] + + + def Process(self, btn, idx, split_line): + ret = dialog.QueuedDialog(btn, DLG_ERROR, (self.Get_param(btn, 1), self.Get_param(btn, 2))) # Call the dialog + if self.Param_count(btn) == 2: + btn.root.thread.kill.set() # then kill the thread + return -1 + + + +scripts.Add_command(Dialog_Error()) # register the command diff --git a/commands_keys.py b/commands_keys.py index a85af2a..999ed07 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -217,3 +217,36 @@ def Run( scripts.Add_command(Keys_String()) # register the command + + +# ################################################## +# ### CLASS Keys_Type ### +# ################################################## + +# class that defines the TYPE command (type a string) +class Keys_Type(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("TYPE, type the text that is the concatenation of all variables passed", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Value", False, AVV_YES, PT_STRS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Type {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + val = '' + for i in range(1, self.Param_count(btn)+1): # for each parameter (after the first) + val += str(self.Get_param(btn, i)) # append all the values (force to string) + kb.write(val) + + +scripts.Add_command(Keys_Type()) # register the command + diff --git a/commands_rpncalc.py b/commands_rpncalc.py index d6cc457..adcb6d7 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -177,7 +177,7 @@ def Register_operators(self): self.operators["!?L"] = (self.is_local_not_def, 1) # is local var not defined self.operators["?G"] = (self.is_global_def, 1) # is global var defined self.operators["!?G"] = (self.is_global_not_def, 1) # is global var not defined - self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc + self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc) def add(self, @@ -676,7 +676,7 @@ def __init__( self, ): - super().__init__("RPN_SET", # the name of the command as you have to enter it in the code + super().__init__("RPN_SET, Sets a string to the concatenation of all the variables passed to it", LIB, ( # Desc Opt Var type p1_val p2_val @@ -696,4 +696,60 @@ def Process(self, btn, idx, split_line): self.Set_param(btn, 1, val) # pass the combined string back -scripts.Add_command(Rpn_Set()) # register the command \ No newline at end of file +scripts.Add_command(Rpn_Set()) # register the command + + +# ################################################## +# ### CLASS RPN_CLEAR ### +# ################################################## + +# class that defines the RPN_CLEAR command -- clears variables @@@ needs validation!!! +class Rpn_Clear(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("RPN_CLEAR, Clear variables or stack", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Function", False, AVV_NO, PT_STR, None, None), + ("Variable", True, AVV_REQD, PT_STRS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Clear {1}"), + (2, " Clear {1}: {2}"), + ) ) + + self.doc= ["If parameter 1 is:", + " 'GLOBALS' All the global variables are cleared", + " 'LOCALS' All the local variables are cleared", + " 'VARS' All variables are cleared", + " 'STACK' The stack is cleared", + " 'ALL' All variables and the stack are cleared", + " 'GLOBAL' v1 [v2 [v3...]] Named global variables v1... are deleted", + " 'LOCAL' v1 [v2 [v3...]] Named local variables v1... are deleted", + " 'VAR' v1 [v2 [v3...]] Named variables v1... are deleted"] + + + def Process(self, btn, idx, split_line): + f = (self.Get_param(btn, 1)).upper() + + if f in ["GLOBALS", "VARS", "ALL"]: + with btn.symbols[SYM_GLOBAL][0]: + btn.symbols[SYM_GLOBAL][1].clear() # clear all global variables + if f in ["LOCALS", "VARS", "ALL"]: + btn.symbols[SYM_LOCAL].clear() # clear all local variables + if f in ["STACK", "ALL"]: + btn.symbols[SYM_STACK].clear() # clear the stack + if f in ["GLOBAL", "VAR"]: + for i in range(2, self.Param_count(btn)+1): + with btn.symbols[SYM_GLOBAL][0]: + variables.undef(split_line[i], btn.symbols[SYM_GLOBAL][1]) + if f in ["LOCAL", "VAR"]: + for i in range(2, self.Param_count(btn)+1): + variables.undef(split_line[i], btn.symbols[SYM_LOCAL]) + + +scripts.Add_command(Rpn_Clear()) # register the command \ No newline at end of file diff --git a/commands_win32.py b/commands_win32.py index ac135cb..bb243be 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -28,6 +28,10 @@ def restore_window(self, hwnd, fg = False): return place[1], old_hwnd, hwnd # useful if you want to minimise it again + # minimise a window + def minimise_window(self, hwnd, fg = False): + win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) # minimise it + # resets windows to what they were before the restore def reset_window(self, old_state): state, old_hwnd, hwnd = old_state @@ -50,6 +54,7 @@ def callback (hwnd, hwnds): hwnds = [] win32gui.EnumWindows (callback, hwnds) + hwnds.sort() return hwnds @@ -267,7 +272,7 @@ def __init__( self, ): - super().__init__("W_FIND_HWND", # the name of the command as you have to enter it in the code + super().__init__("W_FIND_HWND, Returns the handle of the nth exactly matching window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -281,6 +286,11 @@ def __init__( (4, " Find {4}th window titled '{1}', returning handle in {2}. Report {3} total matches"), ) ) + self.doc = ["Searches for an exactly matching window `Title`. If multiple are found,", + "the windows are sorted by process id. The number of matching windows is", + "returned in `M`. If `N` or more are found, the nth window handle is", + "returned in `HWND`. -1 is returned if there is an error."] + def Process(self, btn, idx, split_line): def CheckWindow(hwnd, data): @@ -312,6 +322,150 @@ def CheckWindow(hwnd, data): scripts.Add_command(Win32_Find_Hwnd()) # register the command +# ################################################## +# ### CLASS W_SIMILAR_HWND ### +# ################################################## + +# class that defines the W_SIMILAR_HWND command - returns the nth matching window handle +class Win32_Similar_Hwnd(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_SIMILAR_HWND, Returns the handle of the nth similar window", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Title", False, AVV_NO, PT_STR, None, None), # name to search for + ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND + ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M a 1.25 > aa clst\n\nTEST_01\nTEST_01 1\nTEST_11\nTEST_11 1\nTEST_11 a\nTEST_11 aa\nTEST_11 b\nTEST_21\nTEST_21 a\nTEST_21 aa\nTEST_21 c\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_02\nTEST_02 1\nTEST_02 1.25\nTEST_12\nTEST_12 1\nTEST_12 1.25\nTEST_12 a\nTEST_12 aa\nTEST_12 d\nTEST_22\nTEST_22 a\nTEST_22 aa\nTEST_22 e\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_03\nTEST_03 \"1\"\nTEST_03 \"1.25\"\nTEST_13\nTEST_13 \"1\"\nTEST_13 \"1.25\"\nTEST_13 a\nTEST_13 aa\nTEST_13 f\nTEST_23\nTEST_23 a\nTEST_23 aa\nTEST_23 g\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_04\nTEST_04 \"1\"\nTEST_04 \"1.25\"\nTEST_14\nTEST_14 \"1\"\nTEST_14 \"1.25\"\nTEST_14 a\nTEST_14 aa\nTEST_14 h\nTEST_24\nTEST_24 a\nTEST_24 aa\nTEST_24 i\n\nTEST_11 g\nTEST_11 aa\nTEST_12 g\nTEST_12 aa\n\nTEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 85, - 255, - 0 - ], - "text": "@NAME be" - }, - { - "color": [ - 85, - 170, - 0 - ], - "text": "@NAME be," - }, - { - "color": [ - 85, - 85, - 0 - ], - "text": "@NAME question." - }, - { - "color": [ - 85, - 0, - 0 - ], - "text": "@NAME Yorick" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "TEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 170, - 255, - 0 - ], - "text": "@NAME or" - }, - { - "color": [ - 170, - 170, - 0 - ], - "text": "@NAME that" - }, - { - "color": [ - 170, - 85, - 0 - ], - "text": "@NAME Alas" - }, - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME I" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 5" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME Test Dlg 1\n@DESC Tests a useless dialog\nDIALOG_INFO \"Title 1\" \"Message 1\"" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 255, - 255, - 0 - ], - "text": "@NAME not" - }, - { - "color": [ - 255, - 170, - 0 - ], - "text": "@NAME is" - }, - { - "color": [ - 255, - 85, - 0 - ], - "text": "@NAME poor" - }, - { - "color": [ - 255, - 0, - 0 - ], - "text": "@NAME knew him well" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 4" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n-@DOC Press this button and the other similar button. This should result in\n-@DOC only one dialog appearing at a time. Cancelling the button should\n-@DOC close an open dialog, or prevent a queued dialog from showing.\n-@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "ABORT\nEND" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 3" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "CALL:RETURN_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 2" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "CALL:END_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 255, - 85, - 0 - ], - "text": "- test\nDIALOG_OK_CANCEL \"Continue this LPHK script?\" \"Press OK to continue or Cancel to abort right now.\" a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME A 1\nCALL:ABORT_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME Dump\n@DESC Dump all commands\n@DOC Lists all the heaqers, commands, subroutines, and buttons\n@DOC with whatever we can figure out about them\nTEST_DUMP_DEBUG" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ] - ], - "subroutines": [ - [ - "@SUB RETURN_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "RETURN", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB ABORT_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "ABORT", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB END_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "END", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB ADD_ONE @a%", - "@DESC Adds 1 to the integer parameter passed", - "@DOC Line 1 of documentation", - "@DOC Line 2 of documentation", - "RPN_EVAL view_l < a 1 + > a view_l" - ] - ], - "version": "0.2" +{ + "buttons": [ + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME T" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "- Find the Logbook\n@NAME Find Note pad\nW_FIND_HWND \"Untitled - Notepad\" ml_hwnd n 1\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Norm Dlg\n@DESC Displays a dialog, then prints the returned value\nRPN_EVAL < a view\nDIALOG_INFO \"Information message\" \"Don't look behind you, there might be a clown!\"\nRPN_EVAL < a view\nDIALOG_OK_CANCEL \"Do you accept?\" \"You do not have any clowns standing behind you\" a\nRPN_EVAL < a view" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME To" + }, + { + "color": [ + 0, + 170, + 0 + ], + "text": "@NAME to" + }, + { + "color": [ + 0, + 85, + 0 + ], + "text": "@NAME the" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME E" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test\n@DESC Run a series of tests to prove parameter passing is working\n@DOC This test is intended as a regression tool to be used after modifying\n@DOC the parameter handling code. This series of tests passes almost every\n@DOC combination of parameters to test routines. The output can be difficult\n@DOC to make sense of, but the critical thing is that none of the lines of\n@DOC output between START and END should change. Changes indicate a new\n@DOC type of bad behaviour that needs to be rectified.\n@DOC\n@DOC I test this by having a reference testlog-base.log and running LPHK\n@DOC with the output redirected to testlog.log. I then use Beyond Compare\n@DOC to evaluate differences.\n@DOC\n@DOC Also note that this script requires the command_test.py module that is\n@DOC not intended to be included in production releases.\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_01\nTEST_01 1\nTEST_11\nTEST_11 1\nTEST_11 a\nTEST_11 aa\nTEST_11 b\nTEST_21\nTEST_21 a\nTEST_21 aa\nTEST_21 c\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_02\nTEST_02 1\nTEST_02 1.25\nTEST_12\nTEST_12 1\nTEST_12 1.25\nTEST_12 a\nTEST_12 aa\nTEST_12 d\nTEST_22\nTEST_22 a\nTEST_22 aa\nTEST_22 e\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_03\nTEST_03 \"1\"\nTEST_03 \"1.25\"\nTEST_13\nTEST_13 \"1\"\nTEST_13 \"1.25\"\nTEST_13 a\nTEST_13 aa\nTEST_13 f\nTEST_23\nTEST_23 a\nTEST_23 aa\nTEST_23 g\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_04\nTEST_04 \"1\"\nTEST_04 \"1.25\"\nTEST_14\nTEST_14 \"1\"\nTEST_14 \"1.25\"\nTEST_14 a\nTEST_14 aa\nTEST_14 h\nTEST_24\nTEST_24 a\nTEST_24 aa\nTEST_24 i\n\nTEST_11 g\nTEST_11 aa\nTEST_12 g\nTEST_12 aa\n\nTEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 85, + 255, + 0 + ], + "text": "@NAME be" + }, + { + "color": [ + 85, + 170, + 0 + ], + "text": "@NAME be," + }, + { + "color": [ + 85, + 85, + 0 + ], + "text": "@NAME question." + }, + { + "color": [ + 85, + 0, + 0 + ], + "text": "@NAME Yorick" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME S" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "TEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 170, + 255, + 0 + ], + "text": "@NAME or" + }, + { + "color": [ + 170, + 170, + 0 + ], + "text": "@NAME that" + }, + { + "color": [ + 170, + 85, + 0 + ], + "text": "@NAME Alas" + }, + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME I" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME T" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 5" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test Dlg 1\n@DESC Tests a useless dialog\nDIALOG_INFO \"Title 1\" \"Message 1\"" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 255, + 255, + 0 + ], + "text": "@NAME not" + }, + { + "color": [ + 255, + 170, + 0 + ], + "text": "@NAME is" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "@NAME poor" + }, + { + "color": [ + 255, + 0, + 0 + ], + "text": "@NAME knew him well" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME I" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 4" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n@DOC Press this button and the other similar button. This should result in\n@DOC only one dialog appearing at a time. Cancelling the button should\n@DOC close an open dialog, or prevent a queued dialog from showing.\n@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "ABORT\nEND" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME N" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 3" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:RETURN_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME G" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 2" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:END_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME !" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "- test\nDIALOG_OK_CANCEL \"Continue this LPHK script?\" \"Press OK to continue or Cancel to abort right now.\" a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME A 1\nCALL:ABORT_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Dump\n@DESC Dump all commands\n@DOC Lists all the heaqers, commands, subroutines, and buttons\n@DOC with whatever we can figure out about them\nTEST_DUMP_DEBUG" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME W" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WWW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW WW WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME W W" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WWW WWW" + } + ] + ], + "subroutines": [ + [ + "@SUB RETURN_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "RETURN", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ABORT_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "ABORT", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB END_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "END", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ADD_ONE @a%", + "@DESC Adds 1 to the integer parameter passed", + "@DOC Line 1 of documentation", + "@DOC Line 2 of documentation", + "RPN_EVAL view_l < a 1 + > a view_l" + ] + ], + "version": "0.2" } \ No newline at end of file diff --git a/variables.py b/variables.py index aa0aa6e..51803cc 100644 --- a/variables.py +++ b/variables.py @@ -41,6 +41,14 @@ def is_defined(name, vbls): return vbls and str(name).lower() in vbls +def undef(name, vbls): + # remove a variable from the symbol library (existing or not) + try: + del vbls[str(name).lower()] + except: + pass + + # gets a variable using the default conversion of None if the variable is undefined. def get(name, l_vbls, g_vbls, default=param_convs._None): # get a variable. look in one symbol table, then the next. diff --git a/window.py b/window.py index 97afc5e..2efb549 100644 --- a/window.py +++ b/window.py @@ -462,13 +462,16 @@ def txt_col(x, y): else: return 'white' # otherwise it should be white - def txt_font(x, y): + def txt_font(x, y, round=False): if global_vars.ARGS['fit']: # only do this of we're fitting text t = scripts.buttons[x][y].name # get the text l = len(t) # and its length if l < 5 and l > 0: # if it's a reasonable size - return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit - return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size + if round: + return ("Courier", int(0.75 * BUTTON_SIZE / l), "bold") # round buttons need smaller text + else: + return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit + return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size gap = int(BUTTON_SIZE // 4) @@ -479,29 +482,29 @@ def text_x(x): def text_y(y): return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) - def fmt(x, y): + def fmt(x, y, lines=3, cols=5): t = scripts.buttons[x][y].name # get the text - if len(t) <= 5: # if name is less than 5 characters + if len(t) <= cols: # if name is less than 5 characters return t # return it unchanged tl = t.split() # more complex stuff needs to be split and processed n = 0 # split up any word > 5 characters while n < len(tl): - if len(tl[n]) > 5: - tl = tl[:n] + [tl[n][:5]] + [tl[n][5:]] + tl[n+1:] + if len(tl[n]) > cols: + tl = tl[:n] + [tl[n][:cols]] + [tl[n][cols:]] + tl[n+1:] n += 1 n = 0 # combine ajacent short words while n+1 < len(tl): - if len(tl[n]) + len(tl[n+1]) < 5: + if len(tl[n]) + len(tl[n+1]) < cols: tl[n] = tl[n] + ' ' + tl[n+1] del tl[n+1] else: n += 1 - tl = tl[0:3] # limit to 3 lines + tl = tl[0:lines] # limit to 3 lines m = 0 # find the longest line for x in tl: @@ -537,13 +540,15 @@ def fmt(x, y): for x in range(8): if bx == None or (bx == x): y = 0 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) if bx == None or (bx == 8): for y in range(1, 9): if bx == None or (by == y): x = 8 - self.c.itemconfig(self.grid_rects[x][y], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) + self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) for x in range(8): if bx == None or (bx == x): @@ -568,17 +573,23 @@ def fmt(x, y): else: for x in range(8): y = 0 - self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") + self.grid_rects[x][y] = ( \ + self.draw_button(x, y, color=get_colour(x, y), shape="circle"), + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ + ) for y in range(1, 9): x = 8 - self.grid_rects[x][y] = self.draw_button(x, y, color=get_colour(x, y), shape="circle") + self.grid_rects[x][y] = ( \ + self.draw_button(x, y, color=get_colour(x, y), shape="circle"), + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ + ) for x in range(8): for y in range(1, 9): self.grid_rects[x][y] = ( \ self.draw_button(x, y, color=get_colour(x, y)), \ - self.c.create_text(text_x(x), text_y(y), fill="black", text=scripts.buttons[x][y].name, font=("Courier", BUTTON_SIZE // 5, "bold")) \ + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) \ ) if self.button_mode == LM_RUN: From 88379a22dbd340cbe926ad346799a5cfa7cce105 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 15 Mar 2021 20:11:10 +0800 Subject: [PATCH 62/83] Oopsie --- user_layouts/Testing.lpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_layouts/Testing.lpl b/user_layouts/Testing.lpl index 0486ea7..a0191a5 100644 --- a/user_layouts/Testing.lpl +++ b/user_layouts/Testing.lpl @@ -327,7 +327,7 @@ 255, 0 ], - "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n@DOC Press this button and the other similar button. This should result in\n@DOC only one dialog appearing at a time. Cancelling the button should\n@DOC close an open dialog, or prevent a queued dialog from showing.\n@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" + "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n@DOC Press this button and the other similar button. This should result in\n@DOC only one dialog appearing at a time. Cancelling the button should\n@DOC close an open dialog, or prevent a queued dialog from showing.\n@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" }, { "color": [ From 7dd7714d0745526920103a8245132f17ae6b5359 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 16 Mar 2021 20:32:51 +0800 Subject: [PATCH 63/83] More bugs squashed, more documentation, and more commands * commands_browser_automation.py to provide an interface to automation of browsers. Early stages yet... This requires the "selenium" package and is cross-platform compatible. * commands_browser_automation.py New command BA_START to start an automated browser * commands_browser_automation.py new command BA_NAVIGATE to navigate browser to a particular url * commands_browser_automation.py new command BA_STOP to close the browser. * commands_list.py adds commands_browser_automation.py * commands_header.py improved documentation on @NAME, @DESC, and @DOC header use * commands_rpncalc.py almost full implementation of RPN_CLEAR command * commands_scrape.py modified so that an image can be returned in a variable and used in other commands. This reduces the duplication of commands where data is scraped from different sources * commands_scrape.py add the ability to scrape from the clipboard as well as the screen * commands_scrape.py add a command to return the average RGB value of an image * commands_scrape.py add the ability to get the hamming distance between colours. the combination of this and the previous allow background colours on text boxes to be identified. * commands_subroutines.py a stack of documentation added * commands_subroutines.py fix to bug that prevented parameter modifiers from being correctly extracted * commands_win32 use of pyperclip to get clipboard data in a cross-platform manner. This does not make this module cross-platform, but its a start. * constants.py new variable types PT_WORD, PT_WORDS, PT_OBJ, and PT_ANY to allow "better" definition of commands. * constants.py Addition of new constant LM_DEL to support new "delete" mode * INSTALL/* Added entries for new packages * LPHK.py added LM_DEL to list of modes you can start up in * Scripts.py add new method Del (to cgo along with Copy, Move, etc) to support del mode. * window.py implemented new Del mode for LPHK to allow buttons to be easily deleted. Note delete requires 2 clicks on the same button followed by a yes on a dialog, so you can't delete by accident! * window.py modification to draw canvas to show Del mode in a dangerous looking combination of colours. --- INSTALL/environment-build.yml | 2 + INSTALL/environment.yml | 2 + INSTALL/requirements.txt | 2 + LPHK.py | 2 +- command_list.py | 3 +- commands_browser_automation.py | 117 +++ commands_header.py | 58 ++ commands_rpncalc.py | 65 +- commands_scrape.py | 262 ++++- commands_subroutines.py | 130 ++- commands_win32.py | 26 +- constants.py | 10 +- scripts.py | 1759 ++++++++++++++++---------------- window.py | 14 + 14 files changed, 1503 insertions(+), 949 deletions(-) create mode 100644 commands_browser_automation.py diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index dcf8656..c5df65a 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -18,3 +18,5 @@ dependencies: - pytesseract - imagehash - dhash + - selenium + - pyperclip diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 76817ef..4700ab5 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -17,3 +17,5 @@ dependencies: - pytesseract - imagehash - dhash + - selenium + - pyperclip diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index a91d50d..ca20d8f 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -18,4 +18,6 @@ pywin32==227 pytesseract==0.3.7 ImageHash==4.2.0 dhash==1.3 +selenium==3.141.0 +pyperclip=1.8.0 -e git+git://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py diff --git a/LPHK.py b/LPHK.py index f49ebfb..b570dc4 100755 --- a/LPHK.py +++ b/LPHK.py @@ -112,7 +112,7 @@ def init(): help = "Operate without connection to Launchpad", type=str, choices=[LP_MK1, LP_MK2, LP_MINI, LP_PRO]) ap.add_argument( # option to start with launchpad window in a particular mode "-M", "--mode", - help = "Launchpad mode", type=str, choices=[LM_EDIT, LM_MOVE, LM_SWAP, LM_COPY, LM_RUN], default=LM_EDIT) + help = "Launchpad mode", type=str, choices=[LM_EDIT, LM_MOVE, LM_SWAP, LM_COPY, LM_DEL, LM_RUN], default=LM_EDIT) ap.add_argument( # turn of unnecessary verbosity "-q", "--quiet", help = "Disable information popups", action="store_true") diff --git a/command_list.py b/command_list.py index 55ac48c..65ceb4d 100644 --- a/command_list.py +++ b/command_list.py @@ -14,7 +14,8 @@ commands_pause, \ commands_external, \ commands_subroutines, \ - commands_dialog + commands_dialog, \ + commands_browser_automation # @@@ a test command set for testing things! Will be removed for production try: diff --git a/commands_browser_automation.py b/commands_browser_automation.py new file mode 100644 index 0000000..de030ad --- /dev/null +++ b/commands_browser_automation.py @@ -0,0 +1,117 @@ +# This module is VERY specific to Win32 +import command_base, scripts, traceback +from selenium import webdriver +from constants import * + +LIB = "cmds_baut" # name of this library (for logging) + +# ################################################## +# ### CLASS BAUTO_START ### +# ################################################## + +# class that defines the BA_START command that starts a browser under automated control +class Bauto_Start(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_START, Starts a browser under automated control", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Browser", False, AVV_NO, PT_WORD, None, None), + ("Auto", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Open browser {1} for automation as {2}"), + ) ) + + self.doc = ["Starts a browser using selinium for automated control. The return will", + "be an object if the call suceeds, otherwise it will return 0"] + + + def Process(self, btn, idx, split_line): + br = self.Get_param(btn, 1) + try: + auto = webdriver.Chrome() + except: + traceback.print_exc() + auto = 0 + + self.Set_param(btn, 2, auto) # pass the object back + + +scripts.Add_command(Bauto_Start()) # register the command + + +# ################################################## +# ### CLASS BAUTO_NAVIGATE ### +# ################################################## + +# class that defines the BA_NAVIGATE command navigates the browser to a particular page +class Bauto_Navigate(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_NAVIGATE, Starts a browser under automated control", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Auto", False, AVV_REQD, PT_OBJ, None, None), + ("URL", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Navigate browser to {2}"), + ) ) + + + def Process(self, btn, idx, split_line): + auto = self.Get_param(btn, 1) + url = self.Get_param(btn, 2) + try: + auto.get(url) + except: + traceback.print_exc() + auto = 0 + + +scripts.Add_command(Bauto_Navigate()) # register the command + + +# ################################################## +# ### CLASS BAUTO_STOP ### +# ################################################## + +# class that defines the BA_STOP command to close the browser +class Bauto_Stop(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_STOP, Stops the browser under automated control", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Auto", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Stop browser {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + auto = self.Get_param(btn, 1) + try: + auto.quit() + except: + traceback.print_exc() + auto = 0 + + +scripts.Add_command(Bauto_Stop()) # register the command + + diff --git a/commands_header.py b/commands_header.py index d5d8338..633a4a2 100644 --- a/commands_header.py +++ b/commands_header.py @@ -178,6 +178,42 @@ def __init__( ): super().__init__("@NAME, Names a button") + + self.doc = ["The @NAME header defines a name for a script. This name is also", + "displayed on the LPHK form as annotation for the button the script", + "is assigned to.", + "", + "A simple example is as follows:", + "", + " @NAME Boo!", + "", + "This will cause the script to be named `Boo!`, and for this text to", + "appear on the assigned button and in internally generated documentation.", + "", + "The space on buttons is limited. For the larger square buttons,", + "three lines of five characters can be displayed. For the smaller", + "round buttons, only two lines of three characters will fit.", + "", + "LPHK attempts to display the name as best it can. Firstly, it breaks", + "long words into shorter fragments, then it tries to pack those fragments", + "together. The previous example `Boo!` is less than 5 characters, so", + "it fits completely on one line. The name `Pieces of text to display`", + "would first be broken up into `Piece` `s` `of` `text` `to` `displ` ay`,", + "then joined back up as `Piece` `s of` `text`, The remainder can't be", + "fitted, and is dropped off. The button would contain the text:", + "", + " Piece", + " a of", + " text", + "", + "Shorter text strings are are displayed using larger fonts for greater" + "reasability if the option -f or --fit is specified on the command", + "line.", + "", + "Only a single name header is permitted in a script.", + "", + "This is not permitted in a subroutine because the subroutine is named", + "using the `@SUB` header."] # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) @@ -214,6 +250,17 @@ def __init__( super().__init__("@DESC, Defines a description line") + self.doc = ["The @DESC header defines a short description for a script or subroutine.", + "", + "A simple example is as follows:", + "", + " @DESC Do really amazing things", + "", + "This will cause the script or subroutine to be described as `Do really", + "amazing things`, in internally generated documentation.", + "", + "Only a single description header is permitted in a script or subroutine."] + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) def Validate( @@ -246,6 +293,17 @@ def __init__( super().__init__("@DOC, Adds a line to the documentation text") + self.doc = ["The `@DOC` header allows multiple line documentation to be written for a script", + "or subroutine. Each `@DOC` line is appended to the documentation.", + "", + "A simple example is as follows:", + "", + " @DOC This is the first line of the documentation...", + " @DOC ...and this is the second.", + "", + "When the internal documentation is produced for the script or subroutine, this", + "text will appear."] + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) def Validate( diff --git a/commands_rpncalc.py b/commands_rpncalc.py index adcb6d7..2a31c86 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -699,6 +699,24 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Rpn_Set()) # register the command +# constants for RPN_CLEAR +RC_GLOBALS = "GLOBALS" +RC_LOCALS = "LOCALS" +RC_GLOBAL = "GLOBAL" +RC_LOCAL = "LOCAL" +RC_STACK = "STACK" +RC_VARS = "VARS" +RC_VAR = "VAR" +RC_ALL = "ALL" + +RCG_GLOBAL = [RC_GLOBALS, RC_VARS, RC_ALL] +RCG_LOCAL = [RC_LOCALS, RC_VARS, RC_ALL] +RCG_STACK = [RC_STACK, RC_ALL] +RCG_G_VAR = [RC_GLOBAL, RC_VAR] +RCG_L_VAR = [RC_LOCAL, RC_VAR] +RCG_ANY_VAR = [RC_GLOBAL, RC_LOCAL, RC_VAR] +RCG_ALL = [RC_ALL, RC_VARS, RC_GLOBALS, RC_LOCALS, RC_VAR, RC_GLOBAL, RC_LOCAL, RC_STACK] + # ################################################## # ### CLASS RPN_CLEAR ### # ################################################## @@ -712,9 +730,9 @@ def __init__( super().__init__("RPN_CLEAR, Clear variables or stack", LIB, ( - # Desc Opt Var type p1_val p2_val - ("Function", False, AVV_NO, PT_STR, None, None), - ("Variable", True, AVV_REQD, PT_STRS, None, None), + # Desc Opt Var type p1_val p2_val + ("Function", False, AVV_NO, PT_WORD, None, None), + ("Variable", True, AVV_NO, PT_WORDS, None, None), ), ( # num params, format string (trailing comma is important) @@ -723,33 +741,44 @@ def __init__( ) ) self.doc= ["If parameter 1 is:", - " 'GLOBALS' All the global variables are cleared", - " 'LOCALS' All the local variables are cleared", - " 'VARS' All variables are cleared", - " 'STACK' The stack is cleared", - " 'ALL' All variables and the stack are cleared", - " 'GLOBAL' v1 [v2 [v3...]] Named global variables v1... are deleted", - " 'LOCAL' v1 [v2 [v3...]] Named local variables v1... are deleted", - " 'VAR' v1 [v2 [v3...]] Named variables v1... are deleted"] + f" '{RC_GLOBALS}' All the global variables are cleared", + f" '{RC_LOCALS}' All the local variables are cleared", + f" '{RC_VARS}' All variables are cleared", + f" '{RC_STACK}' The stack is cleared", + f" '{RC_ALL}' All variables and the stack are cleared", + f" '{RC_GLOBAL}' v1 [v2 [v3...]] Named global variables v1... are deleted", + f" '{RC_LOCAL}' v1 [v2 [v3...]] Named local variables v1... are deleted", + f" '{RC_VAR}' v1 [v2 [v3...]] Named variables v1... are deleted"] def Process(self, btn, idx, split_line): f = (self.Get_param(btn, 1)).upper() - if f in ["GLOBALS", "VARS", "ALL"]: + if f in RCG_GLOBAL: with btn.symbols[SYM_GLOBAL][0]: btn.symbols[SYM_GLOBAL][1].clear() # clear all global variables - if f in ["LOCALS", "VARS", "ALL"]: + if f in RCG_LOCAL: btn.symbols[SYM_LOCAL].clear() # clear all local variables - if f in ["STACK", "ALL"]: + if f in RCG_STACK: btn.symbols[SYM_STACK].clear() # clear the stack - if f in ["GLOBAL", "VAR"]: + if f in RCG_G_VAR: for i in range(2, self.Param_count(btn)+1): with btn.symbols[SYM_GLOBAL][0]: - variables.undef(split_line[i], btn.symbols[SYM_GLOBAL][1]) - if f in ["LOCAL", "VAR"]: + variables.undef(self.Get_param(btn, i), btn.symbols[SYM_GLOBAL][1]) + if f in RCG_L_VAR: for i in range(2, self.Param_count(btn)+1): - variables.undef(split_line[i], btn.symbols[SYM_LOCAL]) + variables.undef(self.Get_param(btn, i), btn.symbols[SYM_LOCAL]) + + + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if not split_line[1] in RCG_ALL: # invalid subcommand + c_ok = ', '.join(RCG_ALL[:-1]) + ', or ' + RCG_ALL[-1] + s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret scripts.Add_command(Rpn_Clear()) # register the command \ No newline at end of file diff --git a/commands_scrape.py b/commands_scrape.py index 45c214d..d238bfe 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -35,17 +35,24 @@ def get_image(self, hwnd, p_from, p_to): return image # return the image + # scrapes an image from the clipboard + def get_copied_image(self): + image = PIL.ImageGrab.grabclipboard() # grab the image from he clipboard + + return image # return the image + + # ################################################## -# ### CLASS S_OCR_FORM_TEXT ### +# ### CLASS SCRAPE_SCREEN ### # ################################################## -# class that defines the S_OCR_FORM_TEXT command -- reads text from an image on the form +# class that defines the S_ command -- reads text from an image on the form class Scrape_OCR_Form_Text(Command_Scrape): def __init__( self, ): - super().__init__("S_OCR", # the name of the command as you have to enter it in the code + super().__init__("S_GET_SCREEN, captures a part of the screen or of a window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -53,13 +60,13 @@ def __init__( ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), ("Y2 value", False, AVV_YES, PT_INT, None, None), - ("OCR value", False, AVV_REQD, PT_STR, None, None), + ("Image", False, AVV_REQD, PT_OBJ, None, None), ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (5, " OCR current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " OCR form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (5, " captures current form from ({1}, {2}) to ({3}, {4}) into image {5}"), + (6, " captures form {6} from ({1}, {2}) to ({3}, {4}) into image {5}"), ) ) @@ -71,25 +78,23 @@ def Process(self, btn, idx, split_line): image = self.get_image(hwnd, p_from, p_to) # capture an image - txt = pytesseract.image_to_string(image) # OCR the image - - self.Set_param(btn, 5, txt) # pass the text back + self.Set_param(btn, 5, image) # pass the image back scripts.Add_command(Scrape_OCR_Form_Text()) # register the command # ################################################## -# ### CLASS S_IMAGE_HASH ### +# ### CLASS SCRAPE_GET_WINDOW ### # ################################################## -# class that defines the S_HASH command -- Takes an image and calculates a checksum -class Scrape_Image_Hash(Command_Scrape): +# class that defines the S_GET_WIN command -- returns an image from the screen +class Scrape_Get_Window(Command_Scrape): def __init__( self, ): - super().__init__("S_HASH", # the name of the command as you have to enter it in the code + super().__init__("S_GET_WIN, captures a part of the screen or of a window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -97,13 +102,13 @@ def __init__( ("Y1 value", False, AVV_YES, PT_INT, None, None), ("X2 value", False, AVV_YES, PT_INT, None, None), ("Y2 value", False, AVV_YES, PT_INT, None, None), - ("Hash value", False, AVV_REQD, PT_INT, None, None), + ("Image", False, AVV_REQD, PT_OBJ, None, None), ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (5, " Hash of current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " Hash of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (5, " captures current form from ({1}, {2}) to ({3}, {4}) into image {5}"), + (6, " captures form {6} from ({1}, {2}) to ({3}, {4}) into image {5}"), ) ) @@ -115,6 +120,99 @@ def Process(self, btn, idx, split_line): image = self.get_image(hwnd, p_from, p_to) # capture an image + self.Set_param(btn, 5, image) # pass the image back + + +scripts.Add_command(Scrape_Get_Window()) # register the command + + +# ################################################## +# ### CLASS SCRAPE_GET_CLIPBOARD ### +# ################################################## + +# class that defines the S_GET_CLIP command -- reads text from an image on the form +class Scrape_Get_Clipboard(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_GET_CLIP, Returns the image in the clipboard", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Image", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " place clipboard image into image {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + image = self.get_copied_image() # get clipboard image + + self.Set_param(btn, 1, image) # pass the text back + + +scripts.Add_command(Scrape_Get_Clipboard()) # register the command + + +# class that defines the S_OCR command -- OCR on an image +class Scrape_OCR(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_OCR, performs OCR on an image", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Image", False, AVV_REQD, PT_OBJ, None, None), + ("OCR value", False, AVV_REQD, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " OCR image {1} to {2}"), + ) ) + + + def Process(self, btn, idx, split_line): + image = self.get_copied_image() # get copied image + + txt = pytesseract.image_to_string(image) # OCR the image + + self.Set_param(btn, 1, txt) # pass the text back + + +scripts.Add_command(Scrape_OCR()) # register the command + + +# ################################################## +# ### CLASS S_IMAGE_HASH ### +# ################################################## + +# class that defines the S_HASH command -- Takes an image and calculates a checksum +class Scrape_Image_Hash(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_HASH", # the name of the command as you have to enter it in the code + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Image", False, AVV_REQD, PT_OBJ, None, None), + ("Hash value", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Hash image {1} into {2}"), + ) ) + + + def Process(self, btn, idx, split_line): + image = self.Get_param(btn, 1) # get the image + m = hashlib.md5() # create an md5 hash object with io.BytesIO() as memf: # write image to memory image.save(memf, 'PNG') # as png (lossless) @@ -122,12 +220,68 @@ def Process(self, btn, idx, split_line): m.update(data) # put it in the hash object hash = m.hexdigest() # calculate the md5 hash - self.Set_param(btn, 5, hash) # pass the hash back + self.Set_param(btn, 2, hash) # pass the hash back scripts.Add_command(Scrape_Image_Hash()) # register the command +# ################################################## +# ### CLASS S_IMAGE_COLOUR ### +# ################################################## + +# class that defines the S_COLOUR command -- Takes an image and calculates a checksum +class Scrape_Clipboard_Colour(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_COLOUR, determines the average R, G, and B values of a clipboard image", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Image", False, AVV_REQD, PT_OBJ, None, None), + ("Red", False, AVV_REQD, PT_INT, None, None), + ("Green", False, AVV_REQD, PT_INT, None, None), + ("Blue", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (4, " average colour of image {1} in ({2}, {3}, {4})"), + ) ) + + + def Process(self, btn, idx, split_line): + image = self.Get_param(btn, 1) # get the image + + pixels = image.load() # create the pixel map + + r = 0 # initialise RGB and pixel count to 0 + g = 0 + b = 0 + p = 0 + + for i in range(image.size[0]): # for every col: + for j in range(image.size[1]): # for every row + px = pixels[i, j] + p += 1 + r += px[0] + g += px[1] + b += px[2] + + if p > 0: # calc average data + r = r // p + g = g // p + b = b // p + + self.Set_param(btn, 2, r) # send back RGB + self.Set_param(btn, 3, g) + self.Set_param(btn, 4, b) + + +scripts.Add_command(Scrape_Clipboard_Colour()) # register the command + + # ################################################## # ### CLASS S_IMAGE_FINGERPRINT ### # ################################################## @@ -138,38 +292,21 @@ def __init__( self, ): - super().__init__("S_FINGERPRINT", # the name of the command as you have to enter it in the code + super().__init__("S_FINGERPRINT, Fingerprint an image", LIB, ( # Desc Opt Var type p1_val p2_val - ("X1 value", False, AVV_YES, PT_INT, None, None), - ("Y1 value", False, AVV_YES, PT_INT, None, None), - ("X2 value", False, AVV_YES, PT_INT, None, None), - ("Y2 value", False, AVV_YES, PT_INT, None, None), + ("Image", False, AVV_REQD, PT_OBJ, None, None), ("Fingerprint",False, AVV_REQD, PT_INT, None, None), - ("HWND", True, AVV_REQD, PT_INT, None, None), ), ( # num params, format string (trailing comma is important) - (5, " Fingerprint of current form from ({1}, {2}) to ({3}, {4}) into {5}"), - (6, " Fingerprint of form {6} from ({1}, {2}) to ({3}, {4}) into {5}"), + (2, " Fingerprint of image {1} into {2}"), ) ) def Process(self, btn, idx, split_line): - p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords - p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords - - hwnd = self.Get_param(btn, 6, win32gui.GetForegroundWindow()) # get the window we're using - state = self.restore_window(hwnd, True) # restore the window in question and make it FG - try: - p_from = win32gui.ClientToScreen(hwnd, p_from) # convert to screen coords - p_to = win32gui.ClientToScreen(hwnd, p_to) - box = p_from + p_to # make a tuple with both coords - image = PIL.ImageGrab.grab(bbox = box, all_screens=True) # capture an image - image.save('C:/temp/temp.png') - finally: - self.reset_window(state) # restore windows something like previous states + image = self.Get_param(btn, 1) # get the image fingerprint = int(str(imagehash.dhash(image)),16) # calculate an image fingerprint @@ -217,3 +354,52 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Scrape_Fingerprint_Distance()) # register the command + + +# ################################################## +# ### CLASS SCRAPE_COLOUR_DISTANCE ### +# ################################################## + +# class that defines the S_CDIST command -- calculates the hamming difference between fingerprints +class Scrape_Colour_Distance(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("S_CDIST, Calculate the distance between 2 RGB values", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("R1", False, AVV_YES, PT_INT, None, None), + ("G1", False, AVV_YES, PT_INT, None, None), + ("B1", False, AVV_YES, PT_INT, None, None), + ("R2", False, AVV_YES, PT_INT, None, None), + ("G2", False, AVV_YES, PT_INT, None, None), + ("B2", False, AVV_YES, PT_INT, None, None), + ("Distance", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (7, " Return the hamming distance between colours ({1}, {2}, {3}) and ({4}, {5}, {6}) into {7}"), + ) ) + + self.doc = ["This command calculates the hamming distance between 2 RGB values.", \ + "This can be used to determine how similar 2 colours are. The larger", \ + "the hamming distance, the more different the colours are."] + + + def Process(self, btn, idx, split_line): + r1 = self.Get_param(btn, 1) # get the colours + g1 = self.Get_param(btn, 2) + b1 = self.Get_param(btn, 3) + + r2 = self.Get_param(btn, 4) + g2 = self.Get_param(btn, 5) + b2 = self.Get_param(btn, 6) + + dist = abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + + self.Set_param(btn, 7, dist) # pass the distance back + + +scripts.Add_command(Scrape_Colour_Distance()) # register the command diff --git a/commands_subroutines.py b/commands_subroutines.py index 37a4787..7fb8f42 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -54,7 +54,7 @@ def __init__( Lines # The text of the subroutine/function ): - super().__init__(SUBROUTINE_PREFIX + Name, # the name of the command as you have to enter it in the code + super().__init__(SUBROUTINE_PREFIX + Name + ", Define a subroutine that can be called with named parameters", LIB, Params, ( @@ -64,7 +64,123 @@ def __init__( self.routine = Lines # the routine to execute self.btn = scripts.Button(-1, -1, self.routine, None, Name) # we retain this so we only have to validate it once. executions use a deep-ish copy - + + self.doc = ["This header is used to define a subroutine. Subroutines are loaded", + "separately from button scripts and can be saved in layouts.", + "", + "A subroutine header consists of the the text `@SUB` followed by the", + "name of the subroutine, and then the parameters for the subroutine", + "", + "A simple subtoutine `DO_STUFF` that is called without patameters would", + "be defined as follows:", + "", + " @SUB DO_STUFF", + "", + "This would be followed by a script to so whatever the subroutine needs", + "to do.", + "", + "A calling script would call this subroutine using:", + "", + " CALL:DO_STUFF", + "", + "After completion of the subroutine, control would pass to the statement", + "following the call unless an END or ABORT statement was reached, or", + "the operator cancelled the routine by pressing the Launchpad button", + "a second time." + "", + "Subroutines have their own stacks and local variables as well as access", + "to global variables. For access to information within the calling", + "script, and to return information back to the calling script either", + "global variables or parameters can be used.", + "", + "Parameters are defined by placing legal variable names following the", + "subroutine name on the @SUB line. An example is:", + "", + " @SUB DO_STUFF a b", + "", + "This defines a subroutine that takes 2 positional parameters. By", + "default they are integers, and they are passed by value (that is", + "any changes to their values are not passed back to the calling", + "routine.", + "", + "This subroutine cound be called using a script as follows:", + "", + " CALL:DO_IT 42 var2", + "", + "Because the the parameters are passed by value, constants or variables", + "can be used in the call. In this case, in the subroutine, the local", + "variable `a` would have the value 42, and the local variable `b`", + "would be set to the value of the variable `var2` from the calling", + "script.", + "", + "Parameters can also be defined with special modifiers that change this", + "default behaviour. One way of applying these modifiers to parameters", + "is by following the parameter name with a `+` followed by the modifiers.", + "", + "The modifiers are:", + " `%` or `I` - defines the variable as an integer number", + " `#` or `F` - defines the variable as a float or real number", + " `$` or `S` - defines the variable as a string", + " `!` or `B` - defines the variable as a boolean (not fully implemented)", + " `K` - defines the variable as a key (not fully implemented)", + " `-` or `O` - defines the variable as optional", + " `@` or `R` - defines the variable as call by reference (more later)", + "", + "An example using these modifiers is as follows:", + "", + " @SUB DO_MORE a+I b+FO c+R$", + "", + "These parameters are:", + "", + " a+I - the parameter `a` that is an integer (and required)", + " b+FO - the parameter `b` that is an optional floating point", + " c+R$ - a required call-by reference string variable `c`", + "", + "Valid calls to this subroutine are as follows:", + "", + " CALL:DO_MORE 12", + " CALL:DO_MORE x 12.5 line", + "", + "The first call passes the required first parameter, but not the second", + "optional parameter. Because the second parameter was not passed, no", + "more paramters are required. The subroutine would see the variable `a`", + "have the value 12, the variable `b`, 0.0, and the variable `c` would be", + "a blank string. Attempts to change the value of `c` would succeed, but", + "no variable in the calling routine would be affected.", + "", + "The second call passes the required first parameter (a variable this", + "time), a value of 12.5 for the second parameter, and the variable", + "`line` as the final required parameter. Because the second (optional)", + "parameter was passed, the next parameter (being required) was mandatory.", + "Within the subroutine, `a` would have the value of `x` in the calling", + "routine, `b` would have the value of 12.5, and `c` would have the value", + "of the variable `line` in the calling routine. Changing the value of", + "`c` will also change the value of `line` in the calling routine.", + "", + "This method of applying modifiers to variables can be simplified for all", + "modifiers that are not permitted in variable names. These can also be", + "placed before or after the parameter name the following functionally", + "identical subroutine definition:", + "", + " @SUB DO_MORE a% -b# @c$", + "", + "Subroutines are placed in text files and loaded using the Subroutine|Load", + "menu option. Note that multiple subroutines can be placed in a single", + "file. In this case they must be separated by a line consisting of `===`.", + "", + "Subroutines can call other subroutines and can probably be called", + "recursively. A script will fail to load if it depends on a subroutine", + "that is not present. Similarly, subroutines that depend on other", + "subroutines will fail to load if those subroutines are not available.", + "", + "During execution of a subroutine the command RETURN will immediately", + "return control to the calling script or subroutine. The commands END", + "and ABORT will stop execution immediately without returning to the caller.", + "", + "Parameter names follow the same rules as variable names. They must satart", + "with an alpha character, and may then be followed by any number of", + "alpha-numeric characters and underscores (`_`)."] + # process for a subroutine handles parameter passing and then passes off the process to the script in a "dummy" button def Process(self, btn, idx, split_line): @@ -156,12 +272,14 @@ def Get_Name_And_Params(lines, sub_n, fname): if p >= 0: mods += var[p+1:] var = var[:p] - elif not variables.valid_var_name(var): # if there's no '+', look for trailing modifiers without one + + if not variables.valid_var_name(var): # if there's no '+', look for trailing modifiers without one l = len(var) while l > 1: - if variables.valid_var_name(var[:l-1]): - mods += var[l:] - var = var[:l-1] + if not variables.valid_var_name(var): + mods += var[-1] + var = var[:-1] + else: break l -= 1 diff --git a/commands_win32.py b/commands_win32.py index bb243be..b82cf3c 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -364,7 +364,7 @@ def CheckWindow(hwnd, data): data = {'title':title, 'hwnds':hwnds} # data structure to be used by the callback routine win32gui.EnumWindows(CheckWindow, data) # enumerate windows - + hwnds = data['hwnds'] # this is now probably in front to back order hwnds.sort() # helps to ensure we get the windows in the same order. (creation?) @@ -418,7 +418,6 @@ def CheckWindow(hwnd, data): if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd) != '': if data[1] == SENTINEL: # if no window specified if hwnd != data[0]: # is it not LPHK? - print(' 2', hwnd, data[0], win32gui.GetWindowText(hwnd))#@@@ self.minimise_window(hwnd) else: if hwnd == data[1]: # does it match? @@ -489,14 +488,16 @@ def __init__( ) ) def Process(self, btn, idx, split_line): - hwnd = win32gui.GetForegroundWindow() # get the current window try: # clear the clipboard win32clipboard.OpenClipboard(hwnd) win32clipboard.EmptyClipboard() finally: - win32clipboard.CloseClipboard() + try: + win32clipboard.CloseClipboard() + except: + pass # we don't care if the clipboard wasn't opened! try: # do the keyboard stuff for copy (sending a WM_COPY message does not always work) kb.press(kb.sp('ctrl')) @@ -504,13 +505,18 @@ def Process(self, btn, idx, split_line): finally: kb.release(kb.sp('ctrl')) + import pyperclip # pyperclip is cross-platform (better than using windows specific code) + + w = 0 + t = '' + while t == '' and w < 1: # we often have to wait for the text to appear in the clipboard + btn.Safe_sleep(DELAY_EXIT_CHECK) + w += DELAY_EXIT_CHECK + t = pyperclip.paste() + if self.Param_count(btn) > 0: # save to variable if required - try: - win32clipboard.OpenClipboard(hwnd) - t = win32clipboard.GetClipboardData(win32con.CF_TEXT) - self.Set_param(btn, 1, t) - finally: - win32clipboard.CloseClipboard() + t = t.rstrip('\r\n') # remove any line terminators + self.Set_param(btn, 1, t) scripts.Add_command(Win32_Copy()) # register the command diff --git a/constants.py b/constants.py index d2b3f9d..a1cbc1c 100644 --- a/constants.py +++ b/constants.py @@ -59,6 +59,7 @@ AVVS_NO = {AVV_NO} # only allow literals AVVS_YES = {AVV_YES, AVV_REQD} # AVV_YES is potentially ambiguous! AVVS_AMB = {AVV_NO, AVV_REQD} # These are not ambiguous +AVVS_REQ = {AVV_REQD} # ONLY variables AV_P1_VALIDATION = 4 AV_P2_VALIDATION = 5 @@ -70,11 +71,15 @@ PT_STR = ("str", param_convs._str, True, False, AVVS_ALL) # a quoted string PT_STRS = ("strs", param_convs._str, True, True, AVVS_ALL) # 1 or more quoted strings PT_LINE = ("line", param_convs._str, True, True, AVVS_NO) # the rest of the line following first preceeding whitespace -PT_TEXT = ("text", param_convs._str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED +PT_TEXT = ("text", param_convs._str, False, False, AVVS_AMB) # a string without whitespace @@@ DEPRECATED (Not necessarily...) PT_LABEL = ("label", param_convs._str, True, False, AVVS_NO) # Note that this is for a reference to a label, not the definition of a label! PT_TARGET = ("target", param_convs._str, True, False, AVVS_NO) # Note that this is for the definition of a target (e.g. creating a label) PT_KEY = ("key", param_convs._str, True, False, AVVS_NO) # This is a key literal PT_BOOL = ("bool", param_convs._str, True, False, AVVS_ALL) # True/False, Yes/No, Y/N, nonzero/zero <-- for variables +PT_WORD = ("word", param_convs._str, False, False, AVVS_NO) # a parameter not in quotes +PT_WORDS = ("word", param_convs._str, False, True, AVVS_NO) # a parameter not in quotes +PT_OBJ = ("object", param_convs._None, False, False, AVVS_REQ) # an object type +PT_ANY = ("any", param_convs._None, False, False, AVVS_ALL) # allow any type of parameter@@@@@ # constants for auto_message AM_COUNT = 0 @@ -108,6 +113,7 @@ LM_MOVE = "move" LM_SWAP = "swap" LM_COPY = "copy" +LM_DEL = "del" LM_RUN = "run" # Dump constants @@ -126,4 +132,4 @@ DLG_INFO = 1 # a simple titled box with OK DLG_OK_CANCEL = 2 # a simple titled box with OK and Cancel -DLG_ERROR = 3 # a simple titled box with Cancel \ No newline at end of file +DLG_ERROR = 3 # a simple titled box with Cancel diff --git a/scripts.py b/scripts.py index 859e5bd..129f784 100644 --- a/scripts.py +++ b/scripts.py @@ -1,873 +1,886 @@ -import threading, webbrowser, os, subprocess -from time import sleep -from functools import partial -import lp_events, lp_colors, kb, sound, ms, files, command_base, variables -from constants import * -from window import Redraw - - -# VALID_COMMAND is a dictionary of all commands available. -# it is initially empty because now we load it dynamically - -VALID_COMMANDS = dict() - - -# GLOBALS is likewise empty until global variables get created - -GLOBALS = dict() # the globals themselvs -GLOBAL_LOCK = threading.Lock() # a lock for the globals to prevent simultaneous access - - -# Add a new command. This removes any existing command of the same name from the VALID_COMMANDS -# and returns it as the result - -def Add_command( - a_command: command_base.Command_Basic # the command or header to add - ): - - if a_command.name in VALID_COMMANDS: # or if it was a command - p = VALID_COMMANDS.pop(a_command.name) # pop that too - else: # otherwise - p = None # the return value will be None (we're not replacing anything) - - VALID_COMMANDS[a_command.name] = a_command # add the new command - - return p # return any replaced command - - -# Remove a command. This could be useful in handling subroutines - -def Remove_command( - command_name # the command to remove - ): - - if command_name in VALID_COMMANDS: # check command - p = VALID_COMMANDS.pop(command_name) # remove the command - else: - p = None # nothing to remove - - return p # the thing we removed - - -# display info on all commands and headers - -def Dump_commands(style=DS_NORMAL): - def get_name(c): - if isinstance(c, command_base.Command_Basic): - return c.name - elif isinstance(c, Button): - return c.coords - else: - return "ERROR" - - def get_desc(c): - ret = '' - if isinstance(c, command_base.Command_Basic): - if hasattr(c, 'desc') and not callable(c.desc): - ret = c.desc - if hasattr(c, 'btn') and not callable(c.btn) and c.btn: - if c.btn.desc != "": - ret = c.btn.desc - elif isinstance(c, Button): - ret = c.desc - else: - ret = "ERROR" - - return ret - - def dump_name(c_type, c): - print(f" {c_type} \"{get_name(c)}\"", end="") - desc = get_desc(c) - if desc == "": - print() - else: - print(f" - {desc}") - - def get_doc(c): - ret = [] - if isinstance(c, command_base.Command_Basic): - if hasattr(c, 'doc') and not callable(c.doc): - ret = c.doc - if hasattr(c, 'btn') and not callable(c.btn) and c.btn: - if c.btn.doc != []: - ret = c.btn.doc - elif isinstance(c, Button): - ret = c.doc - else: - ret = ["ERROR"] - - return ret - - def dump_doc(c): - doc = get_doc(c) - if doc != []: - print(" Notes") - for n in doc: - print(f" {n}") - - def dump_ancestory(c): - print(" Ancestory") - print(f" {type(c)}") - a = type(c).__bases__[0] - while a != object: - print(f" {a}") - a = a.__bases__[0] - - def dump_params(c): - if isinstance(c, command_base.Command_Basic): - print(" Parameters") - if c.auto_validate == None: - print(" Parameters undocumented (Auto-validation is not defined)") - elif len(c.auto_validate) == 0: - print(" No parameters") - else: - for v in c.auto_validate: - print(f" {v[AV_DESCRIPTION]} - {v[AV_TYPE][AVT_DESC]}", end="") - - if v[AV_OPTIONAL]: - print(" (opt),", end="") - else: - print(" (reqd),", end="") - - if v[AV_VAR_OK] == AVV_NO: - print(" constant only") - elif v[AV_VAR_OK] == AVV_YES: - print(" variable permitted") - elif v[AV_VAR_OK] == AVV_REQD: - print(" variable required (possible return value)") - else: - print(" UNKNOWN VALUE") - - def dump(c_type, c, style): - dump_name(c_type, c) - dump_doc(c) - if D_DEBUG in style: - dump_ancestory(c) - dump_params(c) - - print() - - import commands_subroutines - - if D_HEADERS in style: - print("HEADERS") - print() - for cmd in VALID_COMMANDS: - if isinstance(VALID_COMMANDS[cmd], command_base.Command_Header): - dump("Header", VALID_COMMANDS[cmd], style) - - if D_COMMANDS in style: - print("COMMANDS") - print() - for cmd in VALID_COMMANDS: - if not (isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine) or \ - isinstance(VALID_COMMANDS[cmd], command_base.Command_Header)): - dump("Command", VALID_COMMANDS[cmd], style) - - if D_SUBROUTINES in style: - print("SUBROUTINES") - print() - for cmd in VALID_COMMANDS: - if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): - dump("Subroutine", VALID_COMMANDS[cmd], style) - - if D_BUTTONS in style: - print("BUTTONS") - print() - global buttons - for x in range(9): - for y in range(9): - btn = buttons[x][y] - if btn.script_str != "": - dump("Button", btn, style) - - -# Create a new symbol table. This contains information required for the script to run -# it includes the locations of labels, loop counters, etc. If we implement variables -# this is where we would place them - -def New_symbol_table(): - # returns a new (blank) symbol table - # symbol table is dictionary of objects - symbols = { - SYM_REPEATS: dict(), - SYM_ORIGINAL: dict(), - SYM_LABELS: dict(), - SYM_MOUSE: tuple(), - SYM_GLOBAL: [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) - SYM_LOCAL: dict(), # local (to the script) variables (with no lock) - SYM_STACK: [] } # script stack (for RPN_EVAL) - - return symbols - - -# ################################################## -# ### CLASS Button ### -# ################################################## - -# class that defines a button. -# A button is a class containing all that's essential for a button. -class Button(): - def __init__( - self, - x, # The button column - y, # The button row - script_str, # The Script - root = None, # Who called us - name = '' # name of this button (subroutine) - ): - - self.x = x - self.y = y - self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine - self.script_str = script_str # The script - - self.name = None - self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? - self.desc = "" - self.doc = [] - - self.validated = False # Has the script been validated? - self.symbols = None # The symbol table for the button - self.script_lines = None # the lines of the script - self.thread = None # the thread associated with this button - self._running = False # is the script running? - self.is_async = False # async execution flag - - # The "root" is the button that is scheduled. This allows subroutines to check if the - # initiating button has been killed. - if root == None: # if we are not being called - self.root = self # then we are the root - else: # otherwise - self.root = root # the caller is the root - - - # let us set/change the name of a button - def Set_name(self, name): - update = self.name != None # it is initialisation if the original contents is None - self.name = name # update the name - - self.coords = '' # Start the process of updating the coords - - if self.is_button: # include actual coords if it is actually a button - self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? - if self.name != "": # If it has a name, let's include that too - self.coords = " ".join([self.name, self.coords]) # remember that subroutines don't have coordinates - - if update and self.is_button: # no need to update the window on initialisation - Redraw(self.x, self.y) # and we only need to update this button - - - def running(self, set_to=None): - if type(set_to) == bool and set_to != self._running: - self._running = set_to - Redraw(self.x, self.y) # redraw the canvas when the button run status is changed - - return self._running - - - # Do what is required to parse the script. Parsing does not output any information unless it is an error - def Parse_script(self): - if self.validated: # we don't want to repeat validation over and over - return True - - if self.script_lines == None: # A little setup if the script lines are not created - if isinstance(self.script_str, list): # Subroutines already have this as a list of lines - self.script_lines = self.script_str # Copy the lines - else: # But commands just have the raw stream from a file - self.script_lines = self.script_str.split('\n') # Create the lines - self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks - - self.symbols = New_symbol_table() # Create a shiny new symbol table - self.is_async = False # default is NOT async - - err = True - errors = 0 # no errors found - - for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, - # pass 2 symbol check & assocoated processing - for idx,line in enumerate(self.script_lines): # gen line number and text - if self.Is_ignorable_line(line): - continue # don't process ignorable lines - - cmd_txt = self.Split_cmd_text(line) # get the name of the command - - if cmd_txt in VALID_COMMANDS: # if first element is a command - command = VALID_COMMANDS[cmd_txt]# get the command itself - split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately - - if type(split_line) == tuple: - if err == True: - err = split_line - errors += 1 - else: - res = command.Parse(self, idx, split_line, pass_no); - if res != True: - if err == True: - err = res # note the error - errors += 1 # and 1 more error - else: - msg = " Invalid command '" + cmd_txt + "' on line " + str(idx+1) + "." - if err == True: - err = (msg, line) # note the error - print (msg) - errors += 1 # and 1 more error - - if err != True: - if self.is_button: - print('Pass ' + str(pass_no) + ' complete for button ' + self.coords + '. ' + str(errors) + ' errors detected.') - else: - print('Pass ' + str(pass_no) + ' complete for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') - break # errors prevent next pass - - return err # success or failure - - - # copies parsed info from self to new_btn - def Copy_parsed(self, new_btn, name="SUB"): - new_btn.script_lines = self.script_lines # Copy the lines - new_btn.coords = "(" + name + ")" # set the name - - new_btn.symbols = New_symbol_table() - new_btn.symbols[SYM_REPEATS] = self.symbols[SYM_REPEATS].copy() # copy the repeats - new_btn.symbols[SYM_ORIGINAL] = self.symbols[SYM_ORIGINAL].copy() # and the original values - new_btn.symbols[SYM_LABELS] = self.symbols[SYM_LABELS].copy() # and the position of labels - - new_btn.is_async = self.is_async # default is NOT async - - - # check "self" for death notification - def Check_self_kill(self, killfunc=None): - if not self.thread: - print ("expecting a thread in ", self.coords) - return False - - if self.thread.kill.is_set(): - print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") - #self.thread.kill.clear() - if not self.is_async: - self.running(False) - if killfunc: - killfunc() - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() - return True - else: - return False - - - # Check_kill now checks the root button for death notifications - def Check_kill(self, killfunc=None): - return self.root.Check_self_kill(killfunc) - - - # a sleep method that works with the multiple threads - def Safe_sleep(self, time, endfunc=None): - while time > DELAY_EXIT_CHECK: - sleep(DELAY_EXIT_CHECK) - time -= DELAY_EXIT_CHECK - if self.Check_kill(endfunc): - return False - if time > 0: - sleep(time) - if endfunc: - endfunc() - return True - - - # some lines can be ignored. These include blank lines and comments. It's faster to identify them - # before trying to process them than treat them as an exception afterwards. - - def Is_ignorable_line(self, line): - line = line.strip() # remove leading and trailing spaces - if line != "": - if line[0] == "-": - return True # non-blank lines starting with a hyphen are comments (and can be ignored) - else: - return False # other non-blank lines are significant - else: - return True # blank lines can be igmored - - - def Schedule_script(self): - # @@@ may be worth checking to see if it's a subroutine. Because subroutines shouldn't use this - global to_run - - if self.thread != None: - if self.thread.is_alive(): - # @@@ The following code creates a problem if a script is looking for a second keypress - # @@@ Maybe we need an option to make a script un-interruptable, or alternately require - # @@@ *something* else (maybe ctrl-alt) to be pressed to allow the kill to take place. - print("[scripts] " + self.coords + " Script already running, killing script....") - self.thread.kill.set() - return - - if (self.x, self.y) in [l[1:] for l in to_run]: - print("[scripts] " + self.coords + " Script already scheduled, unscheduling...") - indexes = [i for i, v in enumerate(to_run) if ((v[1] == self.x) and (v[2] == self.y))] - for index in indexes[::-1]: - temp = to_run.pop(index) - return - - if self.is_async: - print("[scripts] " + self.coords + " Starting asynchronous script in background...") - self.thread = threading.Thread(target=Run_script, args=()) - self.thread.kill = threading.Event() - self.thread.start() - elif not self.running(): - print("[scripts] " + self.coords + " No script running, starting script in background...") - self.thread = threading.Thread(target=self.Run_script_and_run_next, args=()) - self.thread.kill = threading.Event() - self.thread.start() - else: - print("[scripts] " + self.coords + " A script is already running, scheduling...") - to_run.append((self.x, self.y)) - - lp_colors.updateXY(self.x, self.y) - - - def Run_next(self): - global to_run - global buttons - - if len(to_run) > 0: - tup = to_run.pop(0) - x = tup[0] - y = tup[1] - - btn = buttons[x][y] - btn.Schedule_script() - - - def Run_script_and_run_next(self): - self.Run_script() - self.Run_next() - - - def Line(self, idx): - if self.script_lines and idx >=0 and idx < len(self.script_lines): - return self.Fix_comment(self.script_lines[idx]) - else: - return "" - - - def Fix_comment(self, line): - # Ensure there's a space after the comment character - if len(line) > 1 and line[0] == "-" and line[1] != " ": - return line[0] + " " + line[1:] - else: - return line - - - def Split_cmd_text(self, line): - # Get the command text - line += ' ' - return line[0:line.find(" ")] - - - def Split_text(self, command, cmd_txt, line): - # Split line differently for "text" type commands - if isinstance(command, command_base.Command_Text_Basic): - # just split the command from the rest of the text - return [cmd_txt, line[len(cmd_txt)+1:]] - else: - def split1(line): # just strip off a single (non-quoted) parameter - param = line.split()[0] # get the parameter - line = line[len(param):].strip() # strip off the parameter - - return param, line # return the parameter and the rest of the line - - # grab a quoted string from the line passed. Handles embedded quotes - def strip_quoted(line): - l2 = line # a copy of the line we can edit - q = l2[0] # the first character is assumed to be a quote - out = '' # nothing to output yet - l2 = l2[1:] # strip the quote from the string - while len(l2) > 0: # while something remains in the line - if l2[0] == q: # if the quote is repeated - if len(l2) == 1 or l2[1] == ' ': # and if it's the last character or followed by a space - l2 = l2[1:].strip() # clean up the rest of the string - return True, out, l2 # return success - elif len(l2) > 1 and l2[1] == q: # if we have 2 quotes in a row - out += q # then this is literally a quote - l2 = l2[2:] # but we need to clean up 2 characters this time - else: - return False, out, line # any other quote-related stuff must be an error - else: # for non-quote characters - out += l2[0] # we just pass them through to the output string - l2 = l2[1:] # and strip them off. - - return False, out, line # if we fall through, that's an error (no closing quote) - - # for all other commands, split on spaces - if isinstance(command, command_base.Command_Basic): - pline = line # something we can alter - avl = command.auto_validate - if avl != None and len(avl) > 0: - cmd, pline = split1(pline) # the command is always a simple split - sline = [cmd] # add it to the return variable - - n = -1 # initialise parameter number pointer to one before the first - while len(pline) > 0: # keep stripping parameters while the line has some content - n += 1 # point to the next parameters - if n < len(avl): # if this parameter has an auto-validation - av = avl[n] # then grab it - else: # otherwise the last parameter must allow for multiple values - av = avl[-1] # so take the last auto-validation - - desc = av[AV_TYPE][AVT_DESC] # get the description of the parameter type (not the description of the parameter!) - - if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Is this one that wants quoted strings? - if pline[0] in ['"', "'", '`']: # if so, does it start with an acceptable quote? - if av[AV_VAR_OK] == AVV_REQD: # it's a problem if a variable is required - return ('Error, quoted string not permitted for param #' + str(n+1), line) # literal not expected - else: - ok, param, pline = strip_quoted(pline) # otherwise we can strip off a quoted string - if ok: # and if that suceeded - sline += ['"'+param] # we'll add it as the parameter value. Note we add a leading " to distinguish it from a variable - else: - return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote - else: # if we want a quoted string, but value doesn't start with a quote - if av[AV_VAR_OK] != AVV_NO: # Are we allowed to pass a variable? - param = pline.split()[0] # then that's OK, just strip off an un-quoted string - pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) - if not variables.valid_var_name(param): # but check it's a valid variable name - return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... - else: - sline += [param] # add it to the list of parameters if it's OK - else: - return ('Error starting quoted string for param#' + str(n+1), line) # This is generally a missing initial quote - - elif desc == PT_LINE[AVT_DESC]: # the rest of the line (regardless of spaces) - sline += [line] # just grab the rest of the line - pline = "" # and leave nothing behind - - else: # in all other cases - param = pline.split(" ")[0] # just strip the first unquoted parameter (@@@ why not use strip1()???) - sline += [param] - pline = pline[len(param):].strip() - - return sline # return a list of command and parameters - - else: - # without autovalidate we just split on spaces - return line.split(" ") - - - # run a script - def Run_script(self): - # @@@ maybe check we're not a subroutine (subroutines should not use this) - lp_colors.updateXY(self.x, self.y) - - if self.Validate_script() != True: - return - - print("[scripts] " + self.coords + " Now running script...") - - self.running(not self.is_async) - - cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters - if cmd_txt in VALID_COMMANDS: - command = VALID_COMMANDS[cmd_txt] - command.Run(self, -1, [cmd_txt]) - - if len(self.script_lines) > 0: - self.running(True) - - def Main_logic(idx): # the main logic to run a line of a script - if self.Check_kill(): # first check to see if we've been asked to die - return idx + 1 # we just return the next line, @@@ returning -1 is better - - line = self.Line(idx) # get the line of the script - - # Handle completely blank lines - if line == "": - return idx + 1 - - # Get the command text - cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact - - # Now get the command object - if cmd_txt in VALID_COMMANDS: # make sure it's a valid command - command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command - - split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters - - if type(split_line) == tuple: # bad news if we get a tuple rather than a list - print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") - else: - # now run the command - return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out - else: - print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") - - return idx + 1 # defaut action is to ask for the next line - - run = True # flag that we're running - idx = 0 # point at the first line - while run: # and while we're still running - idx = Main_logic(idx) # run the current line - if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid - run = False # then we're not going to keep running! - - if not self.is_async: # async commands don't just end - self.running(False) # they have to say they're not running - - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours - - print("[scripts] " + self.coords + " Script ended.") # and print (log?) that the script is complete - - - # run a subroutine. This is a simplified version of running a script because the script takes care of being scheduled and killed - # @@@ this is so close to run_script that it probably should be merged with it at some point -- after I know its working - def Run_subroutine(self): - # @@@ maybe check that we **are** a subroutine first. This is for subroutines ONLY - if self.Validate_script() != True: # validates if not validated - return - - print("[scripts] " + self.coords + " Now running subroutine ...") - - self.running(not self.is_async) # @@@ not sure a async subroutine makes sense - - cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters - if cmd_txt in VALID_COMMANDS: - command = VALID_COMMANDS[cmd_txt] - command.Run(self, -1, [cmd_txt]) - - if len(self.script_lines) > 0: - self.running(True) - - def Main_logic(idx): # the main logic to run a line of a script - if self.Check_kill(): # first check on our death notification - return idx + 1 # we just return the next line, @@@ returning -1 is better - - line = self.Line(idx) # get the line of the script - - # Handle completely blank lines - if line == "": - return idx + 1 - - # Get the command text - cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact - - # Now get the command object - if cmd_txt in VALID_COMMANDS: # make sure it's a valid command - command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command - - split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters - - if type(split_line) == tuple: # bad news if we get a tuple rather than a list - print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") - else: - # now run the command - return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out - else: - print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") - - return idx + 1 # defaut action is to ask for the next line - - run = True # flag that we're running - idx = 0 # point at the first line - while run: # and while we're still running - idx = Main_logic(idx) # run the current line - if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid - run = False # then we're not going to keep running! - - if not self.is_async: # async commands don't just end @@@ again, not sure this makes sense for subroutines - self.running(False) # they have to say they're not running - - print("[scripts] " + self.coords + " Subroutine ended.") # and print (log?) that the script is complete - - - # validating a script consists of doing the checks that we do prior to running, but - # we won't run it afterwards. - def Validate_script(self): - if self.validated or self.script_str == "": # If valid or there is no script... - self.validated = True - return True # ...validation succeeds! - - if self.Parse_script(): # If parsing is OK - self.validated = True # Script is valid - - if len(self.script_lines) > 0: # look for async header and set flag - cmd_txt = self.Split_cmd_text(self.script_lines[0]) - self.is_async = cmd_txt in VALID_COMMANDS and \ - isinstance(VALID_COMMANDS[cmd_txt], command_base.Command_Header) and \ - VALID_COMMANDS[cmd_txt].is_async - else: - self.symbols = None # otherwise destroy symbol table - - return self.validated # and tell us the result - - -# define the buttons structure here. Note that subroutines will likely be a different sort of button, so this may change -buttons = [[Button(x, y, "") for y in range(9)] for x in range(9)] -to_run = [] - - -# bind a button (Note that you can pass a validated button as script_str too) -def Bind(x, y, script_str, color): - global to_run - global buttons - - if isinstance(script_str, Button): # if a button was passed - btn = script_str # then we can skipp the button creation - btn.x = x - btn.y = y - btn.Set_name(btn.name) # force recalc of coords - else: - btn = Button(x, y, script_str) - try: - btn.Validate_script() - except: - pass - - buttons[x][y] = btn - - if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button - for index in indexes[::-1]: # and for each of them (in reverse order) - temp = to_run.pop(index) # Remove them from the list - return # @@@ Why do we return here? - - schedule_script_bindable = lambda a, b: btn.Schedule_script() # @@@ What is this doing? - - lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) - files.layout_changed_since_load = True # Mark the layout as changed - Redraw(x, y) - - -# unbind a button -def Unbind(x, y): - global to_run - global buttons - - lp_events.unbind(x, y) # Clear any events associated with the button - - btn = Button(x, y, "") # create the new blank button - - if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... - indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button - for index in indexes[::-1]: # and for each of them (in reverse order) - temp = to_run.pop(index) # Remove them from the list - buttons[x][y] = btn # Clear the button script - #return # WHY do we return here? - - if btn.thread != None: # If the button is actially executing - thread.kill.set() # then kill it - - buttons[x][y] = btn # Clear the button script - - files.layout_changed_since_load = True # Mark the layout as changed - Redraw(x, y) - - -# swap details for two buttons -def Swap(x1, y1, x2, y2): - global text - - color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 - color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 - - btn_1 = buttons[x1][y1] # btn #1 - btn_2 = buttons[x2][y2] # btn #2 - - Unbind(x1, y1) # Unbind #1 - if btn_2.script_str != "": # If there is a script #2... - Bind(x1, y1, btn_2, color_2) # ...bind it to #1 - lp_colors.updateXY(x1, y1) # Update the colours for btn #1 - - Unbind(x2, y2) # Do the reverse for #2 - if btn_1.script_str != "": - Bind(x2, y2, btn_1, color_1) - lp_colors.updateXY(x2, y2) - - files.layout_changed_since_load = True # Flag that the layout has changed - - -# Duplicate a button -def Copy(x1, y1, x2, y2): - global buttons - - color_1 = lp_colors.curr_colors[x1][y1] # Get colour of btn to be copied - - script_1 = buttons[x1, y1].script_str # Get script to be copied - - Unbind(x2, y2) # Unbind the destination - if script_1 != "": # If we're copying a button with a script... - Bind(x2, y2, script_1, color_1) # ...bind the details to the destination - lp_colors.updateXY(x2, y2) # Update the colours - - files.layout_changed_since_load = True # Flag the layout as changed - - -# move a button -def Move(x1, y1, x2, y2): - global buttons - if (x1, y1) == (x2, y2): - return - - color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour - - btn_1 = buttons[x1][y1] # Get source button script - - Unbind(x1, y1) # Unbind *both* buttons - Unbind(x2, y2) - - if btn_1.script_str != "": # If the source had a script... - Bind(x2, y2, btn_1, color_1) # ...bind it to the destination - lp_colors.updateXY(x2, y2) # Update the destination colours - - files.layout_changed_since_load = True # And flag the layout as changed - - -# determine if a key is bound -def Is_bound(x, y): - global buttons - - if buttons[x][y].script_str == "": # If there is no script... - return False # ...it's not bound - else: - return True # Otherwise it is - - -# kill all threads -def kill_all(): - global buttons - global to_run - - to_run = [] # nothing queued to run - - for x in range(9): # For each column... - for y in range(9): # ...and row - btn = buttons[x][y] - if btn.thread is not None: # If there is a thread... - if btn.thread.isAlive(): # ...and if the thread is alive... - btn.thread.kill.set() # ...kill it - - -# Unbind all keys. -def Unbind_all(): - lp_events.unbind_all() # Unbind all events - text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank - - kill_all() # stop everything running - - files.curr_layout = None # There is no current layout - files.layout_changed_since_load = False # So mark it as unchanged - - -# Unload all subroutines. -def Unload_all(): - kill_all() # stop everything running - - subs = [] # list of subroutines to remove - for cmd in VALID_COMMANDS: # for all the commands that exist - if cmd.startswith(SUBROUTINE_PREFIX):# if this command is a subroutine - subs += [cmd] # add the command to the list - - for cmd in subs: # for each subroutine we've found - Remove_command(cmd) # remove it - - files.layout_changed_since_load = True # mark layout as changed - - +import threading, webbrowser, os, subprocess +from time import sleep +from functools import partial +import lp_events, lp_colors, kb, sound, ms, files, command_base, variables +from constants import * +from window import Redraw + + +# VALID_COMMAND is a dictionary of all commands available. +# it is initially empty because now we load it dynamically + +VALID_COMMANDS = dict() + + +# GLOBALS is likewise empty until global variables get created + +GLOBALS = dict() # the globals themselvs +GLOBAL_LOCK = threading.Lock() # a lock for the globals to prevent simultaneous access + + +# Add a new command. This removes any existing command of the same name from the VALID_COMMANDS +# and returns it as the result + +def Add_command( + a_command: command_base.Command_Basic # the command or header to add + ): + + if a_command.name in VALID_COMMANDS: # or if it was a command + p = VALID_COMMANDS.pop(a_command.name) # pop that too + else: # otherwise + p = None # the return value will be None (we're not replacing anything) + + VALID_COMMANDS[a_command.name] = a_command # add the new command + + return p # return any replaced command + + +# Remove a command. This could be useful in handling subroutines + +def Remove_command( + command_name # the command to remove + ): + + if command_name in VALID_COMMANDS: # check command + p = VALID_COMMANDS.pop(command_name) # remove the command + else: + p = None # nothing to remove + + return p # the thing we removed + + +# display info on all commands and headers + +def Dump_commands(style=DS_NORMAL): + def get_name(c): + if isinstance(c, command_base.Command_Basic): + return c.name + elif isinstance(c, Button): + return c.coords + else: + return "ERROR" + + def get_desc(c): + ret = '' + if isinstance(c, command_base.Command_Basic): + if hasattr(c, 'desc') and not callable(c.desc): + ret = c.desc + if hasattr(c, 'btn') and not callable(c.btn) and c.btn: + if c.btn.desc != "": + ret = c.btn.desc + elif isinstance(c, Button): + ret = c.desc + else: + ret = "ERROR" + + return ret + + def dump_name(c_type, c): + print(f" {c_type} \"{get_name(c)}\"", end="") + desc = get_desc(c) + if desc == "": + print() + else: + print(f" - {desc}") + + def get_doc(c): + ret = [] + if isinstance(c, command_base.Command_Basic): + if hasattr(c, 'doc') and not callable(c.doc): + ret = c.doc + if hasattr(c, 'btn') and not callable(c.btn) and c.btn: + if c.btn.doc != []: + ret = c.btn.doc + elif isinstance(c, Button): + ret = c.doc + else: + ret = ["ERROR"] + + return ret + + def dump_doc(c): + doc = get_doc(c) + if doc != []: + print(" Notes") + for n in doc: + print(f" {n}") + + def dump_ancestory(c): + print(" Ancestory") + print(f" {type(c)}") + a = type(c).__bases__[0] + while a != object: + print(f" {a}") + a = a.__bases__[0] + + def dump_params(c): + if isinstance(c, command_base.Command_Basic): + print(" Parameters") + if c.auto_validate == None: + print(" Parameters undocumented (Auto-validation is not defined)") + elif len(c.auto_validate) == 0: + print(" No parameters") + else: + for v in c.auto_validate: + print(f" {v[AV_DESCRIPTION]} - {v[AV_TYPE][AVT_DESC]}", end="") + + if v[AV_OPTIONAL]: + print(" (opt),", end="") + else: + print(" (reqd),", end="") + + if v[AV_VAR_OK] == AVV_NO: + print(" constant only") + elif v[AV_VAR_OK] == AVV_YES: + print(" variable permitted") + elif v[AV_VAR_OK] == AVV_REQD: + print(" variable required (possible return value)") + else: + print(" UNKNOWN VALUE") + + def dump(c_type, c, style): + dump_name(c_type, c) + dump_doc(c) + if D_DEBUG in style: + dump_ancestory(c) + dump_params(c) + + print() + + import commands_subroutines + + if D_HEADERS in style: + print("HEADERS") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], command_base.Command_Header): + dump("Header", VALID_COMMANDS[cmd], style) + + if D_COMMANDS in style: + print("COMMANDS") + print() + for cmd in VALID_COMMANDS: + if not (isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine) or \ + isinstance(VALID_COMMANDS[cmd], command_base.Command_Header)): + dump("Command", VALID_COMMANDS[cmd], style) + + if D_SUBROUTINES in style: + print("SUBROUTINES") + print() + for cmd in VALID_COMMANDS: + if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): + dump("Subroutine", VALID_COMMANDS[cmd], style) + + if D_BUTTONS in style: + print("BUTTONS") + print() + global buttons + for x in range(9): + for y in range(9): + btn = buttons[x][y] + if btn.script_str != "": + dump("Button", btn, style) + + +# Create a new symbol table. This contains information required for the script to run +# it includes the locations of labels, loop counters, etc. If we implement variables +# this is where we would place them + +def New_symbol_table(): + # returns a new (blank) symbol table + # symbol table is dictionary of objects + symbols = { + SYM_REPEATS: dict(), + SYM_ORIGINAL: dict(), + SYM_LABELS: dict(), + SYM_MOUSE: tuple(), + SYM_GLOBAL: [GLOBAL_LOCK, GLOBALS], # global (to the application) variables (and associated lock) + SYM_LOCAL: dict(), # local (to the script) variables (with no lock) + SYM_STACK: [] } # script stack (for RPN_EVAL) + + return symbols + + +# ################################################## +# ### CLASS Button ### +# ################################################## + +# class that defines a button. +# A button is a class containing all that's essential for a button. +class Button(): + def __init__( + self, + x, # The button column + y, # The button row + script_str, # The Script + root = None, # Who called us + name = '' # name of this button (subroutine) + ): + + self.x = x + self.y = y + self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine + self.script_str = script_str # The script + + self.name = None + self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? + self.desc = "" + self.doc = [] + + self.validated = False # Has the script been validated? + self.symbols = None # The symbol table for the button + self.script_lines = None # the lines of the script + self.thread = None # the thread associated with this button + self._running = False # is the script running? + self.is_async = False # async execution flag + + # The "root" is the button that is scheduled. This allows subroutines to check if the + # initiating button has been killed. + if root == None: # if we are not being called + self.root = self # then we are the root + else: # otherwise + self.root = root # the caller is the root + + + # let us set/change the name of a button + def Set_name(self, name): + update = self.name != None # it is initialisation if the original contents is None + self.name = name # update the name + + self.coords = '' # Start the process of updating the coords + + if self.is_button: # include actual coords if it is actually a button + self.coords += "(" + str(self.x+1) + ',' + str(self.y+1) + ")" # let's just do this the once eh? + if self.name != "": # If it has a name, let's include that too + self.coords = " ".join([self.name, self.coords]) # remember that subroutines don't have coordinates + + if update and self.is_button: # no need to update the window on initialisation + Redraw(self.x, self.y) # and we only need to update this button + + + def running(self, set_to=None): + if type(set_to) == bool and set_to != self._running: + self._running = set_to + Redraw(self.x, self.y) # redraw the canvas when the button run status is changed + + return self._running + + + # Do what is required to parse the script. Parsing does not output any information unless it is an error + def Parse_script(self): + if self.validated: # we don't want to repeat validation over and over + return True + + if self.script_lines == None: # A little setup if the script lines are not created + if isinstance(self.script_str, list): # Subroutines already have this as a list of lines + self.script_lines = self.script_str # Copy the lines + else: # But commands just have the raw stream from a file + self.script_lines = self.script_str.split('\n') # Create the lines + self.script_lines = [i.strip() for i in self.script_lines] # Strip extra blanks + + self.symbols = New_symbol_table() # Create a shiny new symbol table + self.is_async = False # default is NOT async + + err = True + errors = 0 # no errors found + + for pass_no in (VS_PASS_1, VS_PASS_2): # pass 1, collect info & syntax check, + # pass 2 symbol check & assocoated processing + for idx,line in enumerate(self.script_lines): # gen line number and text + if self.Is_ignorable_line(line): + continue # don't process ignorable lines + + cmd_txt = self.Split_cmd_text(line) # get the name of the command + + if cmd_txt in VALID_COMMANDS: # if first element is a command + command = VALID_COMMANDS[cmd_txt]# get the command itself + split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately + + if type(split_line) == tuple: + if err == True: + err = split_line + errors += 1 + else: + res = command.Parse(self, idx, split_line, pass_no); + if res != True: + if err == True: + err = res # note the error + errors += 1 # and 1 more error + else: + msg = " Invalid command '" + cmd_txt + "' on line " + str(idx+1) + "." + if err == True: + err = (msg, line) # note the error + print (msg) + errors += 1 # and 1 more error + + if err != True: + if self.is_button: + print('Pass ' + str(pass_no) + ' complete for button ' + self.coords + '. ' + str(errors) + ' errors detected.') + else: + print('Pass ' + str(pass_no) + ' complete for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') + break # errors prevent next pass + + return err # success or failure + + + # copies parsed info from self to new_btn + def Copy_parsed(self, new_btn, name="SUB"): + new_btn.script_lines = self.script_lines # Copy the lines + new_btn.coords = "(" + name + ")" # set the name + + new_btn.symbols = New_symbol_table() + new_btn.symbols[SYM_REPEATS] = self.symbols[SYM_REPEATS].copy() # copy the repeats + new_btn.symbols[SYM_ORIGINAL] = self.symbols[SYM_ORIGINAL].copy() # and the original values + new_btn.symbols[SYM_LABELS] = self.symbols[SYM_LABELS].copy() # and the position of labels + + new_btn.is_async = self.is_async # default is NOT async + + + # check "self" for death notification + def Check_self_kill(self, killfunc=None): + if not self.thread: + print ("expecting a thread in ", self.coords) + return False + + if self.thread.kill.is_set(): + print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") + #self.thread.kill.clear() + if not self.is_async: + self.running(False) + if killfunc: + killfunc() + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() + return True + else: + return False + + + # Check_kill now checks the root button for death notifications + def Check_kill(self, killfunc=None): + return self.root.Check_self_kill(killfunc) + + + # a sleep method that works with the multiple threads + def Safe_sleep(self, time, endfunc=None): + while time > DELAY_EXIT_CHECK: + sleep(DELAY_EXIT_CHECK) + time -= DELAY_EXIT_CHECK + if self.Check_kill(endfunc): + return False + if time > 0: + sleep(time) + if endfunc: + endfunc() + return True + + + # some lines can be ignored. These include blank lines and comments. It's faster to identify them + # before trying to process them than treat them as an exception afterwards. + + def Is_ignorable_line(self, line): + line = line.strip() # remove leading and trailing spaces + if line != "": + if line[0] == "-": + return True # non-blank lines starting with a hyphen are comments (and can be ignored) + else: + return False # other non-blank lines are significant + else: + return True # blank lines can be igmored + + + def Schedule_script(self): + # @@@ may be worth checking to see if it's a subroutine. Because subroutines shouldn't use this + global to_run + + if self.thread != None: + if self.thread.is_alive(): + # @@@ The following code creates a problem if a script is looking for a second keypress + # @@@ Maybe we need an option to make a script un-interruptable, or alternately require + # @@@ *something* else (maybe ctrl-alt) to be pressed to allow the kill to take place. + print("[scripts] " + self.coords + " Script already running, killing script....") + self.thread.kill.set() + return + + if (self.x, self.y) in [l[1:] for l in to_run]: + print("[scripts] " + self.coords + " Script already scheduled, unscheduling...") + indexes = [i for i, v in enumerate(to_run) if ((v[1] == self.x) and (v[2] == self.y))] + for index in indexes[::-1]: + temp = to_run.pop(index) + return + + if self.is_async: + print("[scripts] " + self.coords + " Starting asynchronous script in background...") + self.thread = threading.Thread(target=Run_script, args=()) + self.thread.kill = threading.Event() + self.thread.start() + elif not self.running(): + print("[scripts] " + self.coords + " No script running, starting script in background...") + self.thread = threading.Thread(target=self.Run_script_and_run_next, args=()) + self.thread.kill = threading.Event() + self.thread.start() + else: + print("[scripts] " + self.coords + " A script is already running, scheduling...") + to_run.append((self.x, self.y)) + + lp_colors.updateXY(self.x, self.y) + + + def Run_next(self): + global to_run + global buttons + + if len(to_run) > 0: + tup = to_run.pop(0) + x = tup[0] + y = tup[1] + + btn = buttons[x][y] + btn.Schedule_script() + + + def Run_script_and_run_next(self): + self.Run_script() + self.Run_next() + + + def Line(self, idx): + if self.script_lines and idx >=0 and idx < len(self.script_lines): + return self.Fix_comment(self.script_lines[idx]) + else: + return "" + + + def Fix_comment(self, line): + # Ensure there's a space after the comment character + if len(line) > 1 and line[0] == "-" and line[1] != " ": + return line[0] + " " + line[1:] + else: + return line + + + def Split_cmd_text(self, line): + # Get the command text + line += ' ' + return line[0:line.find(" ")] + + + def Split_text(self, command, cmd_txt, line): + # Split line differently for "text" type commands + if isinstance(command, command_base.Command_Text_Basic): + # just split the command from the rest of the text + return [cmd_txt, line[len(cmd_txt)+1:]] + else: + def split1(line): # just strip off a single (non-quoted) parameter + param = line.split()[0] # get the parameter + line = line[len(param):].strip() # strip off the parameter + + return param, line # return the parameter and the rest of the line + + # grab a quoted string from the line passed. Handles embedded quotes + def strip_quoted(line): + l2 = line # a copy of the line we can edit + q = l2[0] # the first character is assumed to be a quote + out = '' # nothing to output yet + l2 = l2[1:] # strip the quote from the string + while len(l2) > 0: # while something remains in the line + if l2[0] == q: # if the quote is repeated + if len(l2) == 1 or l2[1] == ' ': # and if it's the last character or followed by a space + l2 = l2[1:].strip() # clean up the rest of the string + return True, out, l2 # return success + elif len(l2) > 1 and l2[1] == q: # if we have 2 quotes in a row + out += q # then this is literally a quote + l2 = l2[2:] # but we need to clean up 2 characters this time + else: + return False, out, line # any other quote-related stuff must be an error + else: # for non-quote characters + out += l2[0] # we just pass them through to the output string + l2 = l2[1:] # and strip them off. + + return False, out, line # if we fall through, that's an error (no closing quote) + + # for all other commands, split on spaces + if isinstance(command, command_base.Command_Basic): + pline = line # something we can alter + avl = command.auto_validate + if avl != None and len(avl) > 0: + cmd, pline = split1(pline) # the command is always a simple split + sline = [cmd] # add it to the return variable + + n = -1 # initialise parameter number pointer to one before the first + while len(pline) > 0: # keep stripping parameters while the line has some content + n += 1 # point to the next parameters + if n < len(avl): # if this parameter has an auto-validation + av = avl[n] # then grab it + else: # otherwise the last parameter must allow for multiple values + av = avl[-1] # so take the last auto-validation + + desc = av[AV_TYPE][AVT_DESC] # get the description of the parameter type (not the description of the parameter!) + + if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Is this one that wants quoted strings? + if pline[0] in ['"', "'", '`']: # if so, does it start with an acceptable quote? + if av[AV_VAR_OK] == AVV_REQD: # it's a problem if a variable is required + return ('Error, quoted string not permitted for param #' + str(n+1), line) # literal not expected + else: + ok, param, pline = strip_quoted(pline) # otherwise we can strip off a quoted string + if ok: # and if that suceeded + sline += ['"'+param] # we'll add it as the parameter value. Note we add a leading " to distinguish it from a variable + else: + return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote + else: # if we want a quoted string, but value doesn't start with a quote + if av[AV_VAR_OK] != AVV_NO: # Are we allowed to pass a variable? + param = pline.split()[0] # then that's OK, just strip off an un-quoted string + pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) + if not variables.valid_var_name(param): # but check it's a valid variable name + return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... + else: + sline += [param] # add it to the list of parameters if it's OK + else: + return ('Error starting quoted string for param#' + str(n+1), line) # This is generally a missing initial quote + + elif desc == PT_LINE[AVT_DESC]: # the rest of the line (regardless of spaces) + sline += [line] # just grab the rest of the line + pline = "" # and leave nothing behind + + else: # in all other cases + param = pline.split(" ")[0] # just strip the first unquoted parameter (@@@ why not use strip1()???) + sline += [param] + pline = pline[len(param):].strip() + + return sline # return a list of command and parameters + + else: + # without autovalidate we just split on spaces + return line.split(" ") + + + # run a script + def Run_script(self): + # @@@ maybe check we're not a subroutine (subroutines should not use this) + lp_colors.updateXY(self.x, self.y) + + if self.Validate_script() != True: + return + + print("[scripts] " + self.coords + " Now running script...") + + self.running(not self.is_async) + + cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters + if cmd_txt in VALID_COMMANDS: + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) + + if len(self.script_lines) > 0: + self.running(True) + + def Main_logic(idx): # the main logic to run a line of a script + if self.Check_kill(): # first check to see if we've been asked to die + return idx + 1 # we just return the next line, @@@ returning -1 is better + + line = self.Line(idx) # get the line of the script + + # Handle completely blank lines + if line == "": + return idx + 1 + + # Get the command text + cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact + + # Now get the command object + if cmd_txt in VALID_COMMANDS: # make sure it's a valid command + command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command + + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters + + if type(split_line) == tuple: # bad news if we get a tuple rather than a list + print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") + else: + # now run the command + return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out + else: + print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") + + return idx + 1 # defaut action is to ask for the next line + + run = True # flag that we're running + idx = 0 # point at the first line + while run: # and while we're still running + idx = Main_logic(idx) # run the current line + if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid + run = False # then we're not going to keep running! + + if not self.is_async: # async commands don't just end + self.running(False) # they have to say they're not running + + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() # queue up a request to update the button colours + + print("[scripts] " + self.coords + " Script ended.") # and print (log?) that the script is complete + + + # run a subroutine. This is a simplified version of running a script because the script takes care of being scheduled and killed + # @@@ this is so close to run_script that it probably should be merged with it at some point -- after I know its working + def Run_subroutine(self): + # @@@ maybe check that we **are** a subroutine first. This is for subroutines ONLY + if self.Validate_script() != True: # validates if not validated + return + + print("[scripts] " + self.coords + " Now running subroutine ...") + + self.running(not self.is_async) # @@@ not sure a async subroutine makes sense + + cmd_txt = "RESET_REPEATS" # before we run, we want to rest loop counters + if cmd_txt in VALID_COMMANDS: + command = VALID_COMMANDS[cmd_txt] + command.Run(self, -1, [cmd_txt]) + + if len(self.script_lines) > 0: + self.running(True) + + def Main_logic(idx): # the main logic to run a line of a script + if self.Check_kill(): # first check on our death notification + return idx + 1 # we just return the next line, @@@ returning -1 is better + + line = self.Line(idx) # get the line of the script + + # Handle completely blank lines + if line == "": + return idx + 1 + + # Get the command text + cmd_txt = self.Split_cmd_text(line) # Just get the command name leaving the line intact + + # Now get the command object + if cmd_txt in VALID_COMMANDS: # make sure it's a valid command + command = VALID_COMMANDS[cmd_txt] # get the command object that will execute the command + + split_line = self.Split_text(command, cmd_txt, line) # get all the parameters as a list, including quoted parameters + + if type(split_line) == tuple: # bad news if we get a tuple rather than a list + print("[scripts] " + self.coords + " Error in: '" + cmd_txt + "' - " + split_line[0] + ", skipping...") + else: + # now run the command + return command.Run(self, idx, split_line) # otherwise we can ask the command to execute itself with the parameters we've parsed out + else: + print("[scripts] " + self.coords + " Invalid command: '" + cmd_txt + "', skipping...") + + return idx + 1 # defaut action is to ask for the next line + + run = True # flag that we're running + idx = 0 # point at the first line + while run: # and while we're still running + idx = Main_logic(idx) # run the current line + if (idx < 0) or (idx >= len(self.script_lines)): # if the next line isn't valid + run = False # then we're not going to keep running! + + if not self.is_async: # async commands don't just end @@@ again, not sure this makes sense for subroutines + self.running(False) # they have to say they're not running + + print("[scripts] " + self.coords + " Subroutine ended.") # and print (log?) that the script is complete + + + # validating a script consists of doing the checks that we do prior to running, but + # we won't run it afterwards. + def Validate_script(self): + if self.validated or self.script_str == "": # If valid or there is no script... + self.validated = True + return True # ...validation succeeds! + + if self.Parse_script(): # If parsing is OK + self.validated = True # Script is valid + + if len(self.script_lines) > 0: # look for async header and set flag + cmd_txt = self.Split_cmd_text(self.script_lines[0]) + self.is_async = cmd_txt in VALID_COMMANDS and \ + isinstance(VALID_COMMANDS[cmd_txt], command_base.Command_Header) and \ + VALID_COMMANDS[cmd_txt].is_async + else: + self.symbols = None # otherwise destroy symbol table + + return self.validated # and tell us the result + + +# define the buttons structure here. Note that subroutines will likely be a different sort of button, so this may change +buttons = [[Button(x, y, "") for y in range(9)] for x in range(9)] +to_run = [] + + +# bind a button (Note that you can pass a validated button as script_str too) +def Bind(x, y, script_str, color): + global to_run + global buttons + + if isinstance(script_str, Button): # if a button was passed + btn = script_str # then we can skipp the button creation + btn.x = x + btn.y = y + btn.Set_name(btn.name) # force recalc of coords + else: + btn = Button(x, y, script_str) + try: + btn.Validate_script() + except: + pass + + buttons[x][y] = btn + + if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + for index in indexes[::-1]: # and for each of them (in reverse order) + temp = to_run.pop(index) # Remove them from the list + return # @@@ Why do we return here? + + schedule_script_bindable = lambda a, b: btn.Schedule_script() # @@@ What is this doing? + + lp_events.bind_func_with_colors(x, y, schedule_script_bindable, color) + files.layout_changed_since_load = True # Mark the layout as changed + Redraw(x, y) + + +# unbind a button +def Unbind(x, y): + global to_run + global buttons + + lp_events.unbind(x, y) # Clear any events associated with the button + + btn = Button(x, y, "") # create the new blank button + + if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... + indexes = [i for i, v in enumerate(to_run) if ((v[1] == x) and (v[2] == y))] #... create a list of locations in the list for this button + for index in indexes[::-1]: # and for each of them (in reverse order) + temp = to_run.pop(index) # Remove them from the list + buttons[x][y] = btn # Clear the button script + #return # WHY do we return here? + + if btn.thread != None: # If the button is actially executing + thread.kill.set() # then kill it + + buttons[x][y] = btn # Clear the button script + + files.layout_changed_since_load = True # Mark the layout as changed + Redraw(x, y) + + +# swap details for two buttons +def Swap(x1, y1, x2, y2): + global text + + color_1 = lp_colors.curr_colors[x1][y1] # Colour for btn #1 + color_2 = lp_colors.curr_colors[x2][y2] # Colour for btn #2 + + btn_1 = buttons[x1][y1] # btn #1 + btn_2 = buttons[x2][y2] # btn #2 + + Unbind(x1, y1) # Unbind #1 + if btn_2.script_str != "": # If there is a script #2... + Bind(x1, y1, btn_2, color_2) # ...bind it to #1 + lp_colors.updateXY(x1, y1) # Update the colours for btn #1 + + Unbind(x2, y2) # Do the reverse for #2 + if btn_1.script_str != "": + Bind(x2, y2, btn_1, color_1) + lp_colors.updateXY(x2, y2) + + files.layout_changed_since_load = True # Flag that the layout has changed + + +# Duplicate a button +def Copy(x1, y1, x2, y2): + global buttons + + color_1 = lp_colors.curr_colors[x1][y1] # Get colour of btn to be copied + + script_1 = buttons[x1, y1].script_str # Get script to be copied + + Unbind(x2, y2) # Unbind the destination + if script_1 != "": # If we're copying a button with a script... + Bind(x2, y2, script_1, color_1) # ...bind the details to the destination + lp_colors.updateXY(x2, y2) # Update the colours + + files.layout_changed_since_load = True # Flag the layout as changed + + +# Delete a button +def Del(x1, y1, x2, y2): + global buttons + + if x1 != x2 or y1 != y2: + return + + Unbind(x2, y2) # Unbind the destination + lp_colors.updateXY(x2, y2) # Update the colours + + files.layout_changed_since_load = True # Flag the layout as changed + + +# move a button +def Move(x1, y1, x2, y2): + global buttons + if (x1, y1) == (x2, y2): + return + + color_1 = lp_colors.curr_colors[x1][y1] # Get source button colour + + btn_1 = buttons[x1][y1] # Get source button script + + Unbind(x1, y1) # Unbind *both* buttons + Unbind(x2, y2) + + if btn_1.script_str != "": # If the source had a script... + Bind(x2, y2, btn_1, color_1) # ...bind it to the destination + lp_colors.updateXY(x2, y2) # Update the destination colours + + files.layout_changed_since_load = True # And flag the layout as changed + + +# determine if a key is bound +def Is_bound(x, y): + global buttons + + if buttons[x][y].script_str == "": # If there is no script... + return False # ...it's not bound + else: + return True # Otherwise it is + + +# kill all threads +def kill_all(): + global buttons + global to_run + + to_run = [] # nothing queued to run + + for x in range(9): # For each column... + for y in range(9): # ...and row + btn = buttons[x][y] + if btn.thread is not None: # If there is a thread... + if btn.thread.isAlive(): # ...and if the thread is alive... + btn.thread.kill.set() # ...kill it + + +# Unbind all keys. +def Unbind_all(): + lp_events.unbind_all() # Unbind all events + text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank + + kill_all() # stop everything running + + files.curr_layout = None # There is no current layout + files.layout_changed_since_load = False # So mark it as unchanged + + +# Unload all subroutines. +def Unload_all(): + kill_all() # stop everything running + + subs = [] # list of subroutines to remove + for cmd in VALID_COMMANDS: # for all the commands that exist + if cmd.startswith(SUBROUTINE_PREFIX):# if this command is a subroutine + subs += [cmd] # add the command to the list + + for cmd in subs: # for each subroutine we've found + Remove_command(cmd) # remove it + + files.layout_changed_since_load = True # mark layout as changed + + diff --git a/window.py b/window.py index 2efb549..0cd0e74 100644 --- a/window.py +++ b/window.py @@ -394,6 +394,8 @@ def click(self, event): elif self.button_mode == LM_SWAP: self.button_mode = LM_COPY elif self.button_mode == LM_COPY: + self.button_mode = LM_DEL + elif self.button_mode == LM_DEL: self.button_mode = LM_RUN else: self.button_mode = LM_EDIT @@ -416,6 +418,7 @@ def click(self, event): move_func = partial(scripts.Move, self.last_clicked[0], self.last_clicked[1], column, row) swap_func = partial(scripts.Swap, self.last_clicked[0], self.last_clicked[1], column, row) copy_func = partial(scripts.Copy, self.last_clicked[0], self.last_clicked[1], column, row) + del_func = partial(scripts.Del, self.last_clicked[0], self.last_clicked[1], column, row) if self.button_mode == LM_MOVE: if scripts.Is_bound(column, row) and ((self.last_clicked) != (column, row)): @@ -427,6 +430,11 @@ def click(self, event): self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to copy a button to an already\nbound button. What would you like to do?", [["Overwrite", copy_func], ["Swap", swap_func], ["Cancel", None]]) else: copy_func() + elif self.button_mode == LM_DEL: + if ((self.last_clicked) != (column, row)): + self.popup_choice(self, "Please confirm to delete", self.warning_image, "You must click twice on the same button to delete it.", [["OK", None]]) + elif scripts.Is_bound(column, row): + self.popup_choice(self, "Last chance!", self.warning_image, "Do you really want to delete this button.", [["No", None], ["Yes", del_func()]]) elif self.button_mode == LM_SWAP: swap_func() self.last_clicked = None @@ -564,6 +572,12 @@ def fmt(x, y, lines=3, cols=5): if lp_connected: app.stat["text"] = f"Running as {lpcon().get_display_name(lp_object)}" app.stat["bg"] = STAT_RUN_COLOR + elif self.button_mode == LM_DEL: + self.c.itemconfig(self.grid_rects[8][0][0], fill="yellow") + self.c.itemconfig(self.grid_rects[8][0][1], fill="red", text=self.button_mode.capitalize()) + if lp_connected: + app.stat["text"] = f"Running as {lpcon().get_display_name(lp_object)}" + app.stat["bg"] = STAT_ACTIVE_COLOR else: self.c.itemconfig(self.grid_rects[8][0][0], fill=self.c["background"]) self.c.itemconfig(self.grid_rects[8][0][1], fill="black", text=self.button_mode.capitalize()) From 080b78c967791bafd8214a8a5deddf1630250841 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 16 Mar 2021 20:35:01 +0800 Subject: [PATCH 64/83] Don't you hate it when you forget to save. Just a small amount of internal documentation... --- commands_scrape.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands_scrape.py b/commands_scrape.py index d238bfe..c6f2117 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -157,6 +157,10 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Scrape_Get_Clipboard()) # register the command +# ################################################## +# ### CLASS SCRAPE_OCR ### +# ################################################## + # class that defines the S_OCR command -- OCR on an image class Scrape_OCR(Command_Scrape): def __init__( From 1a5fc3675e767c211554dad8909886def7581650 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 17 Mar 2021 19:42:51 +0800 Subject: [PATCH 65/83] Another day another check in. Today was all about web automation, the implementation of "object" variable types, and correcting a number of bugs and usability issues. * commands_browser_automation.py Implemented (but not tested) all the browsers supported by selenium in BA_START * commands_browser_automation.py Added BA_GET_URL to get the current url from the browser * commands_browser_automation.py Added BA_GET_ELEMENT to search for and return elements of a web page * commands_browser_automation.py Added BA_GET_TABLE_SIZE to get the size of a table * commands_browser_automation.py Added BA_GET_TABLE_CELL to get the contents of a table cell (element) * commands_browser_automation.py Added BA_GET_ELEMENT_TEXT to return the text of an element * commands_browser_automation.py Added BA_CLICK to click an element * commands_browser_automation.py Added BA_SEND_KEYS to send keys to an element * commands_control.py Added code to try to make differing types in IF statements Comparable * commands_control.py Changed parameters of IF commands from PT_FLOAT to PT_ANY * commands_rpncalc.py added substr command to RPN_EVAL * commands_subroutines.py Document default modifiers and implement/document the new Object modifier * commands_win32.py fixed a bug and improved documentation in W_PASTE * dialog.py Modified handling of dialogs to cause the application to become topmost when a dialog is open * scripts.py Prevent multiple indications of a thread being killed by stopping the reporting after it has been reported once * scripts.py ensure PT_ANY doesn't split quoted strings. --- command_base.py | 1 + commands_browser_automation.py | 463 ++++++++++++++++++++++++++++++++- commands_control.py | 85 +++++- commands_rpncalc.py | 16 +- commands_subroutines.py | 14 +- commands_win32.py | 6 +- dialog.py | 29 ++- scripts.py | 24 +- variables.py | 2 +- 9 files changed, 590 insertions(+), 50 deletions(-) diff --git a/command_base.py b/command_base.py index dd548ed..5f83d91 100644 --- a/command_base.py +++ b/command_base.py @@ -562,6 +562,7 @@ def Get_param(self, btn, n, other=None): param = None else: param = btn.symbols[SYM_PARAMS][n] + if param == None: ret = other else: diff --git a/commands_browser_automation.py b/commands_browser_automation.py index de030ad..57041e7 100644 --- a/commands_browser_automation.py +++ b/commands_browser_automation.py @@ -1,10 +1,25 @@ # This module is VERY specific to Win32 import command_base, scripts, traceback from selenium import webdriver +from selenium.webdriver.common.by import By from constants import * LIB = "cmds_baut" # name of this library (for logging) + +# constants for BA_START +BAS_CHROME = "CHROME" +BAS_CHROMIUM = "CHROMIUM" +BAS_EDGE = "EDGE" +BAS_FIREFOX = "FIREFOX" +BAS_IE = "IE" +BAS_OPERA = "OPERA" +BAS_SAFARI = "SAFARI" +BAS_WEBKITGTK = "WEBKITGTK" +BAS_REMOTE = "REMOTE" + +BASG_ALL = [BAS_CHROME, BAS_CHROMIUM, BAS_EDGE, BAS_FIREFOX, BAS_IE, BAS_OPERA, BAS_SAFARI, BAS_WEBKITGTK, BAS_REMOTE] + # ################################################## # ### CLASS BAUTO_START ### # ################################################## @@ -28,20 +43,66 @@ def __init__( ) ) self.doc = ["Starts a browser using selinium for automated control. The return will", - "be an object if the call suceeds, otherwise it will return 0"] + "be an object if the call suceeds, otherwise it will return -1.", + "", + "NOTE 1: The first parameter should be used to select what browser you", + "want to load. This is implemented, but has only been tested with Chrome.", + "The values that can be used are:", + "", + " {BAS_CHROME}", + " {BAS_CHROMIUM}", + " {BAS_EDGE}", + " {BAS_FIREFOX}", + " {BAS_IE}", + " {BAS_OPERA}", + " {BAS_SAFARI}", + " {BAS_WEBKITGTK}", + " {BAS_REMOTE}", + "", + "NOTE 2: This blocks while the browser loads, so it should use a", + "similar technique to the dialog boxes to pass this processing off", + "to another thread."] def Process(self, btn, idx, split_line): br = self.Get_param(btn, 1) try: - auto = webdriver.Chrome() + if br == BAS_CHROME: + auto = webdriver.Chrome() + elif br == BAS_CHROMIUM: + auto = webdriver.Chromium() + elif br == BAS_EDGE: + auto = webdriver.Edge() + elif br == BAS_FIREFOX: + auto = webdriver.Firefox() + elif br == BAS_IE: + auto = webdriver.Ie() + elif br == BAS_OPERA: + auto = webdriver.Opera() + elif br == BAS_SAFARI: + auto = webdriver.Safari() + elif br == BAS_WEBKITGTK: + auto = webdriver.Webkitgtk() + elif br == BAS_REMOTE: + auto = webdriver.Remote() except: traceback.print_exc() - auto = 0 - + auto = -1 + self.Set_param(btn, 2, auto) # pass the object back + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if not split_line[1] in BASG_ALL: # invalid subcommand + c_ok = ', '.join(BASG_ALL[:-1]) + ', or ' + BASG_ALL[-1] + s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret + + scripts.Add_command(Bauto_Start()) # register the command @@ -55,7 +116,7 @@ def __init__( self, ): - super().__init__("BA_NAVIGATE, Starts a browser under automated control", + super().__init__("BA_NAVIGATE, Navigate controlled browser to a new URL", LIB, ( # Desc Opt Var type p1_val p2_val @@ -67,6 +128,12 @@ def __init__( (2, " Navigate browser to {2}"), ) ) + self.doc = ["Navigates an existing browser to a new URL.", + "", + "NOTE 1: This blocks while the browser loads the page, so it should use", + "a similar technique to the dialog boxes to pass this processing off to", + "another thread."] + def Process(self, btn, idx, split_line): auto = self.Get_param(btn, 1) @@ -75,7 +142,6 @@ def Process(self, btn, idx, split_line): auto.get(url) except: traceback.print_exc() - auto = 0 scripts.Add_command(Bauto_Navigate()) # register the command @@ -102,6 +168,11 @@ def __init__( (1, " Stop browser {1}"), ) ) + self.doc = ["Closes an existing browser.", + "", + "NOTE 1: You should probably clear the variable that held the browser", + "object after you call this."] + def Process(self, btn, idx, split_line): auto = self.Get_param(btn, 1) @@ -109,9 +180,387 @@ def Process(self, btn, idx, split_line): auto.quit() except: traceback.print_exc() - auto = 0 scripts.Add_command(Bauto_Stop()) # register the command +# ################################################## +# ### CLASS BAUTO_GET_URL ### +# ################################################## + +# class that defines the BA_GET_URL command to find out what page the operator is on +class Bauto_Get_Url(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_GET_URL, Returns the URL of the current page", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Auto", False, AVV_REQD, PT_OBJ, None, None), + ("URL", False, AVV_REQD, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Return current url on {1} into {2}"), + ) ) + + self.doc = ["Returns the URL the existing browser is displaying.", + "", + "NOTE 1: This can fail (returning a blank string) if the browser is", + "still loading the page."] + + + def Process(self, btn, idx, split_line): + auto = self.Get_param(btn, 1) + url = '' + try: + url = auto.current_url; + except: + traceback.print_exc() + + self.Set_param(btn, 2, url) # pass the url back + + +scripts.Add_command(Bauto_Get_Url()) # register the command + + +# constants for BA_GET_ELEMENT +BAG_XPATH = "XPATH" +BAG_NAME = "NAME" +BAG_TAG_NAME = "TAG_NAME" +BAG_ID = "ID" +BAG_LINK_TEXT = "LINK_TEXT" +BAG_CLASS_NAME = "CLASS_NAME" + +BAGG_ALL = [BAG_XPATH, BAG_NAME, BAG_TAG_NAME, BAG_ID, BAG_LINK_TEXT, BAG_CLASS_NAME] + +# ################################################## +# ### CLASS BAUTO_GET_ELEMENT ### +# ################################################## + +# class that defines the BA_GET_ELEMENT command to get an element from a loaded page +class Bauto_Get_Element(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_GET_ELEMENT, Returns an element along a particular xpath", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Auto", False, AVV_REQD, PT_OBJ, None, None), + ("Method", False, AVV_NO, PT_WORD, None, None), + ("Search", False, AVV_YES, PT_STR, None, None), + ("Element", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (4, " Return element {3} from {1} into {4} using method {2}"), + ) ) + + self.doc = ["Returns an element at the location. Often this will be used to extract" + "tables for further processing.", + "" + "The first parameter `Auto` is weither an browser object or an element", + "returned from a successful search.", + "", + "The `Method` can be:", + " XPATH - finds element by xpath", + " NAME - finds element by name", + " TAG_NAME - finds element by tag name", + " ID - finds element by its id", + " LINK_TEXT - finds element by its link text", + " CLASS_NAME - finds element by class name", + "", + "See selenium documentation for information about xpaths.", + "", + "The third parameter `Search` is the search string. Thhe format of this", + "depends on the search method.", + "" + "The final parameter `Element` is the element returned from the search. If", + "The search fails, -1 will be returned."] + + + def Process(self, btn, idx, split_line): + auto = self.Get_param(btn, 1) + method = self.Get_param(btn, 2) + search = self.Get_param(btn, 3) + + element = -1 + try: + element = auto.find_element(method.lower().replace("_", " "), search) + except: + traceback.print_exc() + + self.Set_param(btn, 4, element) # pass the element back + + + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if not split_line[1] in BAGG_ALL: # invalid subcommand + c_ok = ', '.join(BAGG_ALL[:-1]) + ', or ' + BAGG_ALL[-1] + s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret + + +scripts.Add_command(Bauto_Get_Element()) # register the command + + +# ################################################## +# ### CLASS BAUTO_GET_TABLE_SIZE ### +# ################################################## + +# class that defines the BA_GET_TABLE_SIZE command to get the dimensions of a table +class Bauto_Get_Table_Size(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_GET_TABLE_SIZE, Returns the dimensions of a table", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Table", False, AVV_REQD, PT_OBJ, None, None), + ("Rows", False, AVV_REQD, PT_INT, None, None), + ("Cols", False, AVV_REQD, PT_INT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " Return dimensions of table {1} into ({2}, {3})"), + ) ) + + self.doc = ["Returns the number of rows and columns for a table. This table should" + "have previously been obtained by fetching a table element from a loaded" + "page.", + "", + "The first parameter `Table` is an element returned from a search. it is", + "unlikely that any object other than a table will produce sensible results.", + "", + "The result `Rows` will be the number of rows in the table. -1, will be", + "returned in case of error.", + "", + "the result `Cols` will be the number of columns in the 0th row of the table.", + "Note that because HTML tables can have a variable number of columns, it", + "cannot be assumed that all rows will have this number of columns. -1 will", + "be returned in case of error."] + + + def Process(self, btn, idx, split_line): + table = self.Get_param(btn, 1) + + rows = -1 + cols = -1 + try: + r_elements = table.find_elements(By.TAG_NAME, "tr") + rows = len(r_elements) + if rows > 0: + r_element = r_elements[0] + cols = len(r_element.find_elements(By.TAG_NAME, "td")) + len(r_element.find_elements(By.TAG_NAME, "th")) + except: + traceback.print_exc() + + self.Set_param(btn, 2, rows) # pass the dimensions back + self.Set_param(btn, 3, cols) + + +scripts.Add_command(Bauto_Get_Table_Size()) # register the command + + +# ################################################## +# ### CLASS BAUTO_GET_TABLE_CELL ### +# ################################################## + +# class that defines the BA_GET_TABLE_CELL command to get data from a table cell +class Bauto_Get_Table_Cell(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_GET_TABLE_CELL, Returns the table cell element", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Table", False, AVV_REQD, PT_OBJ, None, None), + ("Row", False, AVV_YES, PT_INT, None, None), + ("Col", False, AVV_YES, PT_INT, None, None), + ("Cell", False, AVV_REQD, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (4, " Return cell ({2}, {3}) from {1} into {4}"), + ) ) + + self.doc = ["The first parameter `Table` is an element returned from a search. it is", + "unlikely that any object other than a table will produce sensible results.", + "", + "The `Row` and `Col` parameters specify the 0-based offset in " + "have previously been obtained by fetching a table element from a loaded" + "page.", + "", + "The parameter `Cell` will contain the cell from the table. -1 will be", + "returned in case of error."] + + + def Process(self, btn, idx, split_line): + table = self.Get_param(btn, 1) + row = self.Get_param(btn, 2) + col = self.Get_param(btn, 3) + + cell = -1 + try: + r_elements = table.find_elements(By.TAG_NAME, "tr") + if len(r_elements) > row: + r_element = r_elements[row] + + c_elements = r_element.find_elements(By.TAG_NAME, "td") + if col >= len(c_elements): + col = col - len(c_elements) + c_elements = r_element.find_elements(By.TAG_NAME, "th") + + if len(c_elements) > col: + c_element = c_elements[col] + cell = c_element + + except: + traceback.print_exc() + + self.Set_param(btn, 4, cell) # pass the contents back + + +scripts.Add_command(Bauto_Get_Table_Cell()) # register the command + + +# ################################################## +# ### CLASS BAUTO_GET_ELEMENT_TEXT ### +# ################################################## + +# class that defines the BA_GET_ELEMENT_TEXT command to get text from an element +class Bauto_Get_Element_Text(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_GET_ELEMENT_TEXT, Returns the text content of an element", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Element", False, AVV_REQD, PT_OBJ, None, None), + ("Text", False, AVV_REQD, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Return text of element {1} int {2}"), + ) ) + + self.doc = ["The first parameter `Element` is an element returned from a search.", + "", + "The `Text` parameters will be populated the text of the element. -1" + "will be returned in case of error."] + + + def Process(self, btn, idx, split_line): + element = self.Get_param(btn, 1) + + text = -1 + try: + text = element.text + + except: + traceback.print_exc() + + self.Set_param(btn, 2, text) # pass the contents back + + +scripts.Add_command(Bauto_Get_Element_Text()) # register the command + + +# ################################################## +# ### CLASS BAUTO_CLICK_ELEMENT ### +# ################################################## + +# class that defines the BA_CLICK command to click an element +class Bauto_Click_Element(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_CLICK, Clicks on an element", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Element", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Click on {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + element = self.Get_param(btn, 1) + + try: + element.click() + + except: + traceback.print_exc() + + self.doc = ["The first parameter `Element` is an element returned from a search.", + "", + "This method will click on that element.", + "", + "There is no return value."] + + +scripts.Add_command(Bauto_Click_Element()) # register the command + + +# ################################################## +# ### CLASS BAUTO_SEND_KEYS ### +# ################################################## + +# class that defines the BA_SEND_TEXT command to send keys to an element +class Bauto_Send_Keys(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_SEND_TEXT, Sends keys to an element", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Element", False, AVV_REQD, PT_OBJ, None, None), + ("Text", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Click on {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + element = self.Get_param(btn, 1) + text = self.Get_param(btn, 2) + + try: + element.send_keys(text) + + except: + traceback.print_exc() + + self.doc = ["The first parameter `Element` is an element returned from a search.", + "", + "This method will send text to that elementt.", + "", + "There is no return value."] + + +scripts.Add_command(Bauto_Send_Keys()) # register the command + + diff --git a/commands_control.py b/commands_control.py index bc73857..4f81985 100644 --- a/commands_control.py +++ b/commands_control.py @@ -174,11 +174,62 @@ def Init_n_minus_1(self, btn, idx, split_line): # set repeats btn.symbols[SYM_ORIGINAL][idx] = int(split_line[2])-1 self.Reset(btn, idx) + # do our best to make a and b comparable + def Comparable(self, a, b): + + def either_is(a, b, c_type): + return type(a) == c_type or type(b) == c_type + + if isinstance(a, type(b)) or isinstance(b, type(a)): # probably comparable + return a, b + + if either_is(a, b, str): + if either_is(a, b, int): + try: + return int(a), int(b) + except: + None + + try: + return float(a), float(b) + except: + None + + try: + return str(a), str(b) + except: + None + elif either_is(a, b, float): + try: + return float(a), float(b) + except: + None + + try: + return str(a), str(b) + except: + None + elif either_is(a, b, float): + if either_is(a, b, int): + pass + else: + try: + return float(a), float(b) + except: + None + + try: + return str(a), str(b) + except: + None + return a, b def a_eq_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a == b: return True @@ -191,6 +242,8 @@ def a_ne_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a != b: return True @@ -203,6 +256,8 @@ def a_gt_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a > b: return True @@ -215,6 +270,8 @@ def a_lt_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a < b: return True @@ -227,6 +284,8 @@ def a_ge_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a >= b: return True @@ -239,6 +298,8 @@ def a_le_b(self, btn): a = self.Get_param(btn, 2) b = self.Get_param(btn, 3) + a, b = self.Comparable(a, b) # try our best to make a and b comparable + try: if a <= b: return True @@ -289,8 +350,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) @@ -320,8 +381,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) @@ -351,8 +412,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) @@ -382,8 +443,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), #@@@ this splits strings!!!!! (it shouldn't) + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) @@ -413,8 +474,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) @@ -444,8 +505,8 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Label", False, AVV_NO, PT_LABEL, None, None), - ("A", False, AVV_YES,PT_FLOAT, None, None), #@@@ I want to define a PT_ANY that will accept any variable type - ("B", False, AVV_YES,PT_FLOAT, None, None), + ("A", False, AVV_YES,PT_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), ), ( # num params, format string (trailing comma is important) diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 2a31c86..7880aab 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -178,6 +178,7 @@ def Register_operators(self): self.operators["?G"] = (self.is_global_def, 1) # is global var defined self.operators["!?G"] = (self.is_global_not_def, 1) # is global var not defined self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc) + self.operators["SUBSTR"] = (self.substr, 0) # x gets str(z)[x:y] def add(self, @@ -663,6 +664,15 @@ def abort_script(self, symbols, cmd, cmds): return False + def substr(self, symbols, cmd, cmds): + # does a substring + x = variables.pop(symbols) + y = variables.pop(symbols) + z = variables.pop(symbols) + + variables.push(symbols, str(z)[x:y]) + + scripts.Add_command(Rpn_Eval()) # register the command @@ -721,7 +731,7 @@ def Process(self, btn, idx, split_line): # ### CLASS RPN_CLEAR ### # ################################################## -# class that defines the RPN_CLEAR command -- clears variables @@@ needs validation!!! +# class that defines the RPN_CLEAR command -- clears variables class Rpn_Clear(command_base.Command_Basic): def __init__( self, @@ -739,7 +749,7 @@ def __init__( (1, " Clear {1}"), (2, " Clear {1}: {2}"), ) ) - + self.doc= ["If parameter 1 is:", f" '{RC_GLOBALS}' All the global variables are cleared", f" '{RC_LOCALS}' All the local variables are cleared", @@ -774,7 +784,7 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error - if not split_line[1] in RCG_ALL: # invalid subcommand + if not split_line[1] in RCG_ALL: # invalid subcommand c_ok = ', '.join(RCG_ALL[:-1]) + ', or ' + RCG_ALL[-1] s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." return (s_err, btn.Line(idx)) diff --git a/commands_subroutines.py b/commands_subroutines.py index 7fb8f42..30695a5 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -118,13 +118,16 @@ def __init__( "is by following the parameter name with a `+` followed by the modifiers.", "", "The modifiers are:", - " `%` or `I` - defines the variable as an integer number", + " `%` or `I` - defines the variable as an integer number (default)", " `#` or `F` - defines the variable as a float or real number", " `$` or `S` - defines the variable as a string", " `!` or `B` - defines the variable as a boolean (not fully implemented)", + " `&` or `J` - defines the variable as an object", " `K` - defines the variable as a key (not fully implemented)", " `-` or `O` - defines the variable as optional", + " `M` - defines the variable as mandatory (default)", " `@` or `R` - defines the variable as call by reference (more later)", + " `V` - defines the variable as call by value (default)", "", "An example using these modifiers is as follows:", "", @@ -218,9 +221,9 @@ def Parse_Sub(self): raise -MOD_TRANS = str.maketrans('-%#$!@', 'OIFSBR') # standardise the modifiers -MOD_CONSOLIDATE = str.maketrans('MOIFSBRV', 'MMIIIIVV') # consoidate the modifiers -VALID_CONSOLIDATED = {"M", "I", "V"} # set of all valid consolidate modifiers +MOD_TRANS = str.maketrans('-%#$!&@', 'OIFSBJR') # standardise the modifiers +MOD_CONSOLIDATE = str.maketrans('MOIFSBJRV', 'MMIIIIIVV') # consoidate the modifiers +VALID_CONSOLIDATED = {"M", "I", "V"} # set of all valid consolidate modifiers def Get_Name_And_Params(lines, sub_n, fname): @@ -321,6 +324,9 @@ def Get_Name_And_Params(lines, sub_n, fname): return name, f'Error - Key cannot be passed by reference on line {lin_num+1} of subroutine "{name}" in "{fname}"', lin_num elif mods.find('B') >= 0: prm[3] = PT_BOOL # parameter is a boolean + elif mods.find('J') >= 0: + prm[3] = PT_OBJ # parameter is an object + prm[2] = AVV_REQD # must be a variable params += (tuple(prm),) # add a new parameter diff --git a/commands_win32.py b/commands_win32.py index b82cf3c..b670d78 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -526,7 +526,7 @@ def Process(self, btn, idx, split_line): # ### CLASS W_PASTE ### # ################################################## -# class that defines the W_Paste command - copies and places (optionally) text into variable +# class that defines the W_Paste command - paste from a variable or clipboard class Win32_Paste(Command_Win32): def __init__( self, @@ -551,10 +551,12 @@ def Process(self, btn, idx, split_line): c = self.Get_param(btn, 1) # get the value try: - win32clipboard.OpenClipboard(hwnd) + win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() # clear the clipboard first (because that makes it work) win32clipboard.SetClipboardText(str(c)) # and put the string in the clipboard finally: + import traceback + traceback.print_exc() win32clipboard.CloseClipboard() # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste diff --git a/dialog.py b/dialog.py index 1a8aa3c..5352189 100644 --- a/dialog.py +++ b/dialog.py @@ -14,20 +14,19 @@ class Dialog(tk.Toplevel): def __init__(self, parent, title=None, message=None, ok=False, cancel=False): - tk.Toplevel.__init__(self, parent) - + self.transient(parent) - + if title: self.title(title) - + self.parent = parent self.result = None - + body = tk.Frame(self) body.pack(ipadx=2, ipady=2) - + if message: message = message.replace("\\n", "\n") # allow the manual splitting of lines. msg = tk.Label(body, text=message, wraplength=350, justify="center") @@ -35,14 +34,14 @@ def __init__(self, parent, title=None, message=None, ok=False, cancel=False): foot = tk.Frame(body) foot.pack(padx=4, pady=4) - + if ok: b1 = tk.Button(foot, text="OK", width=8, command=self.btn_OK) b1.pack(side='left', padx=5) if cancel: b2 = tk.Button(foot, text="Cancel", width=8, command=self.btn_Cancel) b2.pack(side='left', padx=5) - #b1.focus_set() + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, parent.winfo_rooty()+50)) @@ -207,12 +206,13 @@ def IdleProcess(parent): DIALOG_OBJECT.state() # Raises an exception if the window is closed except: DIALOG_ACTIVE = False - CloseDialog() # act to close the dialog + CloseDialog(parent) # act to close the dialog + if DIALOG_ACTIVE: # if there is a dialog if DIALOG_BUTTON.root.thread.kill.is_set(): # has the controlling button been killed? - CloseDialog() # act to close the dialog + CloseDialog(parent) # act to close the dialog else: # there is no dialog open @@ -225,7 +225,7 @@ def IdleProcess(parent): # Close any open dialog -def CloseDialog(): +def CloseDialog(parent): global DIALOG_ACTIVE global DIALOG_BUTTON global DIALOG_REQUEST @@ -236,7 +236,9 @@ def CloseDialog(): if DIALOG_OBJECT: DIALOG_OBJECT.destroy() DIALOG_REQUEST[R_CALLBACK]((True, DIALOG_RETURN)) # and return success, and the return info from the dialog - + + parent.master.attributes("-topmost", False) # No need to be topmost any more + DIALOG_OBJECT = None DIALOG_ACTIVE = False # no dialog open DIALOG_BUTTON = None # no button @@ -263,4 +265,7 @@ def OpenDialog(parent, request): DIALOG_OBJECT = Dialog(parent, request[R_PARAM][0], request[R_PARAM][1], ok=True, cancel=True) elif request[R_TYPE] == DLG_ERROR: DIALOG_OBJECT = Dialog(parent, request[R_PARAM][0], request[R_PARAM][1], cancel=True) + + parent.master.attributes("-topmost", True) # bring parent right to the top so we can see the dialog and so it's never obscured + diff --git a/scripts.py b/scripts.py index 129f784..bc492e7 100644 --- a/scripts.py +++ b/scripts.py @@ -343,13 +343,14 @@ def Check_self_kill(self, killfunc=None): return False if self.thread.kill.is_set(): - print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") - #self.thread.kill.clear() - if not self.is_async: - self.running(False) - if killfunc: - killfunc() - threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() + if self.running(): # now we don't clear this, we need to ignore multiple reports + print("[scripts] " + self.coords + " Recieved exit flag, script exiting...") + #self.thread.kill.clear() + if not self.is_async: + self.running(False) + if killfunc: + killfunc() + threading.Timer(EXIT_UPDATE_DELAY, lp_colors.updateXY, (self.x, self.y)).start() return True else: return False @@ -516,7 +517,8 @@ def strip_quoted(line): desc = av[AV_TYPE][AVT_DESC] # get the description of the parameter type (not the description of the parameter!) - if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]): # Is this one that wants quoted strings? + if (desc == PT_STR[AVT_DESC]) or (desc == PT_STRS[AVT_DESC]) or \ + (desc == PT_ANY[AVT_DESC]): # Is this one that wants quoted strings? if pline[0] in ['"', "'", '`']: # if so, does it start with an acceptable quote? if av[AV_VAR_OK] == AVV_REQD: # it's a problem if a variable is required return ('Error, quoted string not permitted for param #' + str(n+1), line) # literal not expected @@ -531,7 +533,11 @@ def strip_quoted(line): param = pline.split()[0] # then that's OK, just strip off an un-quoted string pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) if not variables.valid_var_name(param): # but check it's a valid variable name - return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... + if desc == PT_ANY[AVT_DESC]: # PT_ANY will accept non-variables as strings + sline += [param] # we'll add it as the parameter value. Note we don't add a leading " + # because we can try to interpret it as numeric later on + else: + return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... else: sline += [param] # add it to the list of parameters if it's OK else: diff --git a/variables.py b/variables.py index 51803cc..fcace08 100644 --- a/variables.py +++ b/variables.py @@ -60,7 +60,7 @@ def get(name, l_vbls, g_vbls, default=param_convs._None): return l_vbls[name] if is_defined(name, g_vbls): # then the global one return g_vbls[name] - return default(None) # return default value (rather than always 0) + return default(None) # return default value (rather than always 0) # raise Exception("Variable not found") From 2ba338bddec770a9fc13eaade0b18c42625a6ddb Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 18 Mar 2021 19:33:05 +0800 Subject: [PATCH 66/83] More documentation and bug fixes than new functionality this time. * commands_file.py added to introduce new filesystem commands to find home directory and to delete files * commands_browser_automation.py fix validation of wrong parameter, also placing comments in the right place * commands_browser_automation.py added new command BA_SHOW_ELEMENTS to dump the contents of an element * commands_external.py slight documentation and allow variables in CODE_NOWAIT * commands_rpncalc.py fix parameter ordering for substr, and ensure a return value is set. * commands_win32.py allow variables, and note problems with validation * commands_win32.py fix bug in W_COPY * scripts.py fix another bug in copy * window.py add code to display button descriptions when the mouse is over buttons --- command_list.py | 3 +- commands_browser_automation.py | 73 +- commands_external.py | 4 +- commands_file.py | 67 ++ commands_mouse.py | 1 - commands_rpncalc.py | 5 +- commands_win32.py | 8 +- scripts.py | 2 +- user_layouts/Testing.lpl | 1392 ++++++++++++++++---------------- window.py | 48 +- 10 files changed, 876 insertions(+), 727 deletions(-) create mode 100644 commands_file.py diff --git a/command_list.py b/command_list.py index 65ceb4d..2c33fda 100644 --- a/command_list.py +++ b/command_list.py @@ -15,7 +15,8 @@ commands_external, \ commands_subroutines, \ commands_dialog, \ - commands_browser_automation + commands_browser_automation, \ + commands_file # @@@ a test command set for testing things! Will be removed for production try: diff --git a/commands_browser_automation.py b/commands_browser_automation.py index 57041e7..e80a25f 100644 --- a/commands_browser_automation.py +++ b/commands_browser_automation.py @@ -302,9 +302,9 @@ def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error - if not split_line[1] in BAGG_ALL: # invalid subcommand + if not split_line[2] in BAGG_ALL: # invalid subcommand c_ok = ', '.join(BAGG_ALL[:-1]) + ', or ' + BAGG_ALL[-1] - s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + s_err = f"Invalid subcommand {split_line[2]} when expecting {c_ok}." return (s_err, btn.Line(idx)) return ret @@ -501,6 +501,12 @@ def __init__( (1, " Click on {1}"), ) ) + self.doc = ["The first parameter `Element` is an element returned from a search.", + "", + "This method will click on that element.", + "", + "There is no return value."] + def Process(self, btn, idx, split_line): element = self.Get_param(btn, 1) @@ -511,12 +517,6 @@ def Process(self, btn, idx, split_line): except: traceback.print_exc() - self.doc = ["The first parameter `Element` is an element returned from a search.", - "", - "This method will click on that element.", - "", - "There is no return value."] - scripts.Add_command(Bauto_Click_Element()) # register the command @@ -543,7 +543,13 @@ def __init__( (1, " Click on {1}"), ) ) + self.doc = ["The first parameter `Element` is an element returned from a search.", + "", + "This method will send text to that elementt.", + "", + "There is no return value."] + def Process(self, btn, idx, split_line): element = self.Get_param(btn, 1) text = self.Get_param(btn, 2) @@ -554,13 +560,52 @@ def Process(self, btn, idx, split_line): except: traceback.print_exc() - self.doc = ["The first parameter `Element` is an element returned from a search.", - "", - "This method will send text to that elementt.", - "", - "There is no return value."] - scripts.Add_command(Bauto_Send_Keys()) # register the command +# ################################################## +# ### CLASS BAUTO_SHOW_ELEMENTS ### +# ################################################## + +# class that defines the BA_SHOW_ELEMENTS command to dump elements inside the passed element +class Bauto_ShowElements(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("BA_SHOW_ELEMENTS, Print the elements within the passed element", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Element", False, AVV_REQD, PT_OBJ, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Show elements in {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + element = self.Get_param(btn, 1) + text = self.Get_param(btn, 2) + + try: + e = element + print(e) + try: + print(f"{0}:\n properties: {e.get_property('attributes')}\n text: {e.get_attribute('text')}\n content: {e.get_attribute('textContent')}") + except: + pass + es = element.find_elements(BAG_XPATH.lower().replace("_", " "), ".//*") + for i, e in enumerate(es): + print(f"{i+1}:\n properties: {e.get_property('attributes')}\n text: {e.get_attribute('text')}\n content: {e.get_attribute('textContent')}") + print(len(es)) + + except: + traceback.print_exc() + + +scripts.Add_command(Bauto_ShowElements()) # register the command + + diff --git a/commands_external.py b/commands_external.py index ea0c34c..8a629c0 100644 --- a/commands_external.py +++ b/commands_external.py @@ -149,7 +149,7 @@ def __init__( ): super().__init__( - "SOUND_STOP", # the name of the command as you have to enter it in the code + "SOUND_STOP, Stop all sound", LIB, ( # Desc Opt Var type p1_val p2_val @@ -238,7 +238,7 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("PID", False, AVV_REQD, PT_INT, None, None), # variable to get PID of new process - ("Command", False, AVV_NO, PT_STRS, None, None), # text of command + ("Command", False, AVV_YES, PT_STRS, None, None), # text of command ), ( # num params, format string (trailing comma is important) diff --git a/commands_file.py b/commands_file.py new file mode 100644 index 0000000..9a68b43 --- /dev/null +++ b/commands_file.py @@ -0,0 +1,67 @@ +# This module contains commands that work on the filesystem +import os, command_base, ms, kb, scripts, traceback, pathlib +from constants import * + +LIB = "cmds_file" # name of this library (for logging) + +# ################################################## +# ### CLASS FILE_HOME ### +# ################################################## + +# class that defines the F_HOME command -- returns the user's home directory +class File_Home(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("F_HOME, Returns the user's home directory", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Home", False, AVV_REQD, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " returns user's home dir in {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + self.Set_param(btn, 1, pathlib.Path.home()) # return the path + + +scripts.Add_command(File_Home()) # register the command + + +# ################################################## +# ### CLASS FILE_DELETE ### +# ################################################## + +# class that defines the F_DEL command -- deletes a file +class File_Delete(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("F_DEL, Returns the user's home directory", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("File", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Deletes {1}"), + ) ) + + + def Process(self, btn, idx, split_line): + file = self.Get_param(btn, 1) + try: + os.remove(self.Get_param(btn, 1, file)) # delete the file + except: + traceback.print_exc() + +scripts.Add_command(File_Delete()) # register the command + + diff --git a/commands_mouse.py b/commands_mouse.py index 2142e78..0d021bb 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -208,7 +208,6 @@ def Process(self, btn, idx, split_line): return -1 - scripts.Add_command(Mouse_Line_Move()) # register the command diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 7880aab..d14dc74 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -670,7 +670,10 @@ def substr(self, symbols, cmd, cmds): y = variables.pop(symbols) z = variables.pop(symbols) - variables.push(symbols, str(z)[x:y]) + r = str(z)[y:x] + variables.push(symbols, r) + + return 1 scripts.Add_command(Rpn_Eval()) # register the command diff --git a/commands_win32.py b/commands_win32.py index b670d78..b087020 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -276,7 +276,7 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_NO, PT_STR, None, None), # name to search for + ("Title", False, AVV_YES, PT_STR, None, None), # name to search for ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M a 1.25 > aa clst\n\nTEST_01\nTEST_01 1\nTEST_11\nTEST_11 1\nTEST_11 a\nTEST_11 aa\nTEST_11 b\nTEST_21\nTEST_21 a\nTEST_21 aa\nTEST_21 c\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_02\nTEST_02 1\nTEST_02 1.25\nTEST_12\nTEST_12 1\nTEST_12 1.25\nTEST_12 a\nTEST_12 aa\nTEST_12 d\nTEST_22\nTEST_22 a\nTEST_22 aa\nTEST_22 e\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_03\nTEST_03 \"1\"\nTEST_03 \"1.25\"\nTEST_13\nTEST_13 \"1\"\nTEST_13 \"1.25\"\nTEST_13 a\nTEST_13 aa\nTEST_13 f\nTEST_23\nTEST_23 a\nTEST_23 aa\nTEST_23 g\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_04\nTEST_04 \"1\"\nTEST_04 \"1.25\"\nTEST_14\nTEST_14 \"1\"\nTEST_14 \"1.25\"\nTEST_14 a\nTEST_14 aa\nTEST_14 h\nTEST_24\nTEST_24 a\nTEST_24 aa\nTEST_24 i\n\nTEST_11 g\nTEST_11 aa\nTEST_12 g\nTEST_12 aa\n\nTEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 85, - 255, - 0 - ], - "text": "@NAME be" - }, - { - "color": [ - 85, - 170, - 0 - ], - "text": "@NAME be," - }, - { - "color": [ - 85, - 85, - 0 - ], - "text": "@NAME question." - }, - { - "color": [ - 85, - 0, - 0 - ], - "text": "@NAME Yorick" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME S" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "TEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 170, - 255, - 0 - ], - "text": "@NAME or" - }, - { - "color": [ - 170, - 170, - 0 - ], - "text": "@NAME that" - }, - { - "color": [ - 170, - 85, - 0 - ], - "text": "@NAME Alas" - }, - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME I" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME T" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 5" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME Test Dlg 1\n@DESC Tests a useless dialog\nDIALOG_INFO \"Title 1\" \"Message 1\"" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 255, - 255, - 0 - ], - "text": "@NAME not" - }, - { - "color": [ - 255, - 170, - 0 - ], - "text": "@NAME is" - }, - { - "color": [ - 255, - 85, - 0 - ], - "text": "@NAME poor" - }, - { - "color": [ - 255, - 0, - 0 - ], - "text": "@NAME knew him well" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME I" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 4" - }, - { - "color": [ - 0, - 255, - 0 - ], +{ + "buttons": [ + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME T" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "- Find the Logbook\n@NAME Find Note pad\n@DESC this is an example of a description\nW_FIND_HWND \"Untitled - Notepad\" ml_hwnd n 1\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Norm Dlg\n@DESC Displays a dialog, then prints the returned value\nRPN_EVAL < a view\nDIALOG_INFO \"Information message\" \"Don't look behind you, there might be a clown!\"\nRPN_EVAL < a view\nDIALOG_OK_CANCEL \"Do you accept?\" \"You do not have any clowns standing behind you\" a\nRPN_EVAL < a view" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME To" + }, + { + "color": [ + 0, + 170, + 0 + ], + "text": "@NAME to" + }, + { + "color": [ + 0, + 85, + 0 + ], + "text": "@NAME the" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME E" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test\n@DESC Run a series of tests to prove parameter passing is working\n@DOC This test is intended as a regression tool to be used after modifying\n@DOC the parameter handling code. This series of tests passes almost every\n@DOC combination of parameters to test routines. The output can be difficult\n@DOC to make sense of, but the critical thing is that none of the lines of\n@DOC output between START and END should change. Changes indicate a new\n@DOC type of bad behaviour that needs to be rectified.\n@DOC\n@DOC I test this by having a reference testlog-base.log and running LPHK\n@DOC with the output redirected to testlog.log. I then use Beyond Compare\n@DOC to evaluate differences.\n@DOC\n@DOC Also note that this script requires the command_test.py module that is\n@DOC not intended to be included in production releases.\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_01\nTEST_01 1\nTEST_11\nTEST_11 1\nTEST_11 a\nTEST_11 aa\nTEST_11 b\nTEST_21\nTEST_21 a\nTEST_21 aa\nTEST_21 c\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_02\nTEST_02 1\nTEST_02 1.25\nTEST_12\nTEST_12 1\nTEST_12 1.25\nTEST_12 a\nTEST_12 aa\nTEST_12 d\nTEST_22\nTEST_22 a\nTEST_22 aa\nTEST_22 e\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_03\nTEST_03 \"1\"\nTEST_03 \"1.25\"\nTEST_13\nTEST_13 \"1\"\nTEST_13 \"1.25\"\nTEST_13 a\nTEST_13 aa\nTEST_13 f\nTEST_23\nTEST_23 a\nTEST_23 aa\nTEST_23 g\n\nRPN_EVAL 1 > a 1.25 > aa clst\n\nTEST_04\nTEST_04 \"1\"\nTEST_04 \"1.25\"\nTEST_14\nTEST_14 \"1\"\nTEST_14 \"1.25\"\nTEST_14 a\nTEST_14 aa\nTEST_14 h\nTEST_24\nTEST_24 a\nTEST_24 aa\nTEST_24 i\n\nTEST_11 g\nTEST_11 aa\nTEST_12 g\nTEST_12 aa\n\nTEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 85, + 255, + 0 + ], + "text": "@NAME be" + }, + { + "color": [ + 85, + 170, + 0 + ], + "text": "@NAME be," + }, + { + "color": [ + 85, + 85, + 0 + ], + "text": "@NAME question." + }, + { + "color": [ + 85, + 0, + 0 + ], + "text": "@NAME Yorick" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME S" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "TEST_101 \"ABC\"\nTEST_101 \"ABC\" a\nTEST_101 b a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 170, + 255, + 0 + ], + "text": "@NAME or" + }, + { + "color": [ + 170, + 170, + 0 + ], + "text": "@NAME that" + }, + { + "color": [ + 170, + 85, + 0 + ], + "text": "@NAME Alas" + }, + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME I" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME T" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 5" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Test Dlg 1\n@DESC Tests a useless dialog\nDIALOG_INFO \"Title 1\" \"Message 1\"" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 255, + 255, + 0 + ], + "text": "@NAME not" + }, + { + "color": [ + 255, + 170, + 0 + ], + "text": "@NAME is" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "@NAME poor" + }, + { + "color": [ + 255, + 0, + 0 + ], + "text": "@NAME knew him well" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME I" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 4" + }, + { + "color": [ + 0, + 255, + 0 + ], "text": "@NAME Test Dlg 2\n@DESC Also Tests a useless dialog\n@DOC Press this button and the other similar button. This should result in\n@DOC only one dialog appearing at a time. Cancelling the button should\n@DOC close an open dialog, or prevent a queued dialog from showing.\n@DOC OK, Cancel, and closing the dialog produce unique return results.\nDIALOG_INFO \"Title 2\" \"Message 2\"" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "ABORT\nEND" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME N" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 3" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "CALL:RETURN_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME G" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "DELAY 2" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "CALL:END_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - } - ], - [ - { - "color": [ - 170, - 0, - 0 - ], - "text": "@NAME !" - }, - { - "color": [ - 255, - 85, - 0 - ], - "text": "- test\nDIALOG_OK_CANCEL \"Continue this LPHK script?\" \"Press OK to continue or Cancel to abort right now.\" a" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME A 1\nCALL:ABORT_ONE a\nRPN_EVAL view_l" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME Dump\n@DESC Dump all commands\n@DOC Lists all the heaqers, commands, subroutines, and buttons\n@DOC with whatever we can figure out about them\nTEST_DUMP_DEBUG" - } - ], - [ - { - "color": [ - 0, - 0, - 0 - ], - "text": "" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME W" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WW" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WWW" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WW WW WW WW WW" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WW WW" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME W W" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WW WW" - }, - { - "color": [ - 0, - 255, - 0 - ], - "text": "@NAME WWW WWW" - } - ] - ], - "subroutines": [ - [ - "@SUB RETURN_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "RETURN", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB ABORT_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "ABORT", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB END_ONE @a%", - "RPN_EVAL view_l < a 1 + > a view_l", - "END", - "RPN_EVAL view_l < a 1 + > a view_l" - ], - [ - "@SUB ADD_ONE @a%", - "@DESC Adds 1 to the integer parameter passed", - "@DOC Line 1 of documentation", - "@DOC Line 2 of documentation", - "RPN_EVAL view_l < a 1 + > a view_l" - ] - ], - "version": "0.2" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "ABORT\nEND" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME N" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 3" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:RETURN_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME G" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "DELAY 2" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "CALL:END_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + } + ], + [ + { + "color": [ + 170, + 0, + 0 + ], + "text": "@NAME !" + }, + { + "color": [ + 255, + 85, + 0 + ], + "text": "- test\nDIALOG_OK_CANCEL \"Continue this LPHK script?\" \"Press OK to continue or Cancel to abort right now.\" a" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME A 1\nCALL:ABORT_ONE a\nRPN_EVAL view_l" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME Dump\n@DESC Dump all commands\n@DOC Lists all the heaqers, commands, subroutines, and buttons\n@DOC with whatever we can figure out about them\nTEST_DUMP_DEBUG" + } + ], + [ + { + "color": [ + 0, + 0, + 0 + ], + "text": "" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME W" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WWW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW WW WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME W W" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WW WW" + }, + { + "color": [ + 0, + 255, + 0 + ], + "text": "@NAME WWW WWW" + } + ] + ], + "subroutines": [ + [ + "@SUB RETURN_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "RETURN", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ABORT_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "ABORT", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB END_ONE @a%", + "RPN_EVAL view_l < a 1 + > a view_l", + "END", + "RPN_EVAL view_l < a 1 + > a view_l" + ], + [ + "@SUB ADD_ONE @a%", + "@DESC Adds 1 to the integer parameter passed", + "@DOC Line 1 of documentation", + "@DOC Line 2 of documentation", + "RPN_EVAL view_l < a 1 + > a view_l" + ] + ], + "version": "0.2" } \ No newline at end of file diff --git a/window.py b/window.py index 0cd0e74..cb1ca51 100644 --- a/window.py +++ b/window.py @@ -116,6 +116,8 @@ def __init__(self, master=None): self.last_clicked = None self.outline_box = None self._redraw = False + self.over_x = -1 # The button the user is over + self.over_y = -1 def init_window(self): global root @@ -165,12 +167,17 @@ def init_window(self): c_gap = int(BUTTON_SIZE // 4) c_size = (BUTTON_SIZE * 9) + (c_gap * 9) - self.c = tk.Canvas(self, width=c_size, height=c_size) + self.c = tk.Canvas(self, width=c_size, height=c_size - c_gap//2) + self.c.bind("", self.mouse_move) self.c.bind("", self.click) - self.c.grid(row=0, column=0, padx=round(c_gap/2), pady=round(c_gap/2)) + self.c.grid(row=0, column=0, padx=round(c_gap/2), pady=0) + + self.desc = tk.Label(self, text="", fg="#000", height=2) + self.desc.grid(row=1, column=0, pady=0) + self.desc.config(font=("Courier", BUTTON_SIZE // 4, "bold")) self.stat = tk.Label(self, text="No Launchpad Connected", bg=STAT_INACTIVE_COLOR, fg="#fff") - self.stat.grid(row=1, column=0, sticky=tk.EW) + self.stat.grid(row=2, column=0, sticky=tk.EW, pady=0) self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) def redraw(self, x, y): @@ -371,6 +378,26 @@ def save_layout(self): files.save_lp_to_layout(files.curr_layout) files.load_layout_to_lp(files.curr_layout) + # the mouse has entered a button + def mouse_move(self, event): + gap = int(BUTTON_SIZE // 4) + + x = min(8, int(event.x // (BUTTON_SIZE + gap))) + if event.x < (gap/2 + x * (BUTTON_SIZE + gap)) or event.x > (x+1) * (BUTTON_SIZE + gap) - gap/2 - 1: + x = -1 + y = -1 + else: + y = min(8, int(event.y // (BUTTON_SIZE + gap))) + if event.y < (gap/2 + y * (BUTTON_SIZE + gap)) or event.y > (y+1) * (BUTTON_SIZE + gap) - gap/2 - 1: + y = -1 + x = -1 + + #self.c.bind("", self.mouse_move) + if x != self.over_x or y != self.over_y: + self.over_x = x + self.over_y = y + Redraw(x, y) + def click(self, event): gap = int(BUTTON_SIZE // 4) @@ -490,9 +517,7 @@ def text_x(x): def text_y(y): return round((BUTTON_SIZE * y) + (gap * y) + (BUTTON_SIZE / 2) + (gap / 2)) - def fmt(x, y, lines=3, cols=5): - t = scripts.buttons[x][y].name # get the text - + def fmt_str(t, lines=2, cols=55): if len(t) <= cols: # if name is less than 5 characters return t # return it unchanged @@ -521,10 +546,13 @@ def fmt(x, y, lines=3, cols=5): for i in range(len(tl)): # then add left padding to help formatting l = len(tl[i]) if l < m: - tl[i] = ' '*((m - l)//2) + tl[i] + pass #tl[i] = ' '*((m - l)//2) + tl[i] return "\n".join(tl) # and return them with line separations between them + def fmt(x, y, lines=3, cols=5): + return fmt_str(scripts.buttons[x][y].name, lines, cols) + if self.last_clicked != None: if self.outline_box == None: @@ -565,6 +593,12 @@ def fmt(x, y, lines=3, cols=5): self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) + if self.over_x != -1 and self.over_y != -1: + desc = fmt_str(scripts.buttons[self.over_x][self.over_y].desc) + else: + desc = '' + app.desc["text"] = desc + global lp_object if self.button_mode == LM_RUN: self.c.itemconfig(self.grid_rects[8][0][0], fill="red") From b3c57a0c1fe6ff595711278d1d545f29b6b27d79 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 19 Mar 2021 19:23:39 +0800 Subject: [PATCH 67/83] Mostly bug fixes. This includes adding functionality to ensure subroutine dependencies are checked * commands_subroutines.py Fix to parse routine to return errors so we can do something with them * commands_subroutines.py AddFunction properly handles errors in subroutine loading, including not actually loading it! * constants.py New colour (black) for disabled buttons * files.py set new flag "invalid_on_load" to true if script is invalid on load (rather than popping up a dialog that won't go away) * files.py When loading subroutines (not from a layout) keep track of all those loaded successfully or failed. Use this to print a message informing the operator. * files.py after add new routine to check buttons after changing subroutines. * files.py ensure return from Add_function is correctly checked for success * lp_colors add handling for disabled buttons * lp_colors.py supress a message about launchpad discoonnected that will otherwise show up 81 times. * Scripts.py define Button.invalid_on_load and set to False on init. * scripts.py Use invalid_on_load to prevent scheduling of a script * scripts.py Validate_script needs to return error message in case of error * scripts.py mark layout as changed if button scheduled to run is unbound. * scripts.py Unbind_all needs to actually unbind the buttons to cause the text on buttons to be removed. * scripts.py unloading all subroutines also needs to revalidate buttons. * test layout now has an error * window.py fix the "delete button before verifying" bug * window.py get_colour in draw_canvas knows to override colour on invalid buttons * window.py new function mark_disabled adds and removes red crosses over disabled buttons * window.py grid_rects now has room for the lines that need to be drawn and un-drawn to add/remove them * window.py validate_func used to validate scripts in the editor now also updates invalid_on_load --- commands_header.py | 2 +- commands_subroutines.py | 21 ++++++----- constants.py | 1 + files.py | 80 ++++++++++++++++++++++++++++++++++------ lp_colors.py | 8 +++- scripts.py | 27 +++++++++++--- user_layouts/Testing.lpl | 2 +- window.py | 70 ++++++++++++++++++++++++++--------- 8 files changed, 162 insertions(+), 49 deletions(-) diff --git a/commands_header.py b/commands_header.py index 633a4a2..aa5bcfe 100644 --- a/commands_header.py +++ b/commands_header.py @@ -216,7 +216,7 @@ def __init__( "using the `@SUB` header."] - # Dummy validate routine. Simply says all is OK (unless you try to do it in a real button!) + # Dummy validate routine. Simply says all is OK (unless you try to do it in a subroutine!) def Validate( self, btn, diff --git a/commands_subroutines.py b/commands_subroutines.py index 30695a5..799ed71 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -212,10 +212,10 @@ def Process(self, btn, idx, split_line): self.Set_param(btn, n+1, pn) # and store it - # This is not the parse routine called for validation! @@@ not used??? + # This is not the parse routine called for validation! def Parse_Sub(self): try: - script_validate = self.btn.Parse_script() #@@@ does not raise an error + return self.btn.Parse_script() # return any error in validation except: self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") raise @@ -335,11 +335,12 @@ def Get_Name_And_Params(lines, sub_n, fname): def Add_Function(lines, sub_n, fname): # This function is passed a list of lines. The first non-comment line must define the header + err = None # first let's parse out the header to get the name and the parameters name, params, lin = Get_Name_And_Params(lines, sub_n, fname) if isinstance(params, str): - return False, name, params + return False, name, params, err NewCommand = Subroutine(name, params, lines) # Create a new command object for this subroutine @@ -356,11 +357,13 @@ def Add_Function(lines, sub_n, fname): print("[subroutines] Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.") raise - if script_validation != True: # if thre is an error in validation - if old_cmd: # and there is a replaced command - scripts.Add_command(NewCommand) # put the old command back - pass # @@@ there must be more to do! :-) This is the error return + if isinstance(script_validation, bool) and script_validation: # if validation is OK + err = True else: - pass # @@@ this is the success return. There must be more to do! + if old_cmd: # and there is a replaced command + scripts.Add_command(old_cmd) # put the old command back + else: + scripts.Remove_command(NewCommand.name) + err = False - return True, NewCommand.name, params + return True, NewCommand.name, params, err diff --git a/constants.py b/constants.py index a1cbc1c..cba2605 100644 --- a/constants.py +++ b/constants.py @@ -94,6 +94,7 @@ VALID_BOOL = VALID_BOOL_TRUE + VALID_BOOL_FALSE # Misc constants +COLOR_DISABLED = 0 # black COLOR_PRIMED = 5 #red COLOR_FUNC_KEYS_PRIMED = 9 #amber EXIT_UPDATE_DELAY = 0.1 diff --git a/files.py b/files.py index 33b7aaf..6a8daba 100644 --- a/files.py +++ b/files.py @@ -181,13 +181,11 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): print("[files] Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.") raise if script_validation != True: - lp_colors.update_all() - window.Redraw(True) - in_error = True - window.app.save_script(window.app, x, y, script_text, open_editor = True, color = color) - in_error = False + btn.invalid_on_load = True + color = [0, 0, 0] else: - scripts.Bind(x, y, btn, color) + btn.invalid_on_load = False + scripts.Bind(x, y, btn, color) else: lp_colors.setXY(x, y, color) @@ -209,18 +207,76 @@ def load_subroutines_to_lp(name, popups=True, preload=None): with open(name, 'r') as in_subs: subs = in_subs.read().split('\n===\n') + loaded = [] + not_loaded = [] + s_tot = 0 + s_ok = 0 + s_fail = 0 for i, sub in enumerate(subs): - load_subroutine(sub.splitlines(), i+1, name) + ok, name = load_subroutine(sub.splitlines(), i+1, name) + name = name[5:] + s_tot += 1 + if ok: + s_ok += 1 + loaded.append(name) + else: + s_fail += 1 + not_loaded.append(name) + + s_tot = len(loaded) + nl = '\n' + + if s_fail > 0: + window.app.popup(window.app, "Subroutines loaded with errors", window.app.info_image, f"{s_fail} of {s_tot} subroutines not loaded due to errors:{nl}{nl}{nl.join(not_loaded)}", "OK") + elif s_ok > 0: + window.app.popup(window.app, "Subroutines loaded sucessfully", window.app.info_image, f"{s_tot} subroutines loaded sucessfully:{nl}{nl}{nl.join(loaded)}", "OK") + else: + window.app.popup(window.app, "Nothing to load", window.app.info_image, f"No subroutines found in file", "OK") + + validate_all_buttons() + +def validate_all_buttons(): + b_tot = 0 + b_ok = 0 + b_fail = 0 + + for x in range(9): + for y in range(9): + btn = scripts.buttons[x][y] + + if btn.script_str != "": + b_tot += 1 + script_validation = None + try: + script_validation = btn.Validate_script() + except: + new_layout_func = lambda: window.app.unbind_lp(prompt_save = False) + if popups: + window.app.popup(window.app, "Script Validation Error", window.app.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = new_layout_func) + else: + print("[files] Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.") + raise + if script_validation != True: + b_fail += 1 + btn.invalid_on_load = True + else: + b_ok += 1 + btn.invalid_on_load = False + + window.Redraw(True) + + if b_fail > 0: + window.app.popup(window.app, "Buttons in disabled", window.app.info_image, f"{b_fail} of {b_tot} buttons disabled due to errors", "OK") # load a single subroutine def load_subroutine(sub, sub_n, fname): import commands_subroutines - ok, name, params = commands_subroutines.Add_Function(sub, sub_n, fname) # Attempt to load the command + ok, name, params, err = commands_subroutines.Add_Function(sub, sub_n, fname) # Attempt to load the command - if ok: - pass # @@@ there must be more to do! :-) - else: - pass # @@@ likewise + if err == None: + err = False + + return err, name def import_script(name): with open(name, "r") as f: diff --git a/lp_colors.py b/lp_colors.py index b03accd..8a1ca8d 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -101,7 +101,10 @@ def updateXY(x, y): #print("Update colors for (" + str(x) + ", " + str(y) + "), is_running = " + str(is_running)) - if is_running: # if the button is running + if btn.invalid_on_load: + set_color = scripts.COLOR_DISABLED # disabled button due to script error + color_modes[x][y] = "solid" + elif is_running: # if the button is running set_color = scripts.COLOR_PRIMED # set the desired colour color_modes[x][y] = "flash" # and mode elif (x, y) in [l[1:] for l in scripts.to_run]: # is it waiting to run? @@ -136,7 +139,8 @@ def updateXY(x, y): else: lp_object.LedCtrlXYByCode(x, y, set_color) else: - print("[lp_colors] (" + str(x) + ", " + str(y) + ") Launchpad is disconnected, cannot update.") + pass + #print("[lp_colors] (" + str(x) + ", " + str(y) + ") Launchpad is disconnected, cannot update.") # update the colours of all buttons def update_all(): diff --git a/scripts.py b/scripts.py index 7c77860..2a3428d 100644 --- a/scripts.py +++ b/scripts.py @@ -234,6 +234,8 @@ def __init__( self._running = False # is the script running? self.is_async = False # async execution flag + self.invalid_on_load = False # flag for button found invalid on load from stored layout + # The "root" is the button that is scheduled. This allows subroutines to check if the # initiating button has been killed. if root == None: # if we are not being called @@ -402,6 +404,9 @@ def Schedule_script(self): self.thread.kill.set() return + if self.invalid_on_load: # don't schedule invalid code + return + if (self.x, self.y) in [l[1:] for l in to_run]: print("[scripts] " + self.coords + " Script already scheduled, unscheduling...") indexes = [i for i, v in enumerate(to_run) if ((v[1] == self.x) and (v[2] == self.y))] @@ -534,7 +539,7 @@ def strip_quoted(line): pline = pline[len(param):].strip() # and clean up the line (@@@ why not use strip1()??) if not variables.valid_var_name(param): # but check it's a valid variable name if desc == PT_ANY[AVT_DESC]: # PT_ANY will accept non-variables as strings - sline += [param] # we'll add it as the parameter value. Note we don't add a leading " + sline += [param] # we'll add it as the parameter value. Note we don't add a leading " # because we can try to interpret it as numeric later on else: return ('Error in variable for param #' + str(n+1), line) # if it's not a string and not a variable... @@ -691,7 +696,8 @@ def Validate_script(self): self.validated = True return True # ...validation succeeds! - if self.Parse_script(): # If parsing is OK + validation = self.Parse_script() # parse the script + if validation == True: # If parsing is OK self.validated = True # Script is valid if len(self.script_lines) > 0: # look for async header and set flag @@ -701,6 +707,7 @@ def Validate_script(self): VALID_COMMANDS[cmd_txt].is_async else: self.symbols = None # otherwise destroy symbol table + return validation return self.validated # and tell us the result @@ -726,7 +733,7 @@ def Bind(x, y, script_str, color): btn.Validate_script() except: pass - + buttons[x][y] = btn if (x, y) in [l[1:] for l in to_run]: # If this button is scheduled to run... @@ -756,7 +763,8 @@ def Unbind(x, y): for index in indexes[::-1]: # and for each of them (in reverse order) temp = to_run.pop(index) # Remove them from the list buttons[x][y] = btn # Clear the button script - #return # WHY do we return here? + files.layout_changed_since_load = True # Mark the layout as changed + return # WHY do we return here? if btn.thread != None: # If the button is actially executing thread.kill.set() # then kill it @@ -809,7 +817,7 @@ def Copy(x1, y1, x2, y2): # Delete a button def Del(x1, y1, x2, y2): global buttons - + if x1 != x2 or y1 != y2: return @@ -867,7 +875,12 @@ def kill_all(): # Unbind all keys. def Unbind_all(): lp_events.unbind_all() # Unbind all events - text = [["" for y in range(9)] for x in range(9)] # Reienitialise all scripts to blank + + for x in range(9): + for y in range(9): + Unbind(x, y) + + #text = [["" for y in range(9)] ] # Reienitialise all scripts to blank kill_all() # stop everything running @@ -888,5 +901,7 @@ def Unload_all(): Remove_command(cmd) # remove it files.layout_changed_since_load = True # mark layout as changed + + files.validate_all_buttons() # ensure buttons are valid diff --git a/user_layouts/Testing.lpl b/user_layouts/Testing.lpl index fffdfdb..e470e80 100644 --- a/user_layouts/Testing.lpl +++ b/user_layouts/Testing.lpl @@ -31,7 +31,7 @@ 255, 0 ], - "text": "@NAME Norm Dlg\n@DESC Displays a dialog, then prints the returned value\nRPN_EVAL < a view\nDIALOG_INFO \"Information message\" \"Don't look behind you, there might be a clown!\"\nRPN_EVAL < a view\nDIALOG_OK_CANCEL \"Do you accept?\" \"You do not have any clowns standing behind you\" a\nRPN_EVAL < a view" + "text": "@NAME Norm Dlg\n@DESC Displays a dialog, then prints the returned value\nxRPN_EVAL < a view\nDIALOG_INFO \"Information message\" \"Don't look behind you, there might be a clown!\"\nRPN_EVAL < a view\nDIALOG_OK_CANCEL \"Do you accept?\" \"You do not have any clowns standing behind you\" a\nRPN_EVAL < a view" }, { "color": [ diff --git a/window.py b/window.py index cb1ca51..b672a2c 100644 --- a/window.py +++ b/window.py @@ -461,7 +461,7 @@ def click(self, event): if ((self.last_clicked) != (column, row)): self.popup_choice(self, "Please confirm to delete", self.warning_image, "You must click twice on the same button to delete it.", [["OK", None]]) elif scripts.Is_bound(column, row): - self.popup_choice(self, "Last chance!", self.warning_image, "Do you really want to delete this button.", [["No", None], ["Yes", del_func()]]) + self.popup_choice(self, "Last chance!", self.warning_image, "Do you really want to delete this button.", [["No", None], ["Yes", del_func]]) elif self.button_mode == LM_SWAP: swap_func() self.last_clicked = None @@ -484,7 +484,10 @@ def draw_button(self, column, row, color="#000000", shape="square"): def draw_canvas(self, bx=None, by=None): def get_colour(x, y): - if scripts.buttons[x][y].running(): # if the button is running + btn = scripts.buttons[x][y] + if btn.invalid_on_load: # invalid buttons + return "#000000" # black + if btn.running(): # if the button is running return "#FF0000" # make the button red return lp_colors.getXY_RGB(x, y) # otherwise, the normal colour @@ -552,7 +555,28 @@ def fmt_str(t, lines=2, cols=55): def fmt(x, y, lines=3, cols=5): return fmt_str(scripts.buttons[x][y].name, lines, cols) - + + def mark_disabled(x, y): + if len(self.grid_rects[x][y]) != 4: # must have 4 values + return + + btn = scripts.buttons[x][y] # get the button + + if not btn.invalid_on_load: # is it valid? + if not self.grid_rects[x][y][2] == None: # if there's lines + self.c.delete(self.grid_rects[x][y][2]) # remove them + self.c.delete(self.grid_rects[x][y][3]) + self.grid_rects[x][y][2] = None + self.grid_rects[x][y][3] = None + return + elif self.grid_rects[x][y][2] == None: # otherwise, if no lines + x_start = round((BUTTON_SIZE * x) + (gap * x) + (gap / 2)) # calculate the end points and then draw them + y_start = round((BUTTON_SIZE * y) + (gap * y) + (gap / 2)) + x_end = x_start + BUTTON_SIZE + y_end = y_start + BUTTON_SIZE + + self.grid_rects[x][y][2] = self.c.create_line(x_start, y_start, x_end, y_end, width=3, fill='red') + self.grid_rects[x][y][3] = self.c.create_line(x_start, y_end, x_end, y_start, width=3, fill='red') if self.last_clicked != None: if self.outline_box == None: @@ -578,6 +602,7 @@ def fmt(x, y, lines=3, cols=5): y = 0 self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) + mark_disabled(x, y) if bx == None or (bx == 8): for y in range(1, 9): @@ -585,6 +610,7 @@ def fmt(x, y, lines=3, cols=5): x = 8 self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) + mark_disabled(x, y) for x in range(8): if bx == None or (bx == x): @@ -592,6 +618,7 @@ def fmt(x, y, lines=3, cols=5): if by == None or (by == y): self.c.itemconfig(self.grid_rects[x][y][0], fill=get_colour(x, y)) self.c.itemconfig(self.grid_rects[x][y][1], text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) + mark_disabled(x, y) if self.over_x != -1 and self.over_y != -1: desc = fmt_str(scripts.buttons[self.over_x][self.over_y].desc) @@ -621,35 +648,40 @@ def fmt(x, y, lines=3, cols=5): else: for x in range(8): y = 0 - self.grid_rects[x][y] = ( \ + self.grid_rects[x][y] = [ \ self.draw_button(x, y, color=get_colour(x, y), shape="circle"), - self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ - ) + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)), \ + None, None + ] for y in range(1, 9): x = 8 - self.grid_rects[x][y] = ( \ + self.grid_rects[x][y] = [ \ self.draw_button(x, y, color=get_colour(x, y), shape="circle"), - self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)) \ - ) + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y, 2, 3), fill=txt_col(x, y), font=txt_font(x, y, True)), \ + None, None + ] for x in range(8): for y in range(1, 9): - self.grid_rects[x][y] = ( \ + self.grid_rects[x][y] = [ \ self.draw_button(x, y, color=get_colour(x, y)), \ - self.c.create_text(text_x(x), text_y(y), text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)) \ - ) + self.c.create_text(text_x(x), text_y(y), text=fmt(x, y), fill=txt_col(x, y), font=txt_font(x, y)), \ + None, None + ] if self.button_mode == LM_RUN: - self.grid_rects[8][0] = ( \ + self.grid_rects[8][0] = [ \ self.draw_button(8, 0, color="red"), \ - self.c.create_text(text_x(8), text_y(0), fill="yellow", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) \ - ) + self.c.create_text(text_x(8), text_y(0), fill="yellow", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")), \ + None, None + ] else: - self.grid_rects[8][0] = ( \ + self.grid_rects[8][0] = [ \ self.draw_button(8, 0, color=self.c["background"]), \ - self.c.create_text(text_x(8), text_y(0), fill="black", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) \ - ) + self.c.create_text(text_x(8), text_y(0), fill="black", text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")), \ + None, None + ] self.grid_drawn = True @@ -678,6 +710,7 @@ def validate_func(): text_string = t.get(1.0, tk.END) try: btn = scripts.Button(x, y, text_string) + btn.invalid_on_load = True script_validate = btn.Parse_script() except: #self.save_script(w, x, y, text_string) # This will fail and throw a popup error @@ -686,6 +719,7 @@ def validate_func(): if script_validate != True and files.in_error: self.save_script(w, x, y, text_string) else: + btn.invalid_on_load = False w.destroy() w.protocol("WM_DELETE_WINDOW", validate_func) From 1802032a2c654dcd18d49dbdb76439d05681600d Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 22 Mar 2021 16:27:53 +0800 Subject: [PATCH 68/83] Minor fixes * Commands_Dialog allows variables for title and message * commands_keys.py Autovalidation using declared function fails for int when variable is passed - some code commented out to mask this. This duplicates a previous problem. Need to find a better solution * commands_scrape.py incorrectly spelt SW_SHOWMAXIMIZED * ruggedising of clipboard commands_dialog.py * scripts.py when copying a parsed button, the self.validated value must also be copied * added assorted comments and documentation --- commands_dialog.py | 12 +++---- commands_keys.py | 4 +-- commands_scrape.py | 2 +- commands_win32.py | 78 ++++++++++++++++++++++++++++------------------ scripts.py | 1 + 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/commands_dialog.py b/commands_dialog.py index c0da941..0c0530b 100644 --- a/commands_dialog.py +++ b/commands_dialog.py @@ -18,8 +18,8 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_NO, PT_STR, None, None), - ("Message", False, AVV_NO, PT_STR, None, None), + ("Title", False, AVV_YES, PT_STR, None, None), + ("Message", False, AVV_YES, PT_STR, None, None), ("Return", True, AVV_REQD,PT_INT, None, None), ), ( # How to log runtime execution @@ -68,8 +68,8 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_NO, PT_STR, None, None), - ("Message", False, AVV_NO, PT_STR, None, None), + ("Title", False, AVV_YES, PT_STR, None, None), + ("Message", False, AVV_YES, PT_STR, None, None), ), ( # How to log runtime execution # num params, format string (trailing comma is important) @@ -101,8 +101,8 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_NO, PT_STR, None, None), - ("Message", False, AVV_NO, PT_STR, None, None), + ("Title", False, AVV_YES, PT_STR, None, None), + ("Message", False, AVV_YES, PT_STR, None, None), ("Return", True, AVV_REQD,PT_INT, None, None), ), ( # How to log runtime execution diff --git a/commands_keys.py b/commands_keys.py index 999ed07..67a9a9a 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -47,12 +47,12 @@ def __init__( ): super().__init__( - "TAP", # the name of the command as you have to enter it in the code + "TAP, Tap the named key", # the name of the command as you have to enter it in the code LIB, ( # Desc Opt Var type p1_val p2_val ("Key", False, AVV_NO, PT_KEY, None, None), - ("Times", True, AVV_YES, PT_INT, variables.Validate_gt_zero, None), + ("Times", True, AVV_YES, PT_INT, None, None), #@@@ this also doesn't work variables.Validate_gt_zero, None), ("Duration", True, AVV_YES, PT_FLOAT, variables.Validate_ge_zero, None), ), ( diff --git a/commands_scrape.py b/commands_scrape.py index c6f2117..c45e852 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -240,7 +240,7 @@ def __init__( self, ): - super().__init__("S_COLOUR, determines the average R, G, and B values of a clipboard image", + super().__init__("S_COLOUR, determines the average R, G, and B values of an image", LIB, ( # Desc Opt Var type p1_val p2_val diff --git a/commands_win32.py b/commands_win32.py index b087020..62c8e7d 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -17,7 +17,7 @@ def restore_window(self, hwnd, fg = False): place = win32gui.GetWindowPlacement(hwnd) # get info about the window if place[1] == win32con.SW_SHOWMAXIMIZED: # if it is maximised - win32gui.ShowWindow(hwnd, win32con.SW_SHOWMAXIMISED) # then keep it maximised + win32gui.ShowWindow(hwnd, win32con.SW_SHOWMAXIMIZED) # then keep it maximised elif place[1] == win32con.SW_SHOWMINIMIZED: # if minimised win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # then restore it else: @@ -68,7 +68,7 @@ def __init__( self, ): - super().__init__("W_GET_CARET", # the name of the command as you have to enter it in the code + super().__init__("W_GET_CARET, Return the position of the caret on the current window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -117,7 +117,7 @@ def __init__( self, ): - super().__init__("W_GET_FG_HWND", # the name of the command as you have to enter it in the code + super().__init__("W_GET_FG_HWND, Return the handle of the current foreground window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -148,7 +148,7 @@ def __init__( self, ): - super().__init__("W_SET_FG_HWND", # the name of the command as you have to enter it in the code + super().__init__("W_SET_FG_HWND, Make the specified window the current window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -186,7 +186,7 @@ def __init__( self, ): - super().__init__("W_CLIENT_TO_SCREEN", # the name of the command as you have to enter it in the code + super().__init__("W_CLIENT_TO_SCREEN, Convert a client-relative coordinate to a screen-absolute coordinate", LIB, ( # Desc Opt Var type p1_val p2_val @@ -229,7 +229,7 @@ def __init__( self, ): - super().__init__("W_SCREEN_TO_CLIENT", # the name of the command as you have to enter it in the code + super().__init__("W_SCREEN_TO_CLIENT, Convert a screen(absolute) coordinate to a form-relative coordinate", LIB, ( # Desc Opt Var type p1_val p2_val @@ -464,6 +464,35 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Win32_Restore_Hwnd()) # register the command +def ClearClipboard(): + for i in range(5): + x = True + try: # clear the clipboard + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + except: + x = False + finally: + try: + win32clipboard.CloseClipboard() + if x: break + except: + pass # we don't care if the clipboard wasn't opened! + +def SetClipboard(text): + for i in range(5): + x = True + try: + win32clipboard.OpenClipboard() + win32clipboard.SetClipboardText(text) # and put the string in the clipboard + except: + x = False + finally: + try: + win32clipboard.CloseClipboard() + if x: break + except: + pass # we don't care if the clipboard wasn't opened! # ################################################## # ### CLASS W_COPY ### @@ -475,7 +504,7 @@ def __init__( self, ): - super().__init__("W_COPY", # the name of the command as you have to enter it in the code + super().__init__("W_COPY, Copy data from the current window", LIB, ( # Desc Opt Var type p1_val p2_val @@ -490,14 +519,7 @@ def __init__( def Process(self, btn, idx, split_line): hwnd = win32gui.GetForegroundWindow() # get the current window - try: # clear the clipboard - win32clipboard.OpenClipboard() - win32clipboard.EmptyClipboard() - finally: - try: - win32clipboard.CloseClipboard() - except: - pass # we don't care if the clipboard wasn't opened! + ClearClipboard() try: # do the keyboard stuff for copy (sending a WM_COPY message does not always work) kb.press(kb.sp('ctrl')) @@ -532,11 +554,11 @@ def __init__( self, ): - super().__init__("W_PASTE", # the name of the command as you have to enter it in the code + super().__init__("W_PASTE, Paste data into the current window", LIB, ( # Desc Opt Var type p1_val p2_val - ("Clipboard", True, AVV_REQD, PT_STR, None, None), # variable to contain item to paste + ("Clipboard", True, AVV_YES, PT_STR, None, None), # variable to contain item to paste ), ( # num params, format string (trailing comma is important) @@ -550,21 +572,15 @@ def Process(self, btn, idx, split_line): hwnd = win32gui.GetForegroundWindow() # get the current window c = self.Get_param(btn, 1) # get the value + ClearClipboard() + SetClipboard(str(c)) try: - win32clipboard.OpenClipboard() - win32clipboard.EmptyClipboard() # clear the clipboard first (because that makes it work) - win32clipboard.SetClipboardText(str(c)) # and put the string in the clipboard + kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) + kb.tap(kb.sp('v')) finally: - import traceback - traceback.print_exc() - win32clipboard.CloseClipboard() + kb.release(kb.sp('ctrl')) + - # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste - try: - kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) - kb.tap(kb.sp('v')) - finally: - kb.release(kb.sp('ctrl')) scripts.Add_command(Win32_Paste()) # register the command @@ -580,7 +596,7 @@ def __init__( self, ): - super().__init__("W_WAIT", # the name of the command as you have to enter it in the code + super().__init__("W_WAIT, Pause until the process associated with a window handle is reasy for input", LIB, ( # Desc Opt Var type p1_val p2_val @@ -617,7 +633,7 @@ def __init__( self, ): - super().__init__("W_PID_TO_HWND", # the name of the command as you have to enter it in the code + super().__init__("W_PID_TO_HWND, Return the handle of a window associated with a PID", LIB, ( # Desc Opt Var type p1_val p2_val diff --git a/scripts.py b/scripts.py index 2a3428d..2ed7d1a 100644 --- a/scripts.py +++ b/scripts.py @@ -336,6 +336,7 @@ def Copy_parsed(self, new_btn, name="SUB"): new_btn.symbols[SYM_LABELS] = self.symbols[SYM_LABELS].copy() # and the position of labels new_btn.is_async = self.is_async # default is NOT async + new_btn.validated = self.validated # Need to copy over the validation flag (it should be True at this point) # check "self" for death notification From 89d3d2a5b0dc84317f7d7bf7b7a3d7e8a4eff983 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 26 Mar 2021 14:07:53 +0800 Subject: [PATCH 69/83] Documentation, preparation to remove "test" commands, and some functionality improvements * command_list.py - Add commands_documentation.py as a module (to formalise the creation of documentation) * commands_browser_automation.py - add escape characters in sending of text \n (newline) \^ (up-arrow), and \h (home) * commands_browser_automation.py - add LABEL and CSS_SELECTOR as options to find elements * commands_browser_automation.py - rename class Bauto_send_keys to BAuto_send_Text to better match the command (BA_SEND_TEXT) * commands_documentation.py - move the quick and dirty command to create documentation from the test module to its own module (DOCUMENTATION). * commands_file.py - add a return code to F_DELETE * commands_file.py - add a command F_FILE_EXISTS to determine if a file exists * commands_file.py - add a command F_PATH_EXISTS to determine if a path exists * commands_file.py - add a command F_ENSURE_PATH_EXISTS to create directories to make a path valid * commands_scrape.py - add retries to code that gets an image from the clipboard, because it can fail the first time. * constants.py - further constants for DOCUMENTATION command * files.py - fix typo in dialog title * scripts.py - allow source to be included in documentation --- command_list.py | 3 +- commands_browser_automation.py | 39 ++++++---- commands_documentation.py | 106 ++++++++++++++++++++++++++ commands_file.py | 134 ++++++++++++++++++++++++++++++++- commands_scrape.py | 20 +++-- commands_win32.py | 5 +- constants.py | 12 +-- files.py | 2 +- scripts.py | 14 +++- 9 files changed, 302 insertions(+), 33 deletions(-) create mode 100644 commands_documentation.py diff --git a/command_list.py b/command_list.py index 2c33fda..0952aa0 100644 --- a/command_list.py +++ b/command_list.py @@ -16,7 +16,8 @@ commands_subroutines, \ commands_dialog, \ commands_browser_automation, \ - commands_file + commands_file, \ + commands_documentation # @@@ a test command set for testing things! Will be removed for production try: diff --git a/commands_browser_automation.py b/commands_browser_automation.py index e80a25f..bf4ad12 100644 --- a/commands_browser_automation.py +++ b/commands_browser_automation.py @@ -1,11 +1,15 @@ # This module is VERY specific to Win32 import command_base, scripts, traceback from selenium import webdriver +from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from constants import * LIB = "cmds_baut" # name of this library (for logging) +# constants for characters we replace in strings (note that these are not standard) +ESCAPE_CHARS = ["\\n", "\\^", "\\h"] +REPLACE_CHARS = ["\n", Keys.ARROW_UP, Keys.HOME] # constants for BA_START BAS_CHROME = "CHROME" @@ -228,14 +232,16 @@ def Process(self, btn, idx, split_line): # constants for BA_GET_ELEMENT -BAG_XPATH = "XPATH" -BAG_NAME = "NAME" -BAG_TAG_NAME = "TAG_NAME" -BAG_ID = "ID" -BAG_LINK_TEXT = "LINK_TEXT" -BAG_CLASS_NAME = "CLASS_NAME" +BAG_XPATH = "XPATH" +BAG_NAME = "NAME" +BAG_TAG_NAME = "TAG_NAME" +BAG_ID = "ID" +BAG_LABEL = "LABEL" +BAG_LINK_TEXT = "LINK_TEXT" +BAG_CLASS_NAME = "CLASS_NAME" +BAG_CSS_SELECTOR = "CSS_SELECTOR" -BAGG_ALL = [BAG_XPATH, BAG_NAME, BAG_TAG_NAME, BAG_ID, BAG_LINK_TEXT, BAG_CLASS_NAME] +BAGG_ALL = [BAG_XPATH, BAG_NAME, BAG_TAG_NAME, BAG_ID, BAG_LABEL, BAG_LINK_TEXT, BAG_CLASS_NAME, BAG_CSS_SELECTOR] # ################################################## # ### CLASS BAUTO_GET_ELEMENT ### @@ -522,11 +528,11 @@ def Process(self, btn, idx, split_line): # ################################################## -# ### CLASS BAUTO_SEND_KEYS ### +# ### CLASS BAUTO_SEND_TEXT ### # ################################################## # class that defines the BA_SEND_TEXT command to send keys to an element -class Bauto_Send_Keys(command_base.Command_Basic): +class Bauto_Send_Text(command_base.Command_Basic): def __init__( self, ): @@ -545,14 +551,21 @@ def __init__( self.doc = ["The first parameter `Element` is an element returned from a search.", "", - "This method will send text to that elementt.", + "This method will send text to that element. Note that there are a few special", + "escape characters:", + "", + " `\n` - will be replaced with a newline,", + " `\h` - will be replaced with a press of the home key", + " `\^` - will be replaced with a press of the arrow up key", "", "There is no return value."] - + def Process(self, btn, idx, split_line): element = self.Get_param(btn, 1) text = self.Get_param(btn, 2) + for x, y in zip(ESCAPE_CHARS, REPLACE_CHARS): + text = text.replace(x, y) try: element.send_keys(text) @@ -561,7 +574,7 @@ def Process(self, btn, idx, split_line): traceback.print_exc() -scripts.Add_command(Bauto_Send_Keys()) # register the command +scripts.Add_command(Bauto_Send_Text()) # register the command # ################################################## @@ -607,5 +620,3 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Bauto_ShowElements()) # register the command - - diff --git a/commands_documentation.py b/commands_documentation.py new file mode 100644 index 0000000..9a7f85a --- /dev/null +++ b/commands_documentation.py @@ -0,0 +1,106 @@ +import command_base, commands_header, scripts, variables +from constants import * + +LIB = "cmds_docs" # name of this library (for logging) + +# ################################################## +# ### CLASS DOC_DOCUMENT ### +# ################################################## + +# constants for DOC_DOCUMENT +DD_HEADERS = "HEADERS" +DD_COMMANDS = "COMMANDS" +DD_SUBROUTINES = "SUBROUTINES" +DD_BUTTONS = "BUTTONS" +DD_COMMAND_BASE = "COMMAND_BASE" +DD_DEBUG = "DEBUG" +DD_SOURCE = "SOURCE" + +DDG_ALL = [DD_HEADERS, DD_COMMANDS, DD_SUBROUTINES, DD_BUTTONS, DD_COMMAND_BASE, DD_DEBUG, DD_SOURCE] +DDG_DEFAULT = [DD_HEADERS, DD_COMMANDS, DD_SUBROUTINES, DD_BUTTONS] + +# class that defines more the DOCUMENT command that outputs documentation +class DOC_Document(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("DOCUMENT, Produce documentation on LPHK", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Method", True, AVV_NO, PT_WORDS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (0, " Dump headers and commands"), + ) ) + + + self.doc = ["Prints documentation about LPHK to standard output." + "" + "Any number of valid parameters can be passed. These can be a", + "combination of 'category' parameters (that define the subset(s)", + "of documentation to be printed, and 'modifier' parameters that", + "alter how the documentation is produced.", + "", + "If no parameters are passed, a standard output is produced. If", + "the only parameters passed are 'modifier' parameters, they modify", + "the standard output.", + "", + "The `category` parameters cause documentation to be created for:", + " HEADERS - commands starting with '@' used in scripts.", + " COMMANDS - regular macro commands", + " SUBROUTINES - user-defined subroutines", + " BUTTONS - button scripts", + " COMMAND_BASE - (not yet implemented) routines used when writing commands", + "", + "The `modifier` parameters change the way documentation is produced:", + " DEBUG - includes type ancestory for commands.", + " SOURCE - includes source for buttons and subroutines", + "", + "The default categories are HEADERS COMMANDS SUBROUTINES BUTTONS"] + + + def Process(self, btn, idx, split_line): + doc_set = [] + + for i in range(self.Param_count(btn)): + p = self.Get_param(btn, i+1) # For each parameter + + doc_set = [] # start with nothing to request documentation on + + if p == DD_HEADERS: # add requestsa as per the parameters + doc_set += [D_HEADERS] + elif p == DD_COMMANDS: + doc_set += [D_COMMANDS] + elif p == DD_SUBROUTINES: + doc_set += [D_SUBROUTINES] + elif p == DD_BUTTONS: + doc_set += [D_BUTTONS] + elif p == DD_COMMAND_BASE: + doc_set += [D_COMMAND_BASE] + elif p == DD_DEBUG: + doc_set += [D_DEBUG] + elif p == DD_SOURCE: + doc_set += [D_SOURCE] + + if (set(doc_set) - {D_DEBUG, D_SOURCE}) == set({}): # if only modifiers have been specified + doc_set = DS_NORMAL + doc_set # add them to teh "normal" documentation + + scripts.Dump_commands(doc_set) # print documentation + + + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + for i in range(len(split_line)-1): + if not split_line[i+1] in DDG_ALL: # invalid subcommand + c_ok = ', '.join(DDG_ALL[:-1]) + ', or ' + DDG_ALL[-1] + s_err = f"Invalid subcommand {split_line[i+1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret + + +scripts.Add_command(DOC_Document()) # register the command diff --git a/commands_file.py b/commands_file.py index 9a68b43..2e2271f 100644 --- a/commands_file.py +++ b/commands_file.py @@ -25,6 +25,8 @@ def __init__( (1, " returns user's home dir in {1}"), ) ) + self.doc = ["Returns the filly qualified path of the user's home directory."] + def Process(self, btn, idx, split_line): self.Set_param(btn, 1, pathlib.Path.home()) # return the path @@ -43,25 +45,149 @@ def __init__( self, ): - super().__init__("F_DEL, Returns the user's home directory", + super().__init__("F_DELETE, Deletes a file", LIB, ( # Desc Opt Var type p1_val p2_val + ("OK", False, AVV_REQD, PT_INT, None, None), ("File", False, AVV_YES, PT_STR, None, None), ), ( # num params, format string (trailing comma is important) - (1, " Deletes {1}"), + (2, " Deletes {1}"), ) ) + self.doc = ["Attempts to delete the file specified in parameter 2 (File) and returns 1", \ + "in parameter 1 (OK) if the delete suceeds, otherwise returns 0. Note that", \ + "0 will be returned if the file did not exist prior to attempted deletion"] + def Process(self, btn, idx, split_line): - file = self.Get_param(btn, 1) + file = self.Get_param(btn, 2) try: - os.remove(self.Get_param(btn, 1, file)) # delete the file + os.remove(file) # delete the file + self.Set_param(btn, 1, 1) except: traceback.print_exc() + self.Set_param(btn, 1, 0) scripts.Add_command(File_Delete()) # register the command +# ################################################## +# ### CLASS FILE_FILE_EXISTS ### +# ################################################## + +# class that defines the F_FILE_EXISTS command -- confirms the existance of a file +class File_File_Exists(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("F_FILE_EXISTS, Determines if a file exists", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Exists", False, AVV_REQD, PT_INT, None, None), + ("File", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Returns 1 in {1} if {2} exists as a file, else returns 0"), + ) ) + + self.doc = ["Returns 1 in parameter 1 (Exists) if the fully specified file (includes path)", \ + "passed in parameter 2 (File) exists AND is a file, otherwise returns 0."] + + + def Process(self, btn, idx, split_line): + file = self.Get_param(btn, 2) + try: + self.Set_param(btn, 1, int(os.path.exists(file) and os.path.isfile(file))) # Check existance of file + except: + traceback.print_exc() + +scripts.Add_command(File_File_Exists()) # register the command + + +# ################################################## +# ### CLASS FILE_PATH_EXISTS ### +# ################################################## + +# class that defines the F_PATH_EXISTS command -- confirms the existance of a file +class File_Path_Exists(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("F_PATH_EXISTS, Determines if a path exists", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Exists", False, AVV_REQD, PT_INT, None, None), + ("Path", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Returns 1 in {1} if {2} exists as a path, else returns 0"), + ) ) + + self.doc = ["Returns 1 in parameter 1 (Exists) if the path specified in parameter", \ + "2 (Path) exists AND is a directory, otherwise returns 0."] + + + def Process(self, btn, idx, split_line): + path = self.Get_param(btn, 2) + try: + self.Set_param(btn, 1, int(os.path.exists(path) and os.path.isdir(file))) # Check existance of path + except: + traceback.print_exc() + +scripts.Add_command(File_Path_Exists()) # register the command + + +# ################################################## +# ### CLASS FILE_ENSURE_PATH_EXISTS ### +# ################################################## + +# class that defines the F_ENSURE_PATH_EXISTS command -- checks for the existance of a path, creating it if necessary +class File_Ensure_Path_Exists(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("F_ENSURE_PATH_EXISTS, Ensures a path exists by creating it if it doesn't", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("OK", False, AVV_REQD, PT_INT, None, None), + ("Path", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (2, " Returns 1 in {1} if {2} exists as a path (or could be created), else returns 0"), + ) ) + + self.doc = ["Ensures the path passed exists by first checking for its existance, then", \ + "attempting to create the path if it does not exist.", \ + "", \ + "Returns 1 in the first parameter (OK) if the path existed or was", \ + "sucessfully created, otherwise returns 0."] + + + def Process(self, btn, idx, split_line): + path = self.Get_param(btn, 2) + ok = 0 + try: + if not os.path.exists(path): # if it doesn't exist + os.makedirs(path) # make it exist + ok = 1 # success! + except: + traceback.print_exc() + + self.Set_param(btn, 1, ok) + + +scripts.Add_command(File_Ensure_Path_Exists()) # register the command + + diff --git a/commands_scrape.py b/commands_scrape.py index c45e852..f606f0e 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -36,10 +36,18 @@ def get_image(self, hwnd, p_from, p_to): # scrapes an image from the clipboard - def get_copied_image(self): - image = PIL.ImageGrab.grabclipboard() # grab the image from he clipboard - - return image # return the image + def get_copied_image(self, btn): + tries = 5 + while tries >= 0: + try: + image = PIL.ImageGrab.grabclipboard() # grab the image from he clipboard + return image + except: + tries -= 1 + btn.safe_sleep(0.5) + print("retry") + + return -1 # return the image # ################################################## @@ -149,7 +157,7 @@ def __init__( def Process(self, btn, idx, split_line): - image = self.get_copied_image() # get clipboard image + image = self.get_copied_image(btn) # get clipboard image self.Set_param(btn, 1, image) # pass the text back @@ -181,7 +189,7 @@ def __init__( def Process(self, btn, idx, split_line): - image = self.get_copied_image() # get copied image + image = self.get_copied_image(btn) # get copied image txt = pytesseract.image_to_string(image) # OCR the image diff --git a/commands_win32.py b/commands_win32.py index 62c8e7d..8435bd1 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -570,10 +570,13 @@ def Process(self, btn, idx, split_line): if self.Param_count(btn) > 0: # place variable into clipboard if required hwnd = win32gui.GetForegroundWindow() # get the current window - c = self.Get_param(btn, 1) # get the value + ClearClipboard() + SetClipboard(str(c)) + + # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste try: kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) kb.tap(kb.sp('v')) diff --git a/constants.py b/constants.py index cba2605..81482aa 100644 --- a/constants.py +++ b/constants.py @@ -118,11 +118,13 @@ LM_RUN = "run" # Dump constants -D_HEADERS = 1 -D_COMMANDS = 2 -D_SUBROUTINES = 3 -D_BUTTONS = 4 -D_DEBUG = 5 +D_HEADERS = 1 # produce documentation for headers +D_COMMANDS = 2 # produce documentation for commands +D_SUBROUTINES = 3 # produce documentation for subroutines +D_BUTTONS = 4 # produce documentation for buttons +D_COMMAND_BASE = 5 # produce documentation for routines used in the creation of commands +D_DEBUG = 6 # add debug info where available +D_SOURCE = 7 # add source where available DS_NORMAL = [D_HEADERS, D_COMMANDS, D_SUBROUTINES, D_BUTTONS] diff --git a/files.py b/files.py index 6a8daba..65a4210 100644 --- a/files.py +++ b/files.py @@ -266,7 +266,7 @@ def validate_all_buttons(): window.Redraw(True) if b_fail > 0: - window.app.popup(window.app, "Buttons in disabled", window.app.info_image, f"{b_fail} of {b_tot} buttons disabled due to errors", "OK") + window.app.popup(window.app, "Buttons disabled", window.app.info_image, f"{b_fail} of {b_tot} buttons disabled due to errors", "OK") # load a single subroutine def load_subroutine(sub, sub_n, fname): diff --git a/scripts.py b/scripts.py index 2ed7d1a..8f36d82 100644 --- a/scripts.py +++ b/scripts.py @@ -138,12 +138,24 @@ def dump_params(c): else: print(" UNKNOWN VALUE") + def dump_source(c): + if isinstance(c, commands_subroutines.Subroutine): + print(" Source") + for line in c.routine: + print(f" {line}") + elif isinstance(c, Button): + print(" Source") + for line in c.script_lines: + print(f" {line}") + def dump(c_type, c, style): dump_name(c_type, c) dump_doc(c) if D_DEBUG in style: dump_ancestory(c) dump_params(c) + if D_SOURCE in style: + dump_source(c) print() @@ -169,7 +181,7 @@ def dump(c_type, c, style): print() for cmd in VALID_COMMANDS: if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): - dump("Subroutine", VALID_COMMANDS[cmd], style) + dump("Subroutine", VALID_COMMANDS[cmd], style) if D_BUTTONS in style: print("BUTTONS") From e559585ecdc5e29a826a31260470ab81e7d248e5 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 31 Mar 2021 20:15:20 +0800 Subject: [PATCH 70/83] Mostly bug fixes (kinda) * commands_rpncalc.py - New commands to allow math with dates * commands_scrape.py 2 bugs fixed. safe_sleep -> Safe_sleep, and passing wrong parameter back * commands_win32.py adding ability to search for windows based on a regex, and detecting the size of a window * INSTALL/* adding the extra modules I'm using. One serious bug remains. Passing a string to the IF commands passes the value incorrectly. The workaround is to assign the value to a string variable and pass that. The fix will involve changes to the parameter passing code. --- INSTALL/environment-build.yml | 1 + INSTALL/environment.yml | 1 + INSTALL/requirements.txt | 1 + commands_rpncalc.py | 29 ++++++++- commands_scrape.py | 4 +- commands_win32.py | 108 +++++++++++++++++++++++++++++++++- 6 files changed, 138 insertions(+), 6 deletions(-) diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index c5df65a..84dd0ac 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -20,3 +20,4 @@ dependencies: - dhash - selenium - pyperclip + - python-dateutils \ No newline at end of file diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 4700ab5..7cbec81 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -19,3 +19,4 @@ dependencies: - dhash - selenium - pyperclip + - python-dateutils diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index ca20d8f..b3fbf3f 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -20,4 +20,5 @@ ImageHash==4.2.0 dhash==1.3 selenium==3.141.0 pyperclip=1.8.0 +python-dateutils==2.8.1 -e git+git://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py diff --git a/commands_rpncalc.py b/commands_rpncalc.py index d14dc74..f03a6a5 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -1,4 +1,5 @@ -import command_base, lp_events, scripts, variables, sys, param_convs +import command_base, lp_events, scripts, variables, sys, param_convs, datetime +from dateutil import parser from constants import * LIB = "cmds_rpnc" # name of this library (for logging) @@ -179,6 +180,8 @@ def Register_operators(self): self.operators["!?G"] = (self.is_global_not_def, 1) # is global var not defined self.operators["ABORT"] = (self.abort_script, 0) # abort the script (not just the rpn calc) self.operators["SUBSTR"] = (self.substr, 0) # x gets str(z)[x:y] + self.operators["D>J"] = (self.d_to_j, 0) # converts string date to julian (actually ordinal) + self.operators["J>D"] = (self.j_to_d, 0) # converts a julian to a text date def add(self, @@ -676,6 +679,30 @@ def substr(self, symbols, cmd, cmds): return 1 + def d_to_j(self, symbols, cmd, cmds): + # converts a text date on the top of the stack to a julian date (integer) + d = variables.pop(symbols) + dt = parser.parse(d) + + j = dt.toordinal() + + variables.push(symbols, j) + + return 1 + + + def j_to_d(self, symbols, cmd, cmds): + # converts a julian date (integer) on the top of the stack to a text date + j = variables.pop(symbols) + dt = datetime.date.fromordinal(j) + + d = dt.strftime("%d-%b-%Y") + + variables.push(symbols, d) + + return 1 + + scripts.Add_command(Rpn_Eval()) # register the command diff --git a/commands_scrape.py b/commands_scrape.py index f606f0e..cb5d127 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -44,7 +44,7 @@ def get_copied_image(self, btn): return image except: tries -= 1 - btn.safe_sleep(0.5) + btn.Safe_sleep(0.5) print("retry") return -1 # return the image @@ -322,7 +322,7 @@ def Process(self, btn, idx, split_line): fingerprint = int(str(imagehash.dhash(image)),16) # calculate an image fingerprint - self.Set_param(btn, 5, fingerprint) # pass the hash back + self.Set_param(btn, 2, fingerprint) # pass the hash back scripts.Add_command(Scrape_Image_Fingerprint()) # register the command diff --git a/commands_win32.py b/commands_win32.py index 8435bd1..8af1a98 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -1,5 +1,5 @@ # This module is VERY specific to Win32 -import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard, win32event +import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard, win32event, re from constants import * LIB = "cmds_wn32" # name of this library (for logging) @@ -356,8 +356,11 @@ def Process(self, btn, idx, split_line): def CheckWindow(hwnd, data): # callback function to receive enumerated window handles - if win32gui.GetWindowText(hwnd)[:len(data['title'])] == data['title']: # does the beginning of the title match? - data['hwnds'] += [hwnd] # add to list + try: + if win32gui.GetWindowText(hwnd)[:len(data['title'])] == data['title']: # does the beginning of the title match? + data['hwnds'] += [hwnd] # add to list + except: + pass # ignore errors (probably a bad regular expression) hwnds = [] # reset the list of window handles title = self.Get_param(btn, 1) # get the title we're searching for @@ -383,6 +386,67 @@ def CheckWindow(hwnd, data): scripts.Add_command(Win32_Similar_Hwnd()) # register the command +# ################################################## +# ### CLASS W_REGEX_HWND ### +# ################################################## + +# class that defines the W_REGEX_HWND command - returns the nth matching window handle +class Win32_Regex_Hwnd(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_REGEX_HWND, Returns the handle of the nth pattern-matched window", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Regex", False, AVV_YES, PT_STR, None, None), # regular expression search for + ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND + ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M Date: Tue, 6 Apr 2021 16:54:37 +0800 Subject: [PATCH 71/83] Oopsie in the install files --- INSTALL/environment-build.yml | 2 +- INSTALL/environment.yml | 2 +- INSTALL/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index 84dd0ac..b8ae4db 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -20,4 +20,4 @@ dependencies: - dhash - selenium - pyperclip - - python-dateutils \ No newline at end of file + - python-dateutil \ No newline at end of file diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 7cbec81..7ac3d1d 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -19,4 +19,4 @@ dependencies: - dhash - selenium - pyperclip - - python-dateutils + - python-dateutil diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index b3fbf3f..7595c0d 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -20,5 +20,5 @@ ImageHash==4.2.0 dhash==1.3 selenium==3.141.0 pyperclip=1.8.0 -python-dateutils==2.8.1 +python-dateutil==2.8.1 -e git+git://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py From e6a68773f107c0c74d606950681caccf41025b96 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Mon, 12 Apr 2021 16:18:03 +0800 Subject: [PATCH 72/83] Way better documentation. Also notification of deprecated commands * command_base.py - Added fields for command deprecation * commands_browser_automation.py - Improved documentation * commands_control.py - Added new IF command * commands_control.py - Deprecated old if commands * commands_control.py - Added Assert command * commands_dialog.py - Improved documentation * commands_documentation.py - Improved access to documentation * commands_file.py - Improved documentation * commands_header.py - improved documentation * commands_header.py - Added @DOC+ header to better allow for text wrapping * commands_rpncalc.py - Added documentation (including autogeneration) * commands_scrape.py - slight improvement for documentation * commands_subroutines.py - move the documentation to the right place! * commands_test.py - mark all commands as deprecated * commands_win32.py - improved W_COPY command * commands_win32.py - improved documentation * commands_win32.py - new command to list all windows * constants.py - minor change to add documentation command to hide stuff you don't need to see * files.py - automatically remove blank lines at the beginning and end of subroutines * INSTALL/* - fixups for required libraries * scripts.py - smarter documentation routine to automatically wrap most text * scripts.py - Allow documentation lines in subroutines and buttons to be skipped when printing routine * window.py - Modification to work with newer python versions --- INSTALL/environment-build.yml | 5 +- INSTALL/environment.yml | 3 +- INSTALL/requirements.txt | 5 +- command_base.py | 3 + commands_browser_automation.py | 60 +++++---- commands_control.py | 237 ++++++++++++++++++++++++++++++--- commands_dialog.py | 16 +-- commands_documentation.py | 40 ++++-- commands_file.py | 18 +-- commands_header.py | 131 +++++++++++++++--- commands_rpncalc.py | 133 +++++++++++++++++- commands_scrape.py | 8 +- commands_subroutines.py | 165 ++++++++++++----------- commands_test.py | 37 ++++- commands_win32.py | 101 ++++++++++---- constants.py | 1 + files.py | 7 + scripts.py | 82 +++++++++--- window.py | 4 +- 19 files changed, 829 insertions(+), 227 deletions(-) diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index b8ae4db..074398d 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -6,7 +6,6 @@ dependencies: - pip - tk - pip: - - git+git://github.com/FMMT666/launchpad.py.git@master - pillow - pygame - pynput @@ -20,4 +19,6 @@ dependencies: - dhash - selenium - pyperclip - - python-dateutil \ No newline at end of file + - python-dateutil + - launchpad-py + - launchpad diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 7ac3d1d..0d95756 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -6,7 +6,6 @@ dependencies: - pip - tk - pip: - - git+git://github.com/FMMT666/launchpad.py.git@master - pillow - pygame - pynput @@ -20,3 +19,5 @@ dependencies: - selenium - pyperclip - python-dateutil + - launchpad-py + - launchpad diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index 7595c0d..9024173 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -19,6 +19,7 @@ pytesseract==0.3.7 ImageHash==4.2.0 dhash==1.3 selenium==3.141.0 -pyperclip=1.8.0 +pyperclip==1.8.0 python-dateutil==2.8.1 --e git+git://github.com/FMMT666/launchpad.py.git@master#egg=launchpad-py +launchpad-py +launchpad diff --git a/command_base.py b/command_base.py index 5f83d91..4e1f091 100644 --- a/command_base.py +++ b/command_base.py @@ -41,6 +41,9 @@ def __init__( self.run_states = [RS_INIT, RS_GET, RS_INFO, RS_VALIDATE, RS_RUN, RS_FINAL] # by default we'll do everything if you don't override self.validation_states = [VS_COUNT, VS_PASS_1, VS_PASS_2] # by default we'll do a count and both passes if you don't override + self.deprecated = False # by default, commands are not deprecated + self.deprecated_use = "" # allow text to specify a replacement + def validate_init(self): # This helps validate the parameters passed to __init__. It helps enforce the rules diff --git a/commands_browser_automation.py b/commands_browser_automation.py index bf4ad12..f252c56 100644 --- a/commands_browser_automation.py +++ b/commands_browser_automation.py @@ -46,11 +46,11 @@ def __init__( (2, " Open browser {1} for automation as {2}"), ) ) - self.doc = ["Starts a browser using selinium for automated control. The return will", + self.doc = ["Starts a browser using selinium for automated control. The return will " "be an object if the call suceeds, otherwise it will return -1.", "", - "NOTE 1: The first parameter should be used to select what browser you", - "want to load. This is implemented, but has only been tested with Chrome.", + "NOTE 1: The first parameter should be used to select what browser you " + "want to load. This is implemented, but has only been tested with Chrome. " "The values that can be used are:", "", " {BAS_CHROME}", @@ -63,8 +63,8 @@ def __init__( " {BAS_WEBKITGTK}", " {BAS_REMOTE}", "", - "NOTE 2: This blocks while the browser loads, so it should use a", - "similar technique to the dialog boxes to pass this processing off", + "NOTE 2: This blocks while the browser loads, so it should use a " + "similar technique to the dialog boxes to pass this processing off " "to another thread."] @@ -134,8 +134,8 @@ def __init__( self.doc = ["Navigates an existing browser to a new URL.", "", - "NOTE 1: This blocks while the browser loads the page, so it should use", - "a similar technique to the dialog boxes to pass this processing off to", + "NOTE 1: This blocks while the browser loads the page, so it should use " + "a similar technique to the dialog boxes to pass this processing off to " "another thread."] @@ -174,7 +174,7 @@ def __init__( self.doc = ["Closes an existing browser.", "", - "NOTE 1: You should probably clear the variable that held the browser", + "NOTE 1: You should probably clear the variable that held the browser " "object after you call this."] @@ -213,7 +213,7 @@ def __init__( self.doc = ["Returns the URL the existing browser is displaying.", "", - "NOTE 1: This can fail (returning a blank string) if the browser is", + "NOTE 1: This can fail (returning a blank string) if the browser is " "still loading the page."] @@ -267,26 +267,29 @@ def __init__( (4, " Return element {3} from {1} into {4} using method {2}"), ) ) - self.doc = ["Returns an element at the location. Often this will be used to extract" + self.doc = ["Returns an element at the location. Often this will be used to extract " "tables for further processing.", - "" - "The first parameter `Auto` is weither an browser object or an element", + "", + "The first parameter `Auto` is weither an browser object or an element " "returned from a successful search.", "", "The `Method` can be:", + "", + "~19", " XPATH - finds element by xpath", " NAME - finds element by name", " TAG_NAME - finds element by tag name", " ID - finds element by its id", " LINK_TEXT - finds element by its link text", " CLASS_NAME - finds element by class name", + "~", "", "See selenium documentation for information about xpaths.", "", - "The third parameter `Search` is the search string. Thhe format of this", + "The third parameter `Search` is the search string. Thhe format of this " "depends on the search method.", - "" - "The final parameter `Element` is the element returned from the search. If", + "", + "The final parameter `Element` is the element returned from the search. If " "The search fails, -1 will be returned."] @@ -341,19 +344,20 @@ def __init__( (3, " Return dimensions of table {1} into ({2}, {3})"), ) ) - self.doc = ["Returns the number of rows and columns for a table. This table should" - "have previously been obtained by fetching a table element from a loaded" + self.doc = ["Returns the number of rows and columns for a table. This table should " + "have previously been obtained by fetching a table element from a loaded " "page.", "", - "The first parameter `Table` is an element returned from a search. it is", + "The first parameter `Table` is an element returned from a search. it is " "unlikely that any object other than a table will produce sensible results.", "", - "The result `Rows` will be the number of rows in the table. -1, will be", + "The result `Rows` will be the number of rows in the table. -1, will be " "returned in case of error.", "", - "the result `Cols` will be the number of columns in the 0th row of the table.", - "Note that because HTML tables can have a variable number of columns, it", - "cannot be assumed that all rows will have this number of columns. -1 will", + "The result `Cols` will be the number of columns in the 0th row of the table.", + "", + "Note that because HTML tables can have a variable number of columns, it " + "cannot be assumed that all rows will have this number of columns. -1 will " "be returned in case of error."] @@ -402,14 +406,14 @@ def __init__( (4, " Return cell ({2}, {3}) from {1} into {4}"), ) ) - self.doc = ["The first parameter `Table` is an element returned from a search. it is", + self.doc = ["The first parameter `Table` is an element returned from a search. it is " "unlikely that any object other than a table will produce sensible results.", "", "The `Row` and `Col` parameters specify the 0-based offset in " "have previously been obtained by fetching a table element from a loaded" "page.", "", - "The parameter `Cell` will contain the cell from the table. -1 will be", + "The parameter `Cell` will contain the cell from the table. -1 will be " "returned in case of error."] @@ -466,7 +470,7 @@ def __init__( self.doc = ["The first parameter `Element` is an element returned from a search.", "", - "The `Text` parameters will be populated the text of the element. -1" + "The `Text` parameters will be populated the text of the element. -1 " "will be returned in case of error."] @@ -551,12 +555,14 @@ def __init__( self.doc = ["The first parameter `Element` is an element returned from a search.", "", - "This method will send text to that element. Note that there are a few special", + "This method will send text to that element. Note that there are a few special " "escape characters:", "", - " `\n` - will be replaced with a newline,", + "~15", + " `\\n` - will be replaced with a newline,", " `\h` - will be replaced with a press of the home key", " `\^` - will be replaced with a press of the arrow up key", + "~", "", "There is no return value."] diff --git a/commands_control.py b/commands_control.py index 4f81985..38ef14b 100644 --- a/commands_control.py +++ b/commands_control.py @@ -224,9 +224,9 @@ def either_is(a, b, c_type): None return a, b - def a_eq_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_eq_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -238,9 +238,9 @@ def a_eq_b(self, btn): return False - def a_ne_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_ne_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -252,9 +252,9 @@ def a_ne_b(self, btn): return True # this is an exception. If we can't compare they can't be equal! - def a_gt_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_gt_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -266,9 +266,9 @@ def a_gt_b(self, btn): return False - def a_lt_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_lt_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -280,9 +280,9 @@ def a_lt_b(self, btn): return False - def a_ge_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_ge_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -294,9 +294,9 @@ def a_ge_b(self, btn): return False - def a_le_b(self, btn): - a = self.Get_param(btn, 2) - b = self.Get_param(btn, 3) + def a_le_b(self, btn, first=2, second=3): + a = self.Get_param(btn, first) + b = self.Get_param(btn, second) a, b = self.Comparable(a, b) # try our best to make a and b comparable @@ -334,6 +334,189 @@ def __init__( scripts.Add_command(Control_Goto_Label()) # register the command +# ################################################## +# ### CLASS IF ### +# ################################################## + +# constants for comparisons +COMP_EQ = ['EQ', '==', '='] +COMP_NE = ['NE', "!=", "<>"] +COMP_GT = ['GT', '>'] +COMP_GE = ['GE', '>='] +COMP_LT = ['LT', '<'] +COMP_LE = ['LE', '<='] + +COMPG_ALL = COMP_EQ + COMP_NE + COMP_GT + COMP_GE + COMP_LT + COMP_LE + +# constants for action +ACT_GOTO = ['GOTO'] +ACT_RETURN = ['RETURN'] +ACT_END = ['END'] +ACT_ABORT = ['ABORT'] + +ACTG_ALL = ACT_GOTO + ACT_RETURN + ACT_END + ACT_ABORT + +# class that defines the IF command +class Control_If(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "IF, IF x comp y GOTO label", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("A", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! + ("comp", False, AVV_NO, PT_WORD, None, None), # comparison operator + ("B", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! + ("Action", False, AVV_NO, PT_WORD, None, None), # GOTO, ABORT, RETURN, END + ("Label", True, AVV_NO, PT_LABEL, None, None), # required for GOTO + ), + ( + # num params, format string (trailing comma is important) + (4, " if {1} {2} {3} then {4}"), + (5, " if {1} {2} {3} then {4} {5}"), + ), + ) + + + def Process(self, btn, idx, split_line): + comp = self.Get_param(btn, 2) + + if comp in COMP_EQ: + comp_p = self.a_eq_b + elif comp in COMP_NE: + comp_p = self.a_ne_b + elif comp in COMP_GE: + comp_p = self.a_ge_b + elif comp in COMP_GT: + comp_p = self.a_gt_b + elif comp in COMP_LE: + comp_p = self.a_le_b + elif comp in COMP_LT: + comp_p = self.a_lt_b + + res = comp_p(btn, 1) + + if not res: + return idx+1 + + act = self.Get_param(btn, 4) + + if act in ACT_RETURN: + return -1 + elif act in ACT_END: + btn.root.thread.kill.set() + return -1 + elif act in ACT_ABORT: + btn.root.thread.kill.set() + return -1 + else: + # perform a goto + ret = btn.symbols[SYM_LABELS][btn.symbols[SYM_PARAMS][5]] + return ret + + + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if not split_line[2] in COMPG_ALL: # invalid subcommand + c_ok = ', '.join(COMPG_ALL[:-1]) + ', or ' + COMPG_ALL[-1] + s_err = f"Invalid comparison operator {split_line[2]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + + if not split_line[4] in ACTG_ALL: # invalid subcommand + c_ok = ', '.join(ACTG_ALL[:-1]) + ', or ' + ACTG_ALL[-1] + s_err = f"Invalid action {split_line[4]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + + if (split_line[4] in ACT_GOTO) and (len(split_line) < 6): + s_err = f"{split_line[4]} requires a label." + return (s_err, btn.Line(idx)) + + if not (split_line[4] in ACT_GOTO) and (len(split_line) >= 6): + s_err = f"{split_line[4]} can not have a label ({split_line[6]})." + return (s_err, btn.Line(idx)) + + return ret + + +scripts.Add_command(Control_If()) # register the command + + +# ################################################## +# ### CLASS ASSERT ### +# ################################################## + +# class that defines the ASSERT command +class Control_Assert(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "ASSERT, IF x comp y is not true, abort with message", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("A", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! + ("comp", False, AVV_NO, PT_WORD, None, None), # comparison operator + ("B", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! + ("Message", True, AVV_NO, PT_STR, None, None), # abort message + ), + ( + # num params, format string (trailing comma is important) + (3, " if {1} {2} {3} fails, then abort"), + (4, " if {1} {2} {3} fails, then abort with message {4}"), + ), + ) + + + def Process(self, btn, idx, split_line): + comp = self.Get_param(btn, 2) + + if comp in COMP_EQ: + comp_p = self.a_eq_b + elif comp in COMP_NE: + comp_p = self.a_ne_b + elif comp in COMP_GE: + comp_p = self.a_ge_b + elif comp in COMP_GT: + comp_p = self.a_gt_b + elif comp in COMP_LE: + comp_p = self.a_le_b + elif comp in COMP_LT: + comp_p = self.a_lt_b + + res = comp_p(btn, 1) + + if res: + return idx+1 + + message = self.Get_param(btn, 4) + print(f'ASSERT `{self.Get_param(btn, 1)}` {comp} `{self.Get_param(btn, 3)}` fails: {message}') + + btn.root.thread.kill.set() + return -1 + + + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if not split_line[2] in COMPG_ALL: # invalid subcommand + c_ok = ', '.join(COMPG_ALL[:-1]) + ', or ' + COMPG_ALL[-1] + s_err = f"Invalid comparison operator {split_line[2]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + + return ret + + +scripts.Add_command(Control_Assert()) # register the command + + # ################################################## # ### CLASS IF_EQ_GOTO ### # ################################################## @@ -361,6 +544,9 @@ def __init__( self.a_eq_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Eq_Goto()) # register the command @@ -392,6 +578,9 @@ def __init__( self.a_ne_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Ne_Goto()) # register the command @@ -423,6 +612,9 @@ def __init__( self.a_gt_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Gt_Goto()) # register the command @@ -454,6 +646,9 @@ def __init__( self.a_gt_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Ge_Goto()) # register the command @@ -485,6 +680,9 @@ def __init__( self.a_lt_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Lt_Goto()) # register the command @@ -516,6 +714,9 @@ def __init__( self.a_le_b ) + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more flexible `IF` command instead." + scripts.Add_command(Control_If_Le_Goto()) # register the command diff --git a/commands_dialog.py b/commands_dialog.py index 0c0530b..679f354 100644 --- a/commands_dialog.py +++ b/commands_dialog.py @@ -28,9 +28,9 @@ def __init__( (3, " Dialog OK/Cancel '{1}'"), ) ) - self.doc = ["A simple dialog with a title, message and OK/Cancel buttons. Closing the", \ - "window is treated the same as cancel. If a return variable is specified," \ - "contain 1 for OK, and 0 for cancel. If no variable is passed for the "\ + self.doc = ["A simple dialog with a title, message and OK/Cancel buttons. Closing the " + "window is treated the same as cancel. If a return variable is specified, " + "contain 1 for OK, and 0 for cancel. If no variable is passed for the " "return value, a cancel will result in a button abort."] @@ -76,8 +76,8 @@ def __init__( (2, " Info dialog '{1}'"), ) ) - self.doc = ["A simple dialog with a title, message and OK button. No return value" \ - "is required since the message only requires acknowledgement. This" \ + self.doc = ["A simple dialog with a title, message and OK button. No return value " + "is required since the message only requires acknowledgement. This " "will never cause an abort"] @@ -111,9 +111,9 @@ def __init__( (3, " Error dialog '{1}' returning {3}"), ) ) - self.doc = ["A simple dialog with a title, message and Cancel button. Typically" \ - "this should be called without a return variable to allow the script" \ - "to abort. If a return value is specified, the script will continue" \ + self.doc = ["A simple dialog with a title, message and Cancel button. Typically " + "this should be called without a return variable to allow the script " + "to abort. If a return value is specified, the script will continue " "after the dialog is dismissed."] diff --git a/commands_documentation.py b/commands_documentation.py index 9a7f85a..1db6cca 100644 --- a/commands_documentation.py +++ b/commands_documentation.py @@ -13,10 +13,12 @@ DD_SUBROUTINES = "SUBROUTINES" DD_BUTTONS = "BUTTONS" DD_COMMAND_BASE = "COMMAND_BASE" +DD_ALL = "ALL" DD_DEBUG = "DEBUG" DD_SOURCE = "SOURCE" +DD_NO_SRC_DOC = "NO_SRC_DOC" -DDG_ALL = [DD_HEADERS, DD_COMMANDS, DD_SUBROUTINES, DD_BUTTONS, DD_COMMAND_BASE, DD_DEBUG, DD_SOURCE] +DDG_ALL = [DD_HEADERS, DD_COMMANDS, DD_SUBROUTINES, DD_BUTTONS, DD_COMMAND_BASE, DD_ALL, DD_DEBUG, DD_SOURCE, DD_NO_SRC_DOC] DDG_DEFAULT = [DD_HEADERS, DD_COMMANDS, DD_SUBROUTINES, DD_BUTTONS] # class that defines more the DOCUMENT command that outputs documentation @@ -39,25 +41,33 @@ def __init__( self.doc = ["Prints documentation about LPHK to standard output." "" - "Any number of valid parameters can be passed. These can be a", - "combination of 'category' parameters (that define the subset(s)", - "of documentation to be printed, and 'modifier' parameters that", + "Any number of valid parameters can be passed. These can be a " + "combination of 'category' parameters (that define the subset(s) " + "of documentation to be printed, and 'modifier' parameters that " "alter how the documentation is produced.", "", - "If no parameters are passed, a standard output is produced. If", - "the only parameters passed are 'modifier' parameters, they modify", + "If no parameters are passed, a standard output is produced. If " + "the only parameters passed are 'modifier' parameters, they modify " "the standard output.", "", "The `category` parameters cause documentation to be created for:", + "", + "~21", " HEADERS - commands starting with '@' used in scripts.", " COMMANDS - regular macro commands", " SUBROUTINES - user-defined subroutines", " BUTTONS - button scripts", " COMMAND_BASE - (not yet implemented) routines used when writing commands", + " ALL - create all documentation (with all modifiers)", + "~", "", "The `modifier` parameters change the way documentation is produced:", + "", + "~21", " DEBUG - includes type ancestory for commands.", " SOURCE - includes source for buttons and subroutines", + " NO_SRC_DOC - hide source lines used for documentation generation", + "~", "", "The default categories are HEADERS COMMANDS SUBROUTINES BUTTONS"] @@ -70,22 +80,24 @@ def Process(self, btn, idx, split_line): doc_set = [] # start with nothing to request documentation on - if p == DD_HEADERS: # add requestsa as per the parameters + if p in [DD_HEADERS, DD_ALL]: # add requestsa as per the parameters doc_set += [D_HEADERS] - elif p == DD_COMMANDS: + if p in [DD_COMMANDS, DD_ALL]: doc_set += [D_COMMANDS] - elif p == DD_SUBROUTINES: + if p in [DD_SUBROUTINES, DD_ALL]: doc_set += [D_SUBROUTINES] - elif p == DD_BUTTONS: + if p in [DD_BUTTONS, DD_ALL]: doc_set += [D_BUTTONS] - elif p == DD_COMMAND_BASE: + if p in [DD_COMMAND_BASE, DD_ALL]: doc_set += [D_COMMAND_BASE] - elif p == DD_DEBUG: + if p in [DD_DEBUG, DD_ALL]: doc_set += [D_DEBUG] - elif p == DD_SOURCE: + if p in [DD_SOURCE, DD_ALL]: doc_set += [D_SOURCE] + if p in [DD_NO_SRC_DOC, DD_ALL]: + doc_set += [D_NO_SRC_DOC] - if (set(doc_set) - {D_DEBUG, D_SOURCE}) == set({}): # if only modifiers have been specified + if (set(doc_set) - {D_DEBUG, D_SOURCE, DD_NO_SRC_DOC}) == set({}): # if only modifiers have been specified doc_set = DS_NORMAL + doc_set # add them to teh "normal" documentation scripts.Dump_commands(doc_set) # print documentation diff --git a/commands_file.py b/commands_file.py index 2e2271f..2b08398 100644 --- a/commands_file.py +++ b/commands_file.py @@ -57,9 +57,9 @@ def __init__( (2, " Deletes {1}"), ) ) - self.doc = ["Attempts to delete the file specified in parameter 2 (File) and returns 1", \ - "in parameter 1 (OK) if the delete suceeds, otherwise returns 0. Note that", \ - "0 will be returned if the file did not exist prior to attempted deletion"] + self.doc = ["Attempts to delete the file specified in parameter 2 (File) and returns 1 " + "in parameter 1 (OK) if the delete suceeds, otherwise returns 0. Note that " + "0 will be returned if the file did not exist prior to attempted deletion"] def Process(self, btn, idx, split_line): @@ -96,7 +96,7 @@ def __init__( (2, " Returns 1 in {1} if {2} exists as a file, else returns 0"), ) ) - self.doc = ["Returns 1 in parameter 1 (Exists) if the fully specified file (includes path)", \ + self.doc = ["Returns 1 in parameter 1 (Exists) if the fully specified file (includes path) " "passed in parameter 2 (File) exists AND is a file, otherwise returns 0."] @@ -132,7 +132,7 @@ def __init__( (2, " Returns 1 in {1} if {2} exists as a path, else returns 0"), ) ) - self.doc = ["Returns 1 in parameter 1 (Exists) if the path specified in parameter", \ + self.doc = ["Returns 1 in parameter 1 (Exists) if the path specified in parameter " "2 (Path) exists AND is a directory, otherwise returns 0."] @@ -168,10 +168,10 @@ def __init__( (2, " Returns 1 in {1} if {2} exists as a path (or could be created), else returns 0"), ) ) - self.doc = ["Ensures the path passed exists by first checking for its existance, then", \ - "attempting to create the path if it does not exist.", \ - "", \ - "Returns 1 in the first parameter (OK) if the path existed or was", \ + self.doc = ["Ensures the path passed exists by first checking for its existance, then " + "attempting to create the path if it does not exist.", + "", + "Returns 1 in the first parameter (OK) if the path existed or was " "sucessfully created, otherwise returns 0."] diff --git a/commands_header.py b/commands_header.py index aa5bcfe..85e0d13 100644 --- a/commands_header.py +++ b/commands_header.py @@ -179,40 +179,40 @@ def __init__( super().__init__("@NAME, Names a button") - self.doc = ["The @NAME header defines a name for a script. This name is also", - "displayed on the LPHK form as annotation for the button the script", + self.doc = ["The @NAME header defines a name for a script. This name is also " + "displayed on the LPHK form as annotation for the button the script " "is assigned to.", "", "A simple example is as follows:", "", " @NAME Boo!", "", - "This will cause the script to be named `Boo!`, and for this text to", + "This will cause the script to be named `Boo!`, and for this text to " "appear on the assigned button and in internally generated documentation.", "", - "The space on buttons is limited. For the larger square buttons,", - "three lines of five characters can be displayed. For the smaller", + "The space on buttons is limited. For the larger square buttons, " + "three lines of five characters can be displayed. For the smaller " "round buttons, only two lines of three characters will fit.", "", - "LPHK attempts to display the name as best it can. Firstly, it breaks", - "long words into shorter fragments, then it tries to pack those fragments", - "together. The previous example `Boo!` is less than 5 characters, so", - "it fits completely on one line. The name `Pieces of text to display`", - "would first be broken up into `Piece` `s` `of` `text` `to` `displ` ay`,", - "then joined back up as `Piece` `s of` `text`, The remainder can't be", + "LPHK attempts to display the name as best it can. Firstly, it breaks " + "long words into shorter fragments, then it tries to pack those fragments " + "together. The previous example `Boo!` is less than 5 characters, so " + "it fits completely on one line. The name `Pieces of text to display` " + "would first be broken up into `Piece` `s` `of` `text` `to` `displ` ay`, " + "then joined back up as `Piece` `s of` `text`, The remainder can't be " "fitted, and is dropped off. The button would contain the text:", "", " Piece", - " a of", + " s of", " text", "", - "Shorter text strings are are displayed using larger fonts for greater" - "reasability if the option -f or --fit is specified on the command", + "Shorter text strings are are displayed using larger fonts for greater " + "readability if the option -f or --fit is specified on the command", "line.", "", "Only a single name header is permitted in a script.", "", - "This is not permitted in a subroutine because the subroutine is named", + "This is not permitted in a subroutine because the subroutine is named " "using the `@SUB` header."] @@ -256,7 +256,7 @@ def __init__( "", " @DESC Do really amazing things", "", - "This will cause the script or subroutine to be described as `Do really", + "This will cause the script or subroutine to be described as `Do really " "amazing things`, in internally generated documentation.", "", "Only a single description header is permitted in a script or subroutine."] @@ -293,7 +293,7 @@ def __init__( super().__init__("@DOC, Adds a line to the documentation text") - self.doc = ["The `@DOC` header allows multiple line documentation to be written for a script", + self.doc = ["The `@DOC` header allows multiple line documentation to be written for a script " "or subroutine. Each `@DOC` line is appended to the documentation.", "", "A simple example is as follows:", @@ -301,7 +301,7 @@ def __init__( " @DOC This is the first line of the documentation...", " @DOC ...and this is the second.", "", - "When the internal documentation is produced for the script or subroutine, this", + "When the internal documentation is produced for the script or subroutine, this " "text will appear."] @@ -320,4 +320,99 @@ def Validate( return True +scripts.Add_command(Header_Doc()) # register the header + + +# ################################################## +# ### CLASS Header_Doc_Add ### +# ################################################## + +# This is a dummy header. It is interpreted for real when a subroutine is loaded, +# but is ignored in the normal running of commands +class Header_Doc_Add(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@DOC+, Extends a line of the documentation text") + + self.doc = ["The `@DOC+` header allows a documentation line to be extended so that " + "word wrapping works correctly", + "", + "A simple example is as follows:", + "", + " @DOC This is the first line of the documentation...", + " @DOC+ ...and this is more of the 1st line.", + "", + "When the internal documentation is produced for the script or subroutine, this " + "text will appear with the line wrapped as required."] + + + # Dummy validate routine. Simply says all is OK (without a validation routine, an error is reported (but not printed) + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + add_part = split_line[1:] + last_part = btn.doc[-1:] + last_part += add_part + btn.doc[-1] = ' '.join(last_part) + + return True + + +scripts.Add_command(Header_Doc_Add()) # register the header + + +# ################################################## +# ### CLASS Header_Deprecated ### +# ################################################## + +# The Deprecated header marks a routine as deprecated with any additional text +# placed in the "use" description. +class Header_Deprecated(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@DEPRECATED, Marks the routine as deprecated") + + self.doc = ["The `@DEPRECSTED` header allows a routine to be marked as Deprecated.", + "", + "Some simple examples follows", + "", + " @DEPRECATED", + " @DEPRECATED Please use the XYZ command instead", + "", + "When the internal documentation is produced for the script or subroutine, it " + "will be flagged as deprecated. The optional message will be printed.", + "", + "A parameter may be available to either flag or prevent the use of deprecated " + "commands..."] + + + # Validate routine. Adds the deprecated information if the command is not already deprecated! + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if self.deprecated: # don't want to deprecate twice! + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted once.", btn.Line(idx)) + else: + self.deprecated = True + self.deprecated_use = ' '.join(split_line[1:]) + + return True + + scripts.Add_command(Header_Doc()) # register the header \ No newline at end of file diff --git a/commands_rpncalc.py b/commands_rpncalc.py index f03a6a5..243a438 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -23,9 +23,27 @@ def __init__( self, ): - super().__init__("RPN_EVAL", # the name of the command as you have to enter it in the code + super().__init__("RPN_EVAL, Evaluate an RPN expression", LIB) + self.doc = ["Evaluates a stack-based expression in a similar style to an old " + "HP programmable calculator or FORTH. RPN has the advantage of " + "allowing expressions of arbitrary complexity without needing " + "brackets or operator precidence. It is also simple to extend " + "with new commands and behaviour.", + "", + "The basic operation is that entries are either values that " + "are pushed onto the stack, or operators that work on the stack.", + "", + "Traditionally the top 4 values on the stack are called X, Y, " + "Z, and T. Some operators (such as X<>Y - which swaps the values " + "in the X and Y registers) explicitly mention these names.", + "", + "Many operators work by removing (popping) the top elements from " + "the stack before pushing the result onto the top of the stack. An " + "example is the `+` command which popps the X and the Y values " + "from the stack, and push the sum of these values back onto the " + "top of the stack."] # this command does not have a standard list of fields, so we need to do some stuff manually self.valid_max_params = 255 # There is no maximum, but this is a reasonable limit! self.valid_num_params = [1, None] # one or more is OK @@ -39,6 +57,21 @@ def __init__( # Now register the operators self.Register_operators() + ml = 0 + for o in self.operators: + ml = max(ml, len(o)) + ml += 4 + + self.doc += ['', 'Operators:', '', f'~{ml+7}'] + for o in self.operators: + op = self.operators[o] + if op[1] == 0: + c = o + else: + c = o + ' {v}' + self.doc += [" " + c + " "*(ml - len(c)) +" - " + op[0].__doc__] + self.doc += ['~'] + # We can simply override the first pass validation def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): @@ -188,6 +221,7 @@ def add(self, symbols, # the symbol table (stack, global vars, etc.) cmd, # the current command cmds): # the rest of the commands on the command line + """Removes the top 2 values from the stack, replacing them with their sum, and placing the previous contents of the X register into LASTX""" ret = 1 # always initialise ret to 1, because the default is to # step token by token along the expression @@ -207,6 +241,8 @@ def add(self, def subtract(self, symbols, cmd, cmds): + """Removes the top 2 values from the stack, replacing them with their difference (Y-X), and placing the previous contents of the X register into LASTX""" + ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) @@ -223,6 +259,8 @@ def subtract(self, symbols, cmd, cmds): def multiply(self, symbols, cmd, cmds): + """Removes the top 2 values from the stack, replacing them with their product (Y-X), and placing the previous contents of the X register into LASTX""" + ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) @@ -239,6 +277,8 @@ def multiply(self, symbols, cmd, cmds): def divide(self, symbols, cmd, cmds): + """Removes the top 2 values from the stack, replacing them with their quotient (Y/X), and placing the previous contents of the X register into LASTX""" + ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) @@ -255,6 +295,8 @@ def divide(self, symbols, cmd, cmds): def i_div(self, symbols, cmd, cmds): + """Removes the top 2 values from the stack, replacing them with the largest integer less than the quotient (Y/X), and placing the previous contents of the X register into LASTX""" + ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) @@ -271,6 +313,8 @@ def i_div(self, symbols, cmd, cmds): def mod(self, symbols, cmd, cmds): + """Removes the top 2 values from the stack, replacing them with the remainder (or modulus), and placing the previous contents of the X register into LASTX""" + ret = 1 a = variables.pop(symbols) b = variables.pop(symbols) @@ -287,6 +331,8 @@ def mod(self, symbols, cmd, cmds): def view(self, symbols, cmd, cmds): + """Displays the value of the X register""" + # view the top of the stack (typically where results are) ret = 1 print('Top of stack = ', variables.top(symbols, 1)) # we're going to peek at the top of the stack without popping @@ -295,6 +341,8 @@ def view(self, symbols, cmd, cmds): def view_s(self, symbols, cmd, cmds): + """Displays the current stack contents""" + # View the entire stack. Probably a debugging tool. ret = 1 print('Stack = ', symbols[SYM_STACK]) # show the entire stack @@ -303,6 +351,8 @@ def view_s(self, symbols, cmd, cmds): def view_l(self, symbols, cmd, cmds): + """Displays the current local variables""" + # View the local variables. Probably a debugging tool. ret = 1 print('Local = ', symbols[SYM_LOCAL]) # show all local variables @@ -311,6 +361,8 @@ def view_l(self, symbols, cmd, cmds): def view_g(self, symbols, cmd, cmds): + """Displays the current global variables""" + # View the global variables. Probably a debugging tool. ret = 1 with symbols[SYM_GLOBAL][0]: # lock the globals while we do this @@ -320,6 +372,8 @@ def view_g(self, symbols, cmd, cmds): def one_on_x(self, symbols, cmd, cmds): + """Replaces the value on the top of the stack with its reciprocal. The previous value of X is placed in LASTX""" + ret = 1 a = variables.pop(symbols) symbols[SYM_LOCAL]['last x'] = a @@ -333,6 +387,8 @@ def one_on_x(self, symbols, cmd, cmds): def int_x(self, symbols, cmd, cmds): + """Replaces the value on the top of the stack with the integer portion it. The previous value of X is placed in LASTX""" + # get the integer part of x ret = 1 a = variables.pop(symbols) @@ -347,6 +403,8 @@ def int_x(self, symbols, cmd, cmds): def frac_x(self, symbols, cmd, cmds): + """Replaces the value on the top of the stack with the fractional portion it. The previous value of X is placed in LASTX""" + # get the fractionasl part of x ret = 1 a = variables.pop(symbols) @@ -361,6 +419,8 @@ def frac_x(self, symbols, cmd, cmds): def chs(self, symbols, cmd, cmds): + """Changes the sign of the value on the top of the stack. LASTX is not modified.""" + ret = 1 a = variables.pop(symbols) @@ -373,6 +433,8 @@ def chs(self, symbols, cmd, cmds): def sqr(self, symbols, cmd, cmds): + """Replaces the value on the top of the stack its square. The previous value of X is placed in LASTX""" + # calculates the square ret = 1 a = variables.pop(symbols) @@ -389,6 +451,8 @@ def sqr(self, symbols, cmd, cmds): def y_to_x(self, symbols, cmd, cmds): + """Replaces the top 2 values on the stack with the value Y^X. The previous value of X is placed in LASTX""" + # calculates the square ret = 1 a = variables.pop(symbols) @@ -406,6 +470,8 @@ def y_to_x(self, symbols, cmd, cmds): def dup(self, symbols, cmd, cmds): + """Duplicates the top value on the stack. Sometimes also known as `ENTER^`""" + # duplicates the value on the top of the stack ret = 1 variables.push(symbols, variables.top(symbols, 1)) @@ -414,6 +480,8 @@ def dup(self, symbols, cmd, cmds): def pop(self, symbols, cmd, cmds): + """Removes and discards the top element of the stack. LASTX is not modified""" + # removes top item from the stack ret = 1 variables.pop(symbols) @@ -422,6 +490,8 @@ def pop(self, symbols, cmd, cmds): def clst(self, symbols, cmd, cmds): + """Clears the entire stack. LASTX is not modified""" + # clears the stack ret = 1 symbols[SYM_STACK].clear() @@ -430,6 +500,8 @@ def clst(self, symbols, cmd, cmds): def last_x(self, symbols, cmd, cmds): + """Pushes the LASTX value onto the top of the stack. LASTX is not modified""" + # resurrects the last value of x that was "consumed" by an operation ret = 1 try: @@ -443,6 +515,8 @@ def last_x(self, symbols, cmd, cmds): def cl_l(self, symbols, cmd, cmds): + """Clears all local variables""" + # clears the stack ret = 1 symbols[SYM_LOCAL].clear() @@ -451,6 +525,8 @@ def cl_l(self, symbols, cmd, cmds): def stack_len(self, symbols, cmd, cmds): + """Pushes the length of the stack onto the stack. Note that after a CLST the length of the stack is Zero.""" + # returns stack length ret = 1 variables.push(symbols, len(symbols[SYM_STACK])) @@ -459,6 +535,8 @@ def stack_len(self, symbols, cmd, cmds): def swap_x_y(self, symbols, cmd, cmds): + """Exchanges the top two values on the stack. LASTX is not modified.""" + # exchanges top two values on the stack ret = 1 @@ -472,6 +550,8 @@ def swap_x_y(self, symbols, cmd, cmds): def sto(self, symbols, cmd, cmds): + """Stores the top value on the stack into a variable. If a local variable of that name exists it will be used; otherwise if a global variable of that name exists, it will be used; finally if neither exist, a local variable will be created to contain the value. Neither the stsack or LASTX is modified.""" + # stores the value in local var if it exists, otherwise global var. If neither, creates local ret = 1 ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? @@ -483,6 +563,8 @@ def sto(self, symbols, cmd, cmds): def sto_g(self, symbols, cmd, cmds): + """Stores the top value on the stack into a global variable. If it does not exist, it will be created. Neither the stsack or LASTX is modified.""" + # stores the value on the top of the stack into the global variable named by the next token ret = 1 ret, v = variables.next_cmd(ret, cmds) # what's the name of the variable? @@ -494,6 +576,8 @@ def sto_g(self, symbols, cmd, cmds): def sto_l(self, symbols, cmd, cmds): + """Stores the top value on the stack into a local variable. If it does not exist, it will be created. Neither the stsack or LASTX is modified.""" + # stores the value on the top of the stack into the local variable named by the next token ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -504,6 +588,8 @@ def sto_l(self, symbols, cmd, cmds): def rcl(self, symbols, cmd, cmds): + """Recalls a value from a variable and pushes it onto the stack. If a local variable of that name exists, it will be used; otherwise if a global variable of that name exists, it will be used; otherwise the value 0 will be placed on the stack.""" + # recalls a variable. Try local first, then global ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -515,6 +601,8 @@ def rcl(self, symbols, cmd, cmds): def rcl_l(self, symbols, cmd, cmds): + """Recalls a value from a local variable and pushes it onto the stack. If a local variable of that name does not exist, the value 0 will be placed on the stack.""" + # recalls a local variable (not overly useful, but avoids ambiguity) ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -525,6 +613,8 @@ def rcl_l(self, symbols, cmd, cmds): def rcl_g(self, symbols, cmd, cmds): + """Recalls a value from a global variable and pushes it onto the stack. If a global variable of that name does not exist, the value 0 will be placed on the stack.""" + # recalls a global variable (useful if you define an identical local var) ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -535,6 +625,8 @@ def rcl_g(self, symbols, cmd, cmds): return ret def x_eq_zero(self, symbols, cmd, cmds): + """if X is equal to zero, the evaluation continues, otherwise it stops.""" + # only continues eval if the top of the stack is 0 if variables.top(symbols, 1) == 0: return 1 @@ -543,6 +635,8 @@ def x_eq_zero(self, symbols, cmd, cmds): def x_ne_zero(self, symbols, cmd, cmds): + """if X is not equal to zero, the evaluation continues, otherwise it stops.""" + # only continues eval if the top of the stack is not 0 if variables.top(symbols, 1) != 0: return 1 @@ -551,6 +645,8 @@ def x_ne_zero(self, symbols, cmd, cmds): def x_eq_y(self, symbols, cmd, cmds): + """if X is equal to Y, the evaluation continues, otherwise it stops.""" + # only continues eval if the two top values are equal if variables.top(symbols, 1) == variables.top(symbols, 2): return 1 @@ -559,6 +655,8 @@ def x_eq_y(self, symbols, cmd, cmds): def x_ne_y(self, symbols, cmd, cmds): + """if X is not equal to Y, the evaluation continues, otherwise it stops.""" + # only continues eval if the two top values are not equal if variables.top(symbols, 1) != variables.top(symbols, 2): return 1 @@ -567,6 +665,8 @@ def x_ne_y(self, symbols, cmd, cmds): def x_gt_y(self, symbols, cmd, cmds): + """if X is greater than Y, the evaluation continues, otherwise it stops.""" + # only continue if the top value > the second value on the stack if variables.top(symbols, 1) > variables.top(symbols, 2): return 1 @@ -575,6 +675,8 @@ def x_gt_y(self, symbols, cmd, cmds): def x_ge_y(self, symbols, cmd, cmds): + """if X is greater than or equal to Y, the evaluation continues, otherwise it stops.""" + # only continue if the top value >= the second value on the stack if variables.top(symbols, 1) >= variables.top(symbols, 2): return 1 @@ -583,6 +685,8 @@ def x_ge_y(self, symbols, cmd, cmds): def x_lt_y(self, symbols, cmd, cmds): + """if X is less than Y, the evaluation continues, otherwise it stops.""" + # only continue if the top value < the second value on the stack if variables.top(symbols, 1) < variables.top(symbols, 2): return 1 @@ -591,6 +695,8 @@ def x_lt_y(self, symbols, cmd, cmds): def x_le_y(self, symbols, cmd, cmds): + """if X is less than or equal to than Y, the evaluation continues, otherwise it stops.""" + # only continue if the top value <= the second value on the stack if variables.top(symbols, 1) <= variables.top(symbols, 2): return 1 @@ -599,6 +705,8 @@ def x_le_y(self, symbols, cmd, cmds): def is_def(self, symbols, cmd, cmds): + """if the variable is defined (locally or globally) the evaluation continues, otherwise it stops.""" + # only continue if the variable is defined (locally or globally is OK) ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -610,6 +718,8 @@ def is_def(self, symbols, cmd, cmds): def is_not_def(self, symbols, cmd, cmds): + """if the variable is not defined (locally or globally) the evaluation continues, otherwise it stops.""" + # only continue if the variable is not defined (either locally or globally) ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -621,6 +731,8 @@ def is_not_def(self, symbols, cmd, cmds): def is_local_def(self, symbols, cmd, cmds): + """if the variable is defined locally the evaluation continues, otherwise it stops.""" + # only continue if the variable is defined locally ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -631,6 +743,8 @@ def is_local_def(self, symbols, cmd, cmds): def is_local_not_def(self, symbols, cmd, cmds): + """if the variable is not defined locally the evaluation continues, otherwise it stops.""" + # only continue if the variable is not defined locally ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -641,6 +755,8 @@ def is_local_not_def(self, symbols, cmd, cmds): def is_global_def(self, symbols, cmd, cmds): + """if the variable is defined globally the evaluation continues, otherwise it stops.""" + # only continue if the variable is defined globally ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -652,6 +768,8 @@ def is_global_def(self, symbols, cmd, cmds): def is_global_not_def(self, symbols, cmd, cmds): + """if the variable is not defined globally the evaluation continues, otherwise it stops.""" + # only continue if the variable is not defined globally ret = 1 ret, v = variables.next_cmd(ret, cmds) @@ -663,11 +781,15 @@ def is_global_not_def(self, symbols, cmd, cmds): def abort_script(self, symbols, cmd, cmds): + """Abort the script (not just the expression evaluation).""" + # cause the script to be aborted return False def substr(self, symbols, cmd, cmds): + """Removes the top thee values from the stack, pushing the characters from Y to X-1 of Z onto the top of the stack.""" + # does a substring x = variables.pop(symbols) y = variables.pop(symbols) @@ -680,6 +802,8 @@ def substr(self, symbols, cmd, cmds): def d_to_j(self, symbols, cmd, cmds): + """Converts a text date on the top of the stack to the days since Jan-1-1900.""" + # converts a text date on the top of the stack to a julian date (integer) d = variables.pop(symbols) dt = parser.parse(d) @@ -692,6 +816,8 @@ def d_to_j(self, symbols, cmd, cmds): def j_to_d(self, symbols, cmd, cmds): + """Converts the days since Jan-1-1900 in on the top of the stack to a text date.""" + # converts a julian date (integer) on the top of the stack to a text date j = variables.pop(symbols) dt = datetime.date.fromordinal(j) @@ -781,6 +907,8 @@ def __init__( ) ) self.doc= ["If parameter 1 is:", + "", + "~28", f" '{RC_GLOBALS}' All the global variables are cleared", f" '{RC_LOCALS}' All the local variables are cleared", f" '{RC_VARS}' All variables are cleared", @@ -788,7 +916,8 @@ def __init__( f" '{RC_ALL}' All variables and the stack are cleared", f" '{RC_GLOBAL}' v1 [v2 [v3...]] Named global variables v1... are deleted", f" '{RC_LOCAL}' v1 [v2 [v3...]] Named local variables v1... are deleted", - f" '{RC_VAR}' v1 [v2 [v3...]] Named variables v1... are deleted"] + f" '{RC_VAR}' v1 [v2 [v3...]] Named variables v1... are deleted", + "~"] def Process(self, btn, idx, split_line): diff --git a/commands_scrape.py b/commands_scrape.py index cb5d127..d95d592 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -351,8 +351,8 @@ def __init__( (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) - self.doc = ["This command calculates the hamming distance between 2 fingerprints.", \ - "This can be used to determine how similar 2 images are. The larger", \ + self.doc = ["This command calculates the hamming distance between 2 fingerprints. ", + "This can be used to determine how similar 2 images are. The larger ", "the hamming distance, the more different the images are."] @@ -395,8 +395,8 @@ def __init__( (7, " Return the hamming distance between colours ({1}, {2}, {3}) and ({4}, {5}, {6}) into {7}"), ) ) - self.doc = ["This command calculates the hamming distance between 2 RGB values.", \ - "This can be used to determine how similar 2 colours are. The larger", \ + self.doc = ["This command calculates the hamming distance between 2 RGB values. ", + "This can be used to determine how similar 2 colours are. The larger ", "the hamming distance, the more different the colours are."] diff --git a/commands_subroutines.py b/commands_subroutines.py index 799ed71..0d32459 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -31,93 +31,61 @@ def Validate( pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) ): - if pass_no == 1: - if btn.is_button: - return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a subroutine.", btn.Line(idx)) - - return True - -scripts.Add_command(Header_Sub_Name()) # register the header - - -# ################################################## -# ### CLASS Subroutine_Define ### -# ################################################## - -# class that defines the CALL:xxxx command (runs a subroutine). This parses the routine (pass 1 and 2 validation) -# and adds it as a command if the parsing suceeds. It can then be called just like any other command -class Subroutine(command_base.Command_Basic): - def __init__( - self, - Name, # The name of the command - Params, # The parameter tuple - Lines # The text of the subroutine/function - ): - - super().__init__(SUBROUTINE_PREFIX + Name + ", Define a subroutine that can be called with named parameters", - LIB, - Params, - ( - # num params, format string (trailing comma is important) - (0, " Call "+ Name), - ) ) - - self.routine = Lines # the routine to execute - self.btn = scripts.Button(-1, -1, self.routine, None, Name) # we retain this so we only have to validate it once. executions use a deep-ish copy - - self.doc = ["This header is used to define a subroutine. Subroutines are loaded", + self.doc = ["This header is used to define a subroutine. Subroutines are loaded " "separately from button scripts and can be saved in layouts.", "", - "A subroutine header consists of the the text `@SUB` followed by the", - "name of the subroutine, and then the parameters for the subroutine", + "A subroutine header consists of the the text `@SUB` followed by the " + "name of the subroutine, and then the parameters for the subroutine.", "", - "A simple subtoutine `DO_STUFF` that is called without patameters would", + "A simple subtoutine `DO_STUFF` that is called without patameters would " "be defined as follows:", "", " @SUB DO_STUFF", "", - "This would be followed by a script to so whatever the subroutine needs", + "This would be followed by a script to so whatever the subroutine needs " "to do.", "", "A calling script would call this subroutine using:", "", " CALL:DO_STUFF", "", - "After completion of the subroutine, control would pass to the statement", - "following the call unless an END or ABORT statement was reached, or", - "the operator cancelled the routine by pressing the Launchpad button", + "After completion of the subroutine, control would pass to the statement " + "following the call unless an END or ABORT statement was reached, or " + "the operator cancelled the routine by pressing the Launchpad button " "a second time." "", - "Subroutines have their own stacks and local variables as well as access", - "to global variables. For access to information within the calling", - "script, and to return information back to the calling script either", + "Subroutines have their own stacks and local variables as well as access " + "to global variables. For access to information within the calling " + "script, and to return information back to the calling script either " "global variables or parameters can be used.", "", - "Parameters are defined by placing legal variable names following the", + "Parameters are defined by placing legal variable names following the " "subroutine name on the @SUB line. An example is:", "", " @SUB DO_STUFF a b", "", - "This defines a subroutine that takes 2 positional parameters. By", - "default they are integers, and they are passed by value (that is", - "any changes to their values are not passed back to the calling", + "This defines a subroutine that takes 2 positional parameters. By " + "default they are integers, and they are passed by value (that is " + "any changes to their values are not passed back to the calling " "routine.", "", "This subroutine cound be called using a script as follows:", "", " CALL:DO_IT 42 var2", "", - "Because the the parameters are passed by value, constants or variables", - "can be used in the call. In this case, in the subroutine, the local", - "variable `a` would have the value 42, and the local variable `b`", - "would be set to the value of the variable `var2` from the calling", + "Because the the parameters are passed by value, constants or variables " + "can be used in the call. In this case, in the subroutine, the local " + "variable `a` would have the value 42, and the local variable `b` " + "would be set to the value of the variable `var2` from the calling " "script.", "", - "Parameters can also be defined with special modifiers that change this", - "default behaviour. One way of applying these modifiers to parameters", + "Parameters can also be defined with special modifiers that change this " + "default behaviour. One way of applying these modifiers to parameters " "is by following the parameter name with a `+` followed by the modifiers.", "", "The modifiers are:", + "", + "~19", " `%` or `I` - defines the variable as an integer number (default)", " `#` or `F` - defines the variable as a float or real number", " `$` or `S` - defines the variable as a string", @@ -128,6 +96,7 @@ def __init__( " `M` - defines the variable as mandatory (default)", " `@` or `R` - defines the variable as call by reference (more later)", " `V` - defines the variable as call by value (default)", + "~", "", "An example using these modifiers is as follows:", "", @@ -135,55 +104,91 @@ def __init__( "", "These parameters are:", "", + "~19", " a+I - the parameter `a` that is an integer (and required)", " b+FO - the parameter `b` that is an optional floating point", " c+R$ - a required call-by reference string variable `c`", + "~", "", "Valid calls to this subroutine are as follows:", "", " CALL:DO_MORE 12", " CALL:DO_MORE x 12.5 line", "", - "The first call passes the required first parameter, but not the second", - "optional parameter. Because the second parameter was not passed, no", - "more paramters are required. The subroutine would see the variable `a`", - "have the value 12, the variable `b`, 0.0, and the variable `c` would be", - "a blank string. Attempts to change the value of `c` would succeed, but", + "The first call passes the required first parameter, but not the second " + "optional parameter. Because the second parameter was not passed, no " + "more paramters are required. The subroutine would see the variable `a` " + "have the value 12, the variable `b`, 0.0, and the variable `c` would be " + "a blank string. Attempts to change the value of `c` would succeed, but " "no variable in the calling routine would be affected.", "", - "The second call passes the required first parameter (a variable this", - "time), a value of 12.5 for the second parameter, and the variable", - "`line` as the final required parameter. Because the second (optional)", - "parameter was passed, the next parameter (being required) was mandatory.", - "Within the subroutine, `a` would have the value of `x` in the calling", - "routine, `b` would have the value of 12.5, and `c` would have the value", - "of the variable `line` in the calling routine. Changing the value of", + "The second call passes the required first parameter (a variable this " + "time), a value of 12.5 for the second parameter, and the variable " + "`line` as the final required parameter. Because the second (optional) " + "parameter was passed, the next parameter (being required) was mandatory. " + "Within the subroutine, `a` would have the value of `x` in the calling " + "routine, `b` would have the value of 12.5, and `c` would have the value " + "of the variable `line` in the calling routine. Changing the value of " "`c` will also change the value of `line` in the calling routine.", "", - "This method of applying modifiers to variables can be simplified for all", - "modifiers that are not permitted in variable names. These can also be", - "placed before or after the parameter name the following functionally", + "This method of applying modifiers to variables can be simplified for all " + "modifiers that are not permitted in variable names. These can also be " + "placed before or after the parameter name the following functionally " "identical subroutine definition:", "", " @SUB DO_MORE a% -b# @c$", "", - "Subroutines are placed in text files and loaded using the Subroutine|Load", - "menu option. Note that multiple subroutines can be placed in a single", + "Subroutines are placed in text files and loaded using the Subroutine|Load " + "menu option. Note that multiple subroutines can be placed in a single " "file. In this case they must be separated by a line consisting of `===`.", "", - "Subroutines can call other subroutines and can probably be called", - "recursively. A script will fail to load if it depends on a subroutine", - "that is not present. Similarly, subroutines that depend on other", + "Subroutines can call other subroutines and can probably be called " + "recursively. A script will fail to load if it depends on a subroutine " + "that is not present. Similarly, subroutines that depend on other " "subroutines will fail to load if those subroutines are not available.", "", - "During execution of a subroutine the command RETURN will immediately", - "return control to the calling script or subroutine. The commands END", + "During execution of a subroutine the command RETURN will immediately " + "return control to the calling script or subroutine. The commands END " "and ABORT will stop execution immediately without returning to the caller.", "", - "Parameter names follow the same rules as variable names. They must satart", - "with an alpha character, and may then be followed by any number of", + "Parameter names follow the same rules as variable names. They must start " + "with an alpha character, and may then be followed by any number of " "alpha-numeric characters and underscores (`_`)."] - + + if pass_no == 1: + if btn.is_button: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' is only permitted in a subroutine.", btn.Line(idx)) + + return True + +scripts.Add_command(Header_Sub_Name()) # register the header + + +# ################################################## +# ### CLASS Subroutine_Define ### +# ################################################## + +# class that defines the CALL:xxxx command (runs a subroutine). This parses the routine (pass 1 and 2 validation) +# and adds it as a command if the parsing suceeds. It can then be called just like any other command +class Subroutine(command_base.Command_Basic): + def __init__( + self, + Name, # The name of the command + Params, # The parameter tuple + Lines # The text of the subroutine/function + ): + + super().__init__(SUBROUTINE_PREFIX + Name + ", Define a subroutine that can be called with named parameters", + LIB, + Params, + ( + # num params, format string (trailing comma is important) + (0, " Call "+ Name), + ) ) + + self.routine = Lines # the routine to execute + self.btn = scripts.Button(-1, -1, self.routine, None, Name) # we retain this so we only have to validate it once. executions use a deep-ish copy + # process for a subroutine handles parameter passing and then passes off the process to the script in a "dummy" button def Process(self, btn, idx, split_line): diff --git a/commands_test.py b/commands_test.py index 9140bed..f227513 100644 --- a/commands_test.py +++ b/commands_test.py @@ -4,6 +4,7 @@ LIB = "cmds_test" # name of this library (for logging) class Test_XX(command_base.Command_Basic): + def clean(self, s): # remove stuff that changes (memory addresses) p = s.find(" at 0x") if p >=0: @@ -53,7 +54,8 @@ def __init__( ) ) self.one = 1 - + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_01()) # register the command @@ -75,6 +77,8 @@ def __init__( ) ) self.one = 1 + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_02()) # register the command @@ -97,6 +101,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_03()) # register the command @@ -119,6 +125,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_04()) # register the command @@ -141,6 +149,8 @@ def __init__( ) ) self.one = 1 + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_11()) # register the command @@ -163,6 +173,8 @@ def __init__( ) ) self.one = 1 + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_12()) # register the command @@ -185,6 +197,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_13()) # register the command @@ -207,6 +221,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_14()) # register the command @@ -229,6 +245,8 @@ def __init__( ) ) self.one = 1 + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_21()) # register the command @@ -251,6 +269,8 @@ def __init__( ) ) self.one = 1 + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_22()) # register the command @@ -273,6 +293,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_23()) # register the command @@ -295,6 +317,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_24()) # register the command @@ -318,6 +342,8 @@ def __init__( ) ) self.one = "1" + self.deprecated = True + self.deprecated_use = "This command exists for testing purposes and will not exist in the production version of LPHK." scripts.Add_command(Test_101()) # register the command @@ -339,6 +365,9 @@ def __init__( (0, " Dialog Test"), ) ) + self.deprecated = True + self.deprecated_use = "This command will not exist in the production version of LPHK. Please use one of the `DIALOG_` commands." + def Process(self, btn, idx, split_line): import dialog @@ -364,6 +393,9 @@ def __init__( (0, " Dump headers and commands"), ) ) + self.deprecated = True + self.deprecated_use = "This command will not exist in the production version of LPHK. Please use the `DOCUMENT` command." + def Process(self, btn, idx, split_line): scripts.Dump_commands() @@ -388,6 +420,9 @@ def __init__( (0, " Dump headers and commands"), ) ) + self.deprecated = True + self.deprecated_use = "This command will not exist in the production version of LPHK. Please use the `DOCUMENT` command." + def Process(self, btn, idx, split_line): scripts.Dump_commands(DS_NORMAL + [D_DEBUG]) diff --git a/commands_win32.py b/commands_win32.py index 8af1a98..213077b 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -286,9 +286,9 @@ def __init__( (4, " Find {4}th window titled '{1}', returning handle in {2}. Report {3} total matches"), ) ) - self.doc = ["Searches for an exactly matching window `Title`. If multiple are found,", - "the windows are sorted by process id. The number of matching windows is", - "returned in `M`. If `N` or more are found, the nth window handle is", + self.doc = ["Searches for an exactly matching window `Title`. If multiple are found, " + "the windows are sorted by process id. The number of matching windows is " + "returned in `M`. If `N` or more are found, the nth window handle is " "returned in `HWND`. -1 is returned if there is an error."] def Process(self, btn, idx, split_line): @@ -346,10 +346,10 @@ def __init__( (4, " Find {4}th window titled '{1}', returning handle in {2}. Report {3} total matches"), ) ) - self.doc = ["Searches for windows with titles starting with `Title`. If multiple", - "are found, the windows are sorted by process id. The number of", - "matching windows is returned in `M`. If `N` or more are found, the", - "nth window handle is returned in `HWND`. -1 is returned if there is", + self.doc = ["Searches for windows with titles starting with `Title`. If multiple " + "are found, the windows are sorted by process id. The number of " + "matching windows is returned in `M`. If `N` or more are found, the " + "nth window handle is returned in `HWND`. -1 is returned if there is " "an error."] def Process(self, btn, idx, split_line): @@ -410,10 +410,10 @@ def __init__( (4, " Find {4}th window titled '{1}', returning handle in {2}. Report {3} total matches"), ) ) - self.doc = ["Searches for windows with titles described with the regular expression", - "`Regex`. If multiple are found, the windows are sorted by process id.", - "The number of matching windows is returned in `M`. If `N` or more are", - "found, the nth window handle is returned in `HWND`. -1 is returned if", + self.doc = ["Searches for windows with titles described with the regular expression " + "`Regex`. If multiple are found, the windows are sorted by process id. " + "The number of matching windows is returned in `M`. If `N` or more are " + "found, the nth window handle is returned in `HWND`. -1 is returned if " "there is an error."] def Process(self, btn, idx, split_line): @@ -469,8 +469,8 @@ def __init__( (1, " Minimise specified window {1}"), ) ) - self.doc = ["If no parameter is passed, this command will minimise all windows except LPHK.", - "If a parmeter is passed, the window with this handle is minimised. This also", + self.doc = ["If no parameter is passed, this command will minimise all windows except LPHK. " + "If a parmeter is passed, the window with this handle is minimised. This also " "allows LPHK to be minimised."] @@ -572,12 +572,13 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val + ("Success", False, AVV_REQD, PT_INT, None, None), # variable to contain success value ("Clipboard", True, AVV_REQD, PT_STR, None, None), # variable to contain cut item ), ( # num params, format string (trailing comma is important) - (0, " Copy into system clipboard"), - (1, " Copy into system clipboard and {1}"), + (1, " Copy into system clipboard returning success in {2}"), + (2, " Copy into system clipboard and {1}, returning success in {2}"), ) ) def Process(self, btn, idx, split_line): @@ -593,16 +594,33 @@ def Process(self, btn, idx, split_line): import pyperclip # pyperclip is cross-platform (better than using windows specific code) - w = 0 - t = '' - while t == '' and w < 1: # we often have to wait for the text to appear in the clipboard - btn.Safe_sleep(DELAY_EXIT_CHECK) - w += DELAY_EXIT_CHECK + success = False + r = 20 + while r > 0: + r -= 1 + try: + n = win32clipboard.CountClipboardFormats() + if n > 0: + break + except: + n = -1 + btn.Safe_sleep(0.05) + + if (n <= 0): + self.Set_param(btn, 1, -1) + else: t = pyperclip.paste() - if self.Param_count(btn) > 0: # save to variable if required + if n <= 0: + print(f'fail {r}') + self.Set_param(btn, 2, None) + self.Set_param(btn, 1, -1) + else: + t = pyperclip.paste() # get it again t = t.rstrip('\r\n') # remove any line terminators - self.Set_param(btn, 1, t) + print(f"success {r} `{t}`") + self.Set_param(btn, 2, t) + self.Set_param(btn, 1, 0) scripts.Add_command(Win32_Copy()) # register the command @@ -754,10 +772,47 @@ def Process(self, btn, idx, split_line): hwnd = win32gui.GetForegroundWindow() # get the current window hwnd = self.Get_param(btn, 3, hwnd) # override with parameter if passed - _, _, x, y = GetWindowRect(hwnd) # get the size + _, _, x, y = win32gui.GetWindowRect(hwnd) # get the size self.Set_param(btn, 1, x) # return width and height of window self.Set_param(btn, 2, y) scripts.Add_command(Win32_Window_Size()) # register the command + + +# ################################################## +# ### CLASS W_LIST_HWND ### +# ################################################## + +# class that defines the W_LIST_HWND command - lists window titles +class Win32_List_Hwnd(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_LIST_HWND, Lists all windows", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ), + ( + # num params, format string (trailing comma is important) + (0, " List window titles"), + ) ) + + + def Process(self, btn, idx, split_line): + + def CheckWindow(hwnd, data): + # callback function to receive enumerated window handles + print(f"`{win32gui.GetWindowText(hwnd)}`") + + hwnds = [] # reset the list of window handles + title = "" # get the title we're searching for + + data = {'title':title, 'hwnds':hwnds} # data structure to be used by the callback routine + win32gui.EnumWindows(CheckWindow, data) # enumerate windows + + +scripts.Add_command(Win32_List_Hwnd()) # register the command diff --git a/constants.py b/constants.py index 81482aa..40d5d8e 100644 --- a/constants.py +++ b/constants.py @@ -125,6 +125,7 @@ D_COMMAND_BASE = 5 # produce documentation for routines used in the creation of commands D_DEBUG = 6 # add debug info where available D_SOURCE = 7 # add source where available +D_NO_SRC_DOC = 8 # hide irrelevant source documentation DS_NORMAL = [D_HEADERS, D_COMMANDS, D_SUBROUTINES, D_BUTTONS] diff --git a/files.py b/files.py index 65a4210..76e27c7 100644 --- a/files.py +++ b/files.py @@ -271,6 +271,13 @@ def validate_all_buttons(): # load a single subroutine def load_subroutine(sub, sub_n, fname): import commands_subroutines + + while sub[0].strip() == "": # trim leading blank lines + sub = sub[1:] + + while sub[-1].strip() == "": # trim trailing blank lines + sub = sub[:-1] + ok, name, params, err = commands_subroutines.Add_Function(sub, sub_n, fname) # Attempt to load the command if err == None: diff --git a/scripts.py b/scripts.py index 8f36d82..ed16b92 100644 --- a/scripts.py +++ b/scripts.py @@ -52,6 +52,32 @@ def Remove_command( # display info on all commands and headers def Dump_commands(style=DS_NORMAL): + def checkindent(line, oldindent, defaultindent): + skip = False + newindent = oldindent + + if line[:1] == '~': + if line[1:] == '': + newindent = defaultindent + skip = True + else: + try: + newindent = defaultindent + int(line[1:]) + skip = True + except: + pass + + return newindent, skip + + def wrap_line(s, indent=0, wrap=80): + import textwrap + + pre = s[:indent] + post = s[indent:] + wrapped = textwrap.wrap(post, width=wrap-indent) + sep = '\n' + ' '*indent + return pre + sep.join(wrapped) + def get_name(c): if isinstance(c, command_base.Command_Basic): return c.name @@ -76,12 +102,23 @@ def get_desc(c): return ret def dump_name(c_type, c): - print(f" {c_type} \"{get_name(c)}\"", end="") + l = f" {c_type} \"{get_name(c)}\"" desc = get_desc(c) if desc == "": - print() + print(l) else: - print(f" - {desc}") + l = l + ' - ' + print(wrap_line(l + desc, len(l))) + + def dump_deprecated(c_type, c): + ret = [] + if isinstance(c, command_base.Command_Basic) or isinstance(c, Button): + if c.deprecated: + print(" Deprecated") + if c.deprecated_use != "": + print(wrap_line(" "*12 + c.deprecated_use, 12)) + else: + print(wrap_line(" "*12 + "This command may not exist in future versions of LPHK.", 12)) def get_doc(c): ret = [] @@ -102,8 +139,11 @@ def dump_doc(c): doc = get_doc(c) if doc != []: print(" Notes") + indent = 12 for n in doc: - print(f" {n}") + indent, skip = checkindent(n, indent, 12) + if not skip: + print(wrap_line(" "*12 + n, indent)) def dump_ancestory(c): print(" Ancestory") @@ -138,24 +178,31 @@ def dump_params(c): else: print(" UNKNOWN VALUE") - def dump_source(c): - if isinstance(c, commands_subroutines.Subroutine): + def dump_source(c, hide_doc): + + def print_source(lines): print(" Source") - for line in c.routine: - print(f" {line}") + for i, line in enumerate(lines): + if hide_doc and line.lstrip().split()[:1] in [['@DESC'], ['@DOC'], ['@DOC+']]: + continue + l = f" {i+1:3}: " + p = line.lstrip().find(" ") + len(line) - len(line.lstrip()) + 1 + print(wrap_line(l+line, len(l)+p)) + + if isinstance(c, commands_subroutines.Subroutine): + print_source(c.routine) elif isinstance(c, Button): - print(" Source") - for line in c.script_lines: - print(f" {line}") + print_source(c.script_lines) def dump(c_type, c, style): dump_name(c_type, c) + dump_deprecated(c_type, c) dump_doc(c) if D_DEBUG in style: dump_ancestory(c) dump_params(c) if D_SOURCE in style: - dump_source(c) + dump_source(c, D_NO_SRC_DOC in style) print() @@ -181,7 +228,7 @@ def dump(c_type, c, style): print() for cmd in VALID_COMMANDS: if isinstance(VALID_COMMANDS[cmd], commands_subroutines.Subroutine): - dump("Subroutine", VALID_COMMANDS[cmd], style) + dump("Subroutine", VALID_COMMANDS[cmd], style) if D_BUTTONS in style: print("BUTTONS") @@ -255,6 +302,9 @@ def __init__( else: # otherwise self.root = root # the caller is the root + self.deprecated = False # by default, buttons (remember that subroutines are buttons!) are not deprecated + self.deprecated_use = "" # allow text to specify a replacement + # let us set/change the name of a button def Set_name(self, name): @@ -888,11 +938,11 @@ def kill_all(): # Unbind all keys. def Unbind_all(): lp_events.unbind_all() # Unbind all events - + for x in range(9): for y in range(9): Unbind(x, y) - + #text = [["" for y in range(9)] ] # Reienitialise all scripts to blank kill_all() # stop everything running @@ -914,7 +964,7 @@ def Unload_all(): Remove_command(cmd) # remove it files.layout_changed_since_load = True # mark layout as changed - + files.validate_all_buttons() # ensure buttons are valid diff --git a/window.py b/window.py index b672a2c..64f1b3a 100644 --- a/window.py +++ b/window.py @@ -227,14 +227,14 @@ def connect_lp(self): lp = lpcon().get_launchpad() - if lp is -1: + if lp == -1: self.popup(self, "Connect to Unsupported Device", self.error_image, """The device you are attempting to use is not currently supported by LPHK, and there are no plans to add support for it. Please voice your feature requests on the Discord or on GitHub.""", "OK") - if lp is None: + if lp == None: self.popup_choice(self, "No Launchpad Detected...", self.error_image, """Could not detect any connected Launchpads! Disconnect and reconnect your USB cable, From f4829aaff9b64071ba8768e85a849cbf4557df42 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Tue, 13 Apr 2021 23:17:23 +0800 Subject: [PATCH 73/83] A major bug fix and more documentation * command_base.py Modification to constant flag to be a character more unusual than a quote. * command_base.py Addition of filter to display parameters without the constant flag * commands_control.py fix to "comparable" function to ensure literals compare correctly to variable values (finally!!!) * commands_control.py Better documentation of IF command * commands_external.py More documentation * commands_header.py Additional documentation * commands_keys.py More documentation * commands_mouse.py improved documentation * commands_pause improved documentation * commands_scrape.py significantly improved documentation * commands_subroutines.py slightly improved documentation * scripts.py addition of @name to script lines hidden. --- command_base.py | 12 +++++-- commands_control.py | 33 ++++++++++++++++++-- commands_external.py | 13 ++++---- commands_header.py | 2 +- commands_keys.py | 14 ++++----- commands_mouse.py | 8 ++--- commands_pause.py | 2 +- commands_scrape.py | 69 ++++++++++++++++++++++++++++++++++++++--- commands_subroutines.py | 2 +- scripts.py | 6 ++-- 10 files changed, 130 insertions(+), 31 deletions(-) diff --git a/command_base.py b/command_base.py index 4e1f091..2cd22ea 100644 --- a/command_base.py +++ b/command_base.py @@ -254,6 +254,12 @@ def Partial_run_step_get(self, ret, btn, idx, split_line): return ret + # when displaying variables, this will ensure string literals don't have their flag included + def strip_null(self, a): + if type(a) == str and a[:1] == "\0" : # remove leading null indicating literal + a = a[1:] + return a + def Partial_run_step_info(self, ret, btn, idx, split_line): # This step matches the number of parameters passed with the definitions for messages, # printing the matching message, or a default message if no matching message can be found. @@ -262,8 +268,10 @@ def Partial_run_step_info(self, ret, btn, idx, split_line): # for the same number of parameters then you're going to want to override this method. # If you're overriding the method, you will rarely want to call the ancestor method. msg = False + + params = [self.strip_null(param) for param in btn.symbols[SYM_PARAMS]] # hide the literal flag + if self.auto_message: - params = btn.symbols[SYM_PARAMS] param_cnt = btn.symbols[SYM_PARAM_CNT] for msg_def in self.auto_message: if msg_def[AM_COUNT] == param_cnt: @@ -572,7 +580,7 @@ def Get_param(self, btn, n, other=None): if av[AV_VAR_OK] == AVV_REQD: ret = variables.get(param, btn.symbols[SYM_LOCAL], btn.symbols[SYM_GLOBAL][1]) else: - if type(param) == str and av[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '"': + if type(param) == str and av[AV_TYPE][AVT_DESC] in {PT_STR[AVT_DESC], PT_STRS[AVT_DESC]} and param[0:1] == '\0': ret = param[1:] else: ret = param diff --git a/commands_control.py b/commands_control.py index 38ef14b..7d9a43a 100644 --- a/commands_control.py +++ b/commands_control.py @@ -180,6 +180,9 @@ def Comparable(self, a, b): def either_is(a, b, c_type): return type(a) == c_type or type(b) == c_type + a = self.strip_null(a) # remove leading null indicating string literal + b = self.strip_null(b) + if isinstance(a, type(b)) or isinstance(b, type(a)): # probably comparable return a, b @@ -222,6 +225,7 @@ def either_is(a, b, c_type): return str(a), str(b) except: None + return a, b def a_eq_b(self, btn, first=2, second=3): @@ -363,12 +367,12 @@ def __init__( ): super().__init__( - "IF, IF x comp y GOTO label", + "IF, Tests a pair of values and takes an action if the result is True", LIB, ( # Desc Opt Var type p1_val p2_val ("A", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! - ("comp", False, AVV_NO, PT_WORD, None, None), # comparison operator + ("Comp", False, AVV_NO, PT_WORD, None, None), # comparison operator ("B", False, AVV_YES,PT_ANY, None, None), # a and b can be anything! ("Action", False, AVV_NO, PT_WORD, None, None), # GOTO, ABORT, RETURN, END ("Label", True, AVV_NO, PT_LABEL, None, None), # required for GOTO @@ -380,6 +384,31 @@ def __init__( ), ) + self.doc = ["Based on the result of a comparison of 2 values, jump to a label " + "or RETURN/END/ABORT.", + "", + "The 2 values passed can be either constants or variables, and the " + "comparison operators are:", + "", + "~19", + " EQ, =, or == Test the values for equality", + " NE, !=, or <> Test the values for inequality", + " LE, or <= Test if the first value is less than or equal to the second", + " LT, or < Test if the first value is less than the second", + " GT, or > Test if the first value is greater than the second", + " GE, or >= Test if the first value is greater than or equal to the second", + "~", + "If the result of th test is True, the `Action` is performed. The actions are:", + "" + "~19", + " GOTO `Label` Transfer control to the label `Label`", + " RETURN Return from a subroutine or end a button script", + " END Stop execution of the script now (even from within a subroutine)", + " ABORT As for `END` but with the implication of error", + "~", + "", + "The `Label` parameter is required for `GOTO` action, and prohibited for other actions."] + def Process(self, btn, idx, split_line): comp = self.Get_param(btn, 2) diff --git a/commands_external.py b/commands_external.py index 8a629c0..52418d9 100644 --- a/commands_external.py +++ b/commands_external.py @@ -14,7 +14,7 @@ def __init__( ): super().__init__( - "WEB", # the name of the command as you have to enter it in the code + "WEB, Open a page in a web browser", LIB, " Open website '{1}' in default browser" ) @@ -52,7 +52,8 @@ def __init__( super().__init__() - self.name = "WEB_NEW" # the name of the command as you have to enter it in the code + self.name = "WEB_NEW" + self.desc = "Open a page in a new browser window" self.info_msg = " Open website '{1}' in a new browser" @@ -74,7 +75,7 @@ def __init__( ): super().__init__( - "OPEN", # the name of the command as you have to enter it in the code + "OPEN, Open a file or location", LIB, " Open file or location '{1}'" ) @@ -99,7 +100,7 @@ def __init__( self, ): - super().__init__("SOUND") # the name of the command as you have to enter it in the code + super().__init__("SOUND, Play a sound file") def Validate( self, @@ -184,7 +185,7 @@ def __init__( self, ): - super().__init__("CODE") # the name of the command as you have to enter it in the code + super().__init__("CODE, Run a command") def Validate( @@ -233,7 +234,7 @@ def __init__( self, ): - super().__init__("CODE_NOWAIT", # the name of the command as you have to enter it in the code + super().__init__("CODE_NOWAIT, Run a command but don't wait for it to finish", LIB, ( # Desc Opt Var type p1_val p2_val diff --git a/commands_header.py b/commands_header.py index 85e0d13..e4121a4 100644 --- a/commands_header.py +++ b/commands_header.py @@ -334,7 +334,7 @@ def __init__( self, ): - super().__init__("@DOC+, Extends a line of the documentation text") + super().__init__("@DOC+, Extends a line of the documentation text without adding a line break") self.doc = ["The `@DOC+` header allows a documentation line to be extended so that " "word wrapping works correctly", diff --git a/commands_keys.py b/commands_keys.py index 67a9a9a..d2db39c 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -14,7 +14,7 @@ def __init__( ): super().__init__( - "WAIT_PRESSED", # the name of the command as you have to enter it in the code + "WAIT_PRESSED, Wait until the key used to start the script is unpressed", LIB, (), () ) @@ -47,7 +47,7 @@ def __init__( ): super().__init__( - "TAP, Tap the named key", # the name of the command as you have to enter it in the code + "TAP, Tap (and release) a key", LIB, ( # Desc Opt Var type p1_val p2_val @@ -110,7 +110,7 @@ def __init__( ): super().__init__( - "PRESS", # the name of the command as you have to enter it in the code + "PRESS, Press (and hold) a key", LIB, ( # Desc Opt Var type p1_val p2_val @@ -141,7 +141,7 @@ def __init__( ): super().__init__( - "RELEASE", # the name of the command as you have to enter it in the code + "RELEASE, Release a PRESSed key", LIB, ( # Desc Opt Var type p1_val p2_val @@ -172,7 +172,7 @@ def __init__( ): super().__init__( - "RELEASE_ALL", # the name of the command as you have to enter it in the code + "RELEASE_ALL, Release any/all keys that are currently PRESSed", LIB, (), ( @@ -197,7 +197,7 @@ class Keys_String(command_base.Command_Text_Basic): def __init__( self ): - super().__init__("STRING", # the name of the command as you have to enter it in the code + super().__init__("STRING, Type a series of characters", LIB, "Type out string" ) @@ -229,7 +229,7 @@ def __init__( self, ): - super().__init__("TYPE, type the text that is the concatenation of all variables passed", + super().__init__("TYPE, Type the text that is the concatenation of all variables passed", LIB, ( # Desc Opt Var type p1_val p2_val diff --git a/commands_mouse.py b/commands_mouse.py index 0d021bb..bf0eae2 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -113,7 +113,7 @@ def __init__( self, ): - super().__init__("M_LINE, Move the mouse along a line", + super().__init__("M_LINE, Move the mouse along a line from one position to another", LIB, ( # Desc Opt Var type p1_val p2_val @@ -169,7 +169,7 @@ def __init__( self, ): - super().__init__("M_LINE_MOVE, Relative mouse movement", + super().__init__("M_LINE_MOVE, Move the mouse along a line from the current position", LIB, ( # Desc Opt Var type p1_val p2_val @@ -221,7 +221,7 @@ def __init__( self, ): - super().__init__("M_LINE_SET, Absolute mouse movement", + super().__init__("M_LINE_SET, Absolute mouse movement, set the mouse position", LIB, ( # Desc Opt Var type p1_val p2_val @@ -327,7 +327,7 @@ def __init__( self, ): - super().__init__("M_STORE, Store the mouse position", + super().__init__("M_STORE, Store the mouse position, optionally into named variables", LIB, ( # Desc Opt Var type p1_val p2_val diff --git a/commands_pause.py b/commands_pause.py index 27ddde1..4e187ce 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -12,7 +12,7 @@ def __init__( self, ): - super().__init__("DELAY") # the name of the command as you have to enter it in the code + super().__init__("DELAY, Pause the script") def Validate( self, diff --git a/commands_scrape.py b/commands_scrape.py index d95d592..4c63da7 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -45,7 +45,6 @@ def get_copied_image(self, btn): except: tries -= 1 btn.Safe_sleep(0.5) - print("retry") return -1 # return the image @@ -76,8 +75,20 @@ def __init__( (5, " captures current form from ({1}, {2}) to ({3}, {4}) into image {5}"), (6, " captures form {6} from ({1}, {2}) to ({3}, {4}) into image {5}"), ) ) - - + + self.deprecated = True + self.deprecated_use = "This command will not exist in the production release. " + \ + "Use S_GET_WIN instead. possibly in combination with the S_OCR command." + + self.doc = ["Captures a part of the screen from (`X1`,`Y1`) to (`X2`,`Y2`), " + "returning this in an image in `Image`.", + "", + "If a window handle (`HWND`) is passed, the coordinates are relative to " + "that window, otherwise the coordinates are screen absolute.", + "" + "The returned image can be passed to other commands that require an image."] + + def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords p_to = (self.Get_param(btn, 3), self.Get_param(btn, 4)) # and the to coords @@ -119,6 +130,14 @@ def __init__( (6, " captures form {6} from ({1}, {2}) to ({3}, {4}) into image {5}"), ) ) + self.doc = ["Captures a part of the screen from (`X1`,`Y1`) to (`X2`,`Y2`), " + "returning this in an image in `Image`.", + "", + "If a window handle (`HWND`) is passed, the coordinates are relative to " + "that window, otherwise the coordinates are screen absolute.", + "" + "The returned image can be passed to other commands that require an image."] + def Process(self, btn, idx, split_line): p_from = (self.Get_param(btn, 1), self.Get_param(btn, 2)) # Get the from coords @@ -155,6 +174,12 @@ def __init__( (1, " place clipboard image into image {1}"), ) ) + self.doc = ["Captures an image from the clipboard. This is typically an image of the " + "most recent field where text has been copied, but it can be from any " + "source.", + "", + "The returned image can be passed to other commands that require an image."] + def Process(self, btn, idx, split_line): image = self.get_copied_image(btn) # get clipboard image @@ -187,6 +212,11 @@ def __init__( (2, " OCR image {1} to {2}"), ) ) + self.doc = ["Performs OCR on an image, returning the text.", + "", + "The image typically comes from one of the other `S_...` commands, " + "but could be sourced from elsewhere."] + def Process(self, btn, idx, split_line): image = self.get_copied_image(btn) # get copied image @@ -209,7 +239,7 @@ def __init__( self, ): - super().__init__("S_HASH", # the name of the command as you have to enter it in the code + super().__init__("S_HASH, returns a hash value that (almost) uniquely identifies an image", LIB, ( # Desc Opt Var type p1_val p2_val @@ -221,6 +251,15 @@ def __init__( (2, " Hash image {1} into {2}"), ) ) + self.doc = ["Creates the hash of an image, returning a value that changes significantly " + "even with small changes to the original image.", + "", + "The image typically comes from one of the other `S_...` commands, " + "but could be sourced from elsewhere.", + "", + "This command is best used as part of a process to determine if 2 images " + "are identical."] + def Process(self, btn, idx, split_line): image = self.Get_param(btn, 1) # get the image @@ -262,6 +301,18 @@ def __init__( (4, " average colour of image {1} in ({2}, {3}, {4})"), ) ) + self.doc = ["Creates an average colour representation of an `Image`, returning " + "the `Red`, `Green`, and `Blue` values coresponding to that average." + "even with small changes to the original image.", + "", + "The image typically comes from one of the other `S_...` commands, " + "but could be sourced from elsewhere.", + "", + "This command is often used as part of a process to determine if a " + "copied field is of a certain colour. Note that because an average " + "colour is created, the comparason is normally to a range of colours, " + "using the S_CDIST command."] + def Process(self, btn, idx, split_line): image = self.Get_param(btn, 1) # get the image @@ -316,6 +367,16 @@ def __init__( (2, " Fingerprint of image {1} into {2}"), ) ) + self.doc = ["Creates a fingerprint of an image, returning a value that is " + "similar for similar images, and relatively insensitive to " + "small differences between images.", + "", + "The image typically comes from one of the other `S_...` commands, " + "but could be sourced from elsewhere.", + "", + "This command is best used as part of a process to determine if 2 images " + "are similar, often usinf the S+FDIST command."] + def Process(self, btn, idx, split_line): image = self.Get_param(btn, 1) # get the image diff --git a/commands_subroutines.py b/commands_subroutines.py index 0d32459..4025a49 100644 --- a/commands_subroutines.py +++ b/commands_subroutines.py @@ -178,7 +178,7 @@ def __init__( Lines # The text of the subroutine/function ): - super().__init__(SUBROUTINE_PREFIX + Name + ", Define a subroutine that can be called with named parameters", + super().__init__(SUBROUTINE_PREFIX + Name + ", A user-defined subroutine", LIB, Params, ( diff --git a/scripts.py b/scripts.py index ed16b92..00dae29 100644 --- a/scripts.py +++ b/scripts.py @@ -179,11 +179,11 @@ def dump_params(c): print(" UNKNOWN VALUE") def dump_source(c, hide_doc): - + def print_source(lines): print(" Source") for i, line in enumerate(lines): - if hide_doc and line.lstrip().split()[:1] in [['@DESC'], ['@DOC'], ['@DOC+']]: + if hide_doc and line.lstrip().split()[:1] in [['@NAME'], ['@DESC'], ['@DOC'], ['@DOC+']]: continue l = f" {i+1:3}: " p = line.lstrip().find(" ") + len(line) - len(line.lstrip()) + 1 @@ -593,7 +593,7 @@ def strip_quoted(line): else: ok, param, pline = strip_quoted(pline) # otherwise we can strip off a quoted string if ok: # and if that suceeded - sline += ['"'+param] # we'll add it as the parameter value. Note we add a leading " to distinguish it from a variable + sline += ['\0'+param] # we'll add it as the parameter value. Note we add a leading null to distinguish it from a variable else: return ('Error in quoted string for param #' + str(n+1), line) # This is generally something to do with the closing quote else: # if we want a quoted string, but value doesn't start with a quote From 0c0170bf951f53f773dcd110db022abd2a904b9e Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 14 Apr 2021 20:34:42 +0800 Subject: [PATCH 74/83] Update to make -f the default (also to remove the -f option) * commands-header.py - modify documentation to remove mention of the -f command line option * LPHK.py - remove the -f option and make variable size text the default (and only) option * run.bat remove use of -f option * window.py - always use variable size fonts on buttons --- LPHK.py | 3 --- commands_header.py | 3 +-- run.bat | 2 +- window.py | 17 ++++++++--------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/LPHK.py b/LPHK.py index b570dc4..5bf9a9f 100755 --- a/LPHK.py +++ b/LPHK.py @@ -116,9 +116,6 @@ def init(): ap.add_argument( # turn of unnecessary verbosity "-q", "--quiet", help = "Disable information popups", action="store_true") - ap.add_argument( # make button text variable in size (default is small) - "-f", "--fit", - help = "Make short button text fit the button", action="store_true") global_vars.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to diff --git a/commands_header.py b/commands_header.py index e4121a4..8a069ad 100644 --- a/commands_header.py +++ b/commands_header.py @@ -207,8 +207,7 @@ def __init__( " text", "", "Shorter text strings are are displayed using larger fonts for greater " - "readability if the option -f or --fit is specified on the command", - "line.", + "readability.", "", "Only a single name header is permitted in a script.", "", diff --git a/run.bat b/run.bat index 4d72b6e..dba08cc 100644 --- a/run.bat +++ b/run.bat @@ -1,2 +1,2 @@ cls -python LPHK.py -l user_layouts\testing.lpl -s Mk1 -M run -q -f +python LPHK.py -l user_layouts\testing.lpl -s Mk1 -M run -q diff --git a/window.py b/window.py index 64f1b3a..b29d1e9 100644 --- a/window.py +++ b/window.py @@ -501,15 +501,14 @@ def txt_col(x, y): return 'white' # otherwise it should be white def txt_font(x, y, round=False): - if global_vars.ARGS['fit']: # only do this of we're fitting text - t = scripts.buttons[x][y].name # get the text - l = len(t) # and its length - if l < 5 and l > 0: # if it's a reasonable size - if round: - return ("Courier", int(0.75 * BUTTON_SIZE / l), "bold") # round buttons need smaller text - else: - return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit - return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size + t = scripts.buttons[x][y].name # get the text + l = len(t) # and its length + if l < 5 and l > 0: # if it's a reasonable size + if round: + return ("Courier", int(0.75 * BUTTON_SIZE / l), "bold") # round buttons need smaller text + else: + return ("Courier", BUTTON_SIZE // l, "bold") # then make it fit + return ("Courier", BUTTON_SIZE // 5, "bold") # otherwise use the standard size gap = int(BUTTON_SIZE // 4) From 6b5479838d43dff76123a1a8f2220e7e74254edd Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 14 Apr 2021 20:54:50 +0800 Subject: [PATCH 75/83] Brought up-to-date with nimaid/LPHK Dev --- commands_scrape.py | 10 ++++++---- utils/launchpad_connector.py | 5 +++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/commands_scrape.py b/commands_scrape.py index 4c63da7..1366ded 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -1,14 +1,16 @@ # This module is VERY specific to Win32 -import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract, io, hashlib, imagehash, dhash +import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract, io, hashlib, imagehash, dhash, shutil from constants import * LIB = "cmds_sscr" # name of this library (for logging) -T_PATH = os.getenv('LOCALAPPDATA') + '/Tesseract-OCR/tesseract.exe' +T_PATH = shutil.which('tesseract') if not os.path.isfile(T_PATH): - T_PATH = os.getenv('PROGRAMFILES') + '/Tesseract-OCR/tesseract.exe' + T_PATH = os.getenv('LOCALAPPDATA') + '/Tesseract-OCR/tesseract.exe' if not os.path.isfile(T_PATH): - raise Exception("Tesseract OCR not installed or cannot be located") + T_PATH = os.getenv('PROGRAMFILES') + '/Tesseract-OCR/tesseract.exe' + if not os.path.isfile(T_PATH): + raise Exception("Tesseract OCR not installed or cannot be located") pytesseract.pytesseract.tesseract_cmd = T_PATH diff --git a/utils/launchpad_connector.py b/utils/launchpad_connector.py index 0f7ec02..942ba63 100644 --- a/utils/launchpad_connector.py +++ b/utils/launchpad_connector.py @@ -76,4 +76,9 @@ def connect(pad): def disconnect(pad): + mode = get_mode(pad) + + if mode == "Mk3": + pad.LedSetMode(0) + pad.Close() From 48144161dd7fbc811c22e6875ede857adb45748f Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:07:41 +0800 Subject: [PATCH 76/83] More changes based on comments in pull request * command_list.py Changed WARNING to INFO as requested * commands_scrape.py Corrected error introduced with checking for Tesseract on the path * files.py - added check for version number before checking for subroutines --- command_list.py | 2 +- commands_scrape.py | 2 +- files.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/command_list.py b/command_list.py index 0952aa0..69206fb 100644 --- a/command_list.py +++ b/command_list.py @@ -30,7 +30,7 @@ try: import commands_rpncalc except ImportError: - print("[LPHK] WARNING: RPN_EVAL command is not available") + print("[LPHK] INFO: RPN_EVAL command is not available") traceback.print_exc() # This library could be considered optional, and is also platform specific diff --git a/commands_scrape.py b/commands_scrape.py index 1366ded..3203529 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -5,7 +5,7 @@ LIB = "cmds_sscr" # name of this library (for logging) T_PATH = shutil.which('tesseract') -if not os.path.isfile(T_PATH): +if T_PATH == None or not os.path.isfile(T_PATH): # T_PATH will be None if tesseract is not on the path T_PATH = os.getenv('LOCALAPPDATA') + '/Tesseract-OCR/tesseract.exe' if not os.path.isfile(T_PATH): T_PATH = os.getenv('PROGRAMFILES') + '/Tesseract-OCR/tesseract.exe' diff --git a/files.py b/files.py index 76e27c7..f754f2a 100644 --- a/files.py +++ b/files.py @@ -153,9 +153,10 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): layout = preload # load subroutines before buttons so you don't get errors on buttons using them - if "subroutines" in layout: # were subroutines saved? - for sub in layout["subroutines"]: # for all the subroutines that were saved - load_subroutine(sub, 0, 'LAYOUT') # load the subroutine + if layout["version"] >= FILE_VERSION_SUBS: # if it's a version that might have subroutines + if "subroutines" in layout: # and it has subroutines + for sub in layout["subroutines"]: # for all the subroutines that were saved + load_subroutine(sub, 0, 'LAYOUT')# load the subroutine for x in range(9): for y in range(9): From c462c1e521a8ac98094d338e560558fe1a86367d Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 16 Apr 2021 16:04:26 +0800 Subject: [PATCH 77/83] Deprecating the @Load_Layout header in exchange for adding new command LOAD_HEADER * commands_header.py - marking @LOAD_HEADER as deprecated * commands_file.py - Creating new command LOAD_HEADER --- commands_file.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ commands_header.py | 3 +++ 2 files changed, 53 insertions(+) diff --git a/commands_file.py b/commands_file.py index 2b08398..4c20858 100644 --- a/commands_file.py +++ b/commands_file.py @@ -191,3 +191,53 @@ def Process(self, btn, idx, split_line): scripts.Add_command(File_Ensure_Path_Exists()) # register the command +# ################################################## +# ### CLASS File_Load_Layout ### +# ################################################## + +# Loads a new layout. Command rather than header format (doesn't have the F_ prefix for historical reasons) +class File_Load_Layout(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("LOAD_LAYOUT, Loads a new layout", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Layout", False, AVV_YES, PT_STR, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " loads layout {1}"), + ) ) + + self.doc = ["Replaces the current layout with a new one loaded from a layout file."] + + + def Process(self, btn, idx, split_line): + layout_name = self.Get_param(btn, 1) + + layout_path = os.path.join(files.LAYOUT_PATH, layout_name) + if not os.path.isfile(layout_path): + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " ERROR: Layout file does not exist.") + return -1 + + try: + layout = files.load_layout(layout_path, popups=False, save_converted=False) + except files.json.decoder.JSONDecodeError: + print("[cmds_head] " + coords + " Line:" + str(idx+1) + " ERROR: Layout is malformated.") + return -1 + + if files.layout_changed_since_load: + files.save_lp_to_layout(files.curr_layout) + + files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout) + + return idx+1 + + +scripts.Add_command(File_Load_Layout()) # register the header + + + diff --git a/commands_header.py b/commands_header.py index 8a069ad..efb3bfb 100644 --- a/commands_header.py +++ b/commands_header.py @@ -114,6 +114,9 @@ def __init__( super().__init__("@LOAD_LAYOUT") # the name of the header as you have to enter it in the code + self.deprecated = True + self.deprecated_use = "This header should not be used in new scripts. The LOAD_LAYOUT command" + \ + "serves the same function." def Validate( self, From 43c3467a254edf6df3fb777b099f52136a04c974 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sat, 1 May 2021 17:15:18 +0800 Subject: [PATCH 78/83] Minor additions and bug fixes * commands_external.py - Added command OS_USERID to return the user id of the logged on user * commands_scrape.py - add alternate method of determining the dominant (vs average colour of an image using the command S_COLOUR * commands_scrape.py - slight fixes to documentation * commands_scrape.py - new commands to convert colour spaces and to determine brightness * commands_rpncalc.py - added new commands "ABS", "X<0?", and commands to convert to string ">A" and to get the length of a string "LEN" * commands_rpncalc.py - making sure LAST_X is correctly updated and that the documentation reflects this --- commands_external.py | 30 +++++- commands_rpncalc.py | 76 +++++++++++++-- commands_scrape.py | 227 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 296 insertions(+), 37 deletions(-) diff --git a/commands_external.py b/commands_external.py index 52418d9..506e4b2 100644 --- a/commands_external.py +++ b/commands_external.py @@ -225,7 +225,7 @@ def Run( # ################################################## -# ### CLASS External_Code_NOWAIT ### +# ### CLASS External_Code_Nowait ### # ################################################## # class that defines the CODE_NOWAIT command (runs something). This returns immediately @@ -264,3 +264,31 @@ def Process(self, btn, idx, split_line): scripts.Add_command(External_Code_Nowait()) # register the command +# ################################################## +# ### CLASS External_Os_Userid ### +# ################################################## + +# class that defines the OS_USERID command (returns operating system user id) +class External_Os_Userid(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("OS_USERID, Returns the userid of the currently logged on user", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("UserID", False, AVV_REQD, PT_STR, None, None), # variable to receive user_id + ), + ( + # num params, format string (trailing comma is important) + (1, " Return currently logged on User ID in {1}"), + ) ) + + def Process(self, btn, idx, split_line): + self.Set_param(btn, 1, os.getlogin()) # return the logged in user id + + +scripts.Add_command(External_Os_Userid()) # register the command + + diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 243a438..e25faff 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -181,6 +181,7 @@ def Register_operators(self): self.operators["1/X"] = (self.one_on_x, 0) # 1/x self.operators["INT"] = (self.int_x, 0) # integer portion of x self.operators["FRAC"] = (self.frac_x, 0) # fractional part of x + self.operators["ABS"] = (self.abs, 0) # replace x with the absolute value of x self.operators["CHS"] = (self.chs, 0) # change sign of top of stack self.operators["SQR"] = (self.sqr, 0) # **2 self.operators["Y^X"] = (self.y_to_x, 0) # ** @@ -199,6 +200,7 @@ def Register_operators(self): self.operators["Y?"] = (self.x_gt_y, 0) # is x > y? @@ -215,6 +217,8 @@ def Register_operators(self): self.operators["SUBSTR"] = (self.substr, 0) # x gets str(z)[x:y] self.operators["D>J"] = (self.d_to_j, 0) # converts string date to julian (actually ordinal) self.operators["J>D"] = (self.j_to_d, 0) # converts a julian to a text date + self.operators[">A"] = (self.to_alpha, 0) # converts X to alpha + self.operators["LEN"] = (self.len, 0) # replaces x with length of string representation of x def add(self, @@ -432,8 +436,23 @@ def chs(self, symbols, cmd, cmds): return ret + def abs(self, symbols, cmd, cmds): + """Replace the value on the top of the stack with the absolute value. The previous value of X is placed in LASTX.""" + + ret = 1 + a = variables.pop(symbols) + symbols[SYM_LOCAL]['last x'] = a + + try: + variables.push(symbols, abs(a)) + except: + raise Exception("Error in abs: " + str(a)) # Errors are highly improbable here + + return ret + + def sqr(self, symbols, cmd, cmds): - """Replaces the value on the top of the stack its square. The previous value of X is placed in LASTX""" + """Replaces the value on the top of the stack its square. The previous value of X is placed in LASTX.""" # calculates the square ret = 1 @@ -470,7 +489,7 @@ def y_to_x(self, symbols, cmd, cmds): def dup(self, symbols, cmd, cmds): - """Duplicates the top value on the stack. Sometimes also known as `ENTER^`""" + """Duplicates the top value on the stack. Sometimes also known as `ENTER^`. LASTX is not modified.""" # duplicates the value on the top of the stack ret = 1 @@ -515,7 +534,7 @@ def last_x(self, symbols, cmd, cmds): def cl_l(self, symbols, cmd, cmds): - """Clears all local variables""" + """Clears all local variables, including LASTX""" # clears the stack ret = 1 @@ -525,7 +544,7 @@ def cl_l(self, symbols, cmd, cmds): def stack_len(self, symbols, cmd, cmds): - """Pushes the length of the stack onto the stack. Note that after a CLST the length of the stack is Zero.""" + """Pushes the length of the stack onto the stack. Note that after a CLST the length of the stack is Zero. LASTX is not modified.""" # returns stack length ret = 1 @@ -550,7 +569,7 @@ def swap_x_y(self, symbols, cmd, cmds): def sto(self, symbols, cmd, cmds): - """Stores the top value on the stack into a variable. If a local variable of that name exists it will be used; otherwise if a global variable of that name exists, it will be used; finally if neither exist, a local variable will be created to contain the value. Neither the stsack or LASTX is modified.""" + """Stores the top value on the stack into a variable. If a local variable of that name exists it will be used; otherwise if a global variable of that name exists, it will be used; finally if neither exist, a local variable will be created to contain the value. Neither the stack or LASTX is modified.""" # stores the value in local var if it exists, otherwise global var. If neither, creates local ret = 1 @@ -563,7 +582,7 @@ def sto(self, symbols, cmd, cmds): def sto_g(self, symbols, cmd, cmds): - """Stores the top value on the stack into a global variable. If it does not exist, it will be created. Neither the stsack or LASTX is modified.""" + """Stores the top value on the stack into a global variable. If it does not exist, it will be created. Neither the stack or LASTX is modified.""" # stores the value on the top of the stack into the global variable named by the next token ret = 1 @@ -576,7 +595,7 @@ def sto_g(self, symbols, cmd, cmds): def sto_l(self, symbols, cmd, cmds): - """Stores the top value on the stack into a local variable. If it does not exist, it will be created. Neither the stsack or LASTX is modified.""" + """Stores the top value on the stack into a local variable. If it does not exist, it will be created. Neither the stack or LASTX is modified.""" # stores the value on the top of the stack into the local variable named by the next token ret = 1 @@ -644,6 +663,16 @@ def x_ne_zero(self, symbols, cmd, cmds): return len(cmds)+1 + def x_lt_zero(self, symbols, cmd, cmds): + """if X is less than zero, the evaluation continues, otherwise it stops.""" + + # only continues eval if the top of the stack is not 0 + if variables.top(symbols, 1) < 0: + return 1 + else: + return len(cmds)+1 + + def x_eq_y(self, symbols, cmd, cmds): """if X is equal to Y, the evaluation continues, otherwise it stops.""" @@ -788,7 +817,7 @@ def abort_script(self, symbols, cmd, cmds): def substr(self, symbols, cmd, cmds): - """Removes the top thee values from the stack, pushing the characters from Y to X-1 of Z onto the top of the stack.""" + """Removes the top thee values from the stack, pushing the characters from Y to X-1 of Z onto the top of the stack. The previous value of X is placed in LASTX""" # does a substring x = variables.pop(symbols) @@ -797,12 +826,13 @@ def substr(self, symbols, cmd, cmds): r = str(z)[y:x] variables.push(symbols, r) + symbols[SYM_LOCAL]['last x'] = x return 1 def d_to_j(self, symbols, cmd, cmds): - """Converts a text date on the top of the stack to the days since Jan-1-1900.""" + """Converts a text date on the top of the stack to the days since Jan-1-1900. The previous value of X is placed in LASTX.""" # converts a text date on the top of the stack to a julian date (integer) d = variables.pop(symbols) @@ -811,12 +841,13 @@ def d_to_j(self, symbols, cmd, cmds): j = dt.toordinal() variables.push(symbols, j) + symbols[SYM_LOCAL]['last x'] = d return 1 def j_to_d(self, symbols, cmd, cmds): - """Converts the days since Jan-1-1900 in on the top of the stack to a text date.""" + """Converts the days since Jan-1-1900 in on the top of the stack to a text date. The previous value of X is placed in LASTX.""" # converts a julian date (integer) on the top of the stack to a text date j = variables.pop(symbols) @@ -825,6 +856,31 @@ def j_to_d(self, symbols, cmd, cmds): d = dt.strftime("%d-%b-%Y") variables.push(symbols, d) + symbols[SYM_LOCAL]['last x'] = j + + return 1 + + + def to_alpha(self, symbols, cmd, cmds): + """Replaces the value in X with its string representation. The previous value of X is placed in LASTX.""" + + x = variables.pop(symbols) + ax = str(x) + + variables.push(symbols, ax) + symbols[SYM_LOCAL]['last x'] = x + + return 1 + + + def len(self, symbols, cmd, cmds): + """Replaces the value in X with the length of its string representation. The previous value of X is placed in LASTX.""" + + x = variables.pop(symbols) + lax = len(str(x)) + + variables.push(symbols, lax) + symbols[SYM_LOCAL]['last x'] = x return 1 diff --git a/commands_scrape.py b/commands_scrape.py index 3203529..03eda54 100644 --- a/commands_scrape.py +++ b/commands_scrape.py @@ -1,5 +1,5 @@ # This module is VERY specific to Win32 -import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract, io, hashlib, imagehash, dhash, shutil +import os, command_base, ms, kb, scripts, variables, win32gui, commands_win32, PIL, pytesseract, io, hashlib, imagehash, dhash, shutil, colorsys from constants import * LIB = "cmds_sscr" # name of this library (for logging) @@ -283,8 +283,14 @@ def Process(self, btn, idx, split_line): # ### CLASS S_IMAGE_COLOUR ### # ################################################## +# constants for S_IMAGE_COLOUR +SIC_MEAN = "MEAN" +SIC_MODAL = "MODAL" + +SICG_ALL = [SIC_MEAN, SIC_MODAL] + # class that defines the S_COLOUR command -- Takes an image and calculates a checksum -class Scrape_Clipboard_Colour(Command_Scrape): +class Scrape_Image_Colour(Command_Scrape): def __init__( self, ): @@ -297,10 +303,12 @@ def __init__( ("Red", False, AVV_REQD, PT_INT, None, None), ("Green", False, AVV_REQD, PT_INT, None, None), ("Blue", False, AVV_REQD, PT_INT, None, None), + ("Method", True, AVV_NO, PT_WORD, None, None), ), ( # num params, format string (trailing comma is important) (4, " average colour of image {1} in ({2}, {3}, {4})"), + (5, " average colour of image {1} in ({2}, {3}, {4}) using method {5}"), ) ) self.doc = ["Creates an average colour representation of an `Image`, returning " @@ -318,33 +326,58 @@ def __init__( def Process(self, btn, idx, split_line): image = self.Get_param(btn, 1) # get the image + method = self.Get_param(btn, 5, SIC_MEAN) pixels = image.load() # create the pixel map - r = 0 # initialise RGB and pixel count to 0 - g = 0 - b = 0 - p = 0 - - for i in range(image.size[0]): # for every col: - for j in range(image.size[1]): # for every row - px = pixels[i, j] - p += 1 - r += px[0] - g += px[1] - b += px[2] - - if p > 0: # calc average data - r = r // p - g = g // p - b = b // p - + if method == SIC_MEAN: + r = 0 # initialise RGB and pixel count to 0 + g = 0 + b = 0 + p = 0 + + for i in range(image.size[0]): # for every col: + for j in range(image.size[1]): # for every row + px = pixels[i, j] + p += 1 + r += px[0] + g += px[1] + b += px[2] + + if p > 0: # calc average data + r = r // p + g = g // p + b = b // p + + elif method == SIC_MODAL: + #Get colors from image object + pixels = image.getcolors(image.size[0] * image.size[1]) + #Sort them by count number(first element of tuple) + sorted_pixels = sorted(pixels, key=lambda t: t[0]) + #Get the most frequent color + dominant_colour = sorted_pixels[-1][1] + + r = dominant_colour[0] + g = dominant_colour[1] + b = dominant_colour[2] + self.Set_param(btn, 2, r) # send back RGB self.Set_param(btn, 3, g) self.Set_param(btn, 4, b) -scripts.Add_command(Scrape_Clipboard_Colour()) # register the command + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if (len(split_line) > 5) and (not split_line[5] in SICG_ALL): # invalid subcommand + c_ok = ', '.join(SICG_ALL[:-1]) + ', or ' + SICG_ALL[-1] + s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret + + +scripts.Add_command(Scrape_Image_Colour()) # register the command # ################################################## @@ -414,8 +447,8 @@ def __init__( (3, " Return the hamming distance between fingerprints {1} and {2} into {3}"), ) ) - self.doc = ["This command calculates the hamming distance between 2 fingerprints. ", - "This can be used to determine how similar 2 images are. The larger ", + self.doc = ["This command calculates the hamming distance between 2 fingerprints. " + "This can be used to determine how similar 2 images are. The larger " "the hamming distance, the more different the images are."] @@ -458,9 +491,8 @@ def __init__( (7, " Return the hamming distance between colours ({1}, {2}, {3}) and ({4}, {5}, {6}) into {7}"), ) ) - self.doc = ["This command calculates the hamming distance between 2 RGB values. ", - "This can be used to determine how similar 2 colours are. The larger ", - "the hamming distance, the more different the colours are."] + self.doc = ["This can be used to determine how similar 2 colours are. The larger " + "the distance, the more different the colours are."] def Process(self, btn, idx, split_line): @@ -478,3 +510,146 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Scrape_Colour_Distance()) # register the command + + +# ################################################## +# ### CLASS SCRAPE_RGB_TO_HSV ### +# ################################################## + +# class that defines the S_RGB_TO_HSV command -- converts RGB to an HSV colour space +class Scrape_Rgb_To_Hsv(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("S_RGB_TO_HSV, Converts RGB to an HSV colour space", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("R", False, AVV_YES, PT_INT, None, None), + ("G", False, AVV_YES, PT_INT, None, None), + ("B", False, AVV_YES, PT_INT, None, None), + ("H", False, AVV_REQD, PT_FLOAT, None, None), + ("S", False, AVV_REQD, PT_FLOAT, None, None), + ("V", False, AVV_REQD, PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (6, " converts RGB ({1}, {2}, {3}) to HSV ({4}, {5}, {6})"), + ) ) + + self.doc = ["This command converts an RGB value into an HSV value. " + "It can be used to determine the Hue, Saturation, and/or " + "Value (Brightness) of a colour.", + "", + "All values are in the range 0..255"] + + + def Process(self, btn, idx, split_line): + + r = self.Get_param(btn, 1) # get the colours + g = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255) + + self.Set_param(btn, 4, int(h*255)) + self.Set_param(btn, 5, int(s*255)) + self.Set_param(btn, 6, int(v*255)) + + +scripts.Add_command(Scrape_Rgb_To_Hsv()) # register the command + + +# ################################################## +# ### CLASS SCRAPE_RGB_TO_HSL ### +# ################################################## + +# class that defines the S_RGB_TO_HSL command -- converts RGB to an HSL colour space +class Scrape_Rgb_To_Hsl(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("S_RGB_TO_HSL, Converts RGB to an HSL colour space", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("R", False, AVV_YES, PT_INT, None, None), + ("G", False, AVV_YES, PT_INT, None, None), + ("B", False, AVV_YES, PT_INT, None, None), + ("H", False, AVV_REQD, PT_FLOAT, None, None), + ("S", False, AVV_REQD, PT_FLOAT, None, None), + ("L", False, AVV_REQD, PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (6, " converts RGB ({1}, {2}, {3}) to HSV ({4}, {5}, {6})"), + ) ) + + self.doc = ["This command converts an RGB value into an HSL value. " + "It can be used to determine the Hue, Saturation, and/or " + "Luminance of a colour.", + "", + "All values are in the range 0..255"] + + + def Process(self, btn, idx, split_line): + + r = self.Get_param(btn, 1) # get the colours + g = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + h, l, s = colorsys.rgb_to_hls(r/255, g/255, b/255) + + self.Set_param(btn, 4, int(h*255)) + self.Set_param(btn, 5, int(s*255)) + self.Set_param(btn, 6, int(l*255)) + + +scripts.Add_command(Scrape_Rgb_To_Hsl()) # register the command + + +# ################################################## +# ### CLASS SCRAPE_RGB_TO_Brightness ### +# ################################################## + +# class that defines the S_RGB_TO_B command -- an alternative routine to calculate the perceptual brightness of a colour +class Scrape_Rgb_To_Brightness(command_base.Command_Basic): + def __init__( + self, + ): + + super().__init__("S_RGB_TO_B, An alternative routine to calculate the perceptual brightness of a colour", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("R", False, AVV_YES, PT_INT, None, None), + ("G", False, AVV_YES, PT_INT, None, None), + ("B", False, AVV_YES, PT_INT, None, None), + ("Bright", False, AVV_REQD, PT_FLOAT, None, None), + ), + ( + # num params, format string (trailing comma is important) + (4, " converts RGB ({1}, {2}, {3}) to Brightness ({4})"), + ) ) + + self.doc = ["This command converts an RGB value into an alternative brightness " + "value. It can be used to determine Value (Brightness) of a colour " + "in a more perceptually correct manner.", + "", + "All values are in the range 0..255"] + + + def Process(self, btn, idx, split_line): + r = self.Get_param(btn, 1) # get the colours + g = self.Get_param(btn, 2) + b = self.Get_param(btn, 3) + + br = 0.243863271*r + 0.673319569*g + 0.08281716*b + v = cmax * 100 + + self.Set_param(btn, 4, int(br)) + + +scripts.Add_command(Scrape_Rgb_To_Brightness()) # register the command From 3494ebb26b77fe556432007cabf2e4d04bf619bc Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 9 May 2021 09:27:02 +0800 Subject: [PATCH 79/83] Added the very obviously missing command to place the contents of a variable onto the clipboard (rather than copying it directly) --- commands_win32.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/commands_win32.py b/commands_win32.py index 213077b..018afef 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -671,6 +671,40 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Win32_Paste()) # register the command +# ################################################## +# ### CLASS W_COPY_VAR ### +# ################################################## + +# class that defines the W_COPY_VAR command - place a variable into the clipboard +class Win32_Copy_Var(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_COPY_VAR, Place data into the clipboard", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Data", False, AVV_YES, PT_STR, None, None), # variable to contain item to paste + ), + ( + # num params, format string (trailing comma is important) + (1, " Place {1} into the system clipboard"), + ) ) + + def Process(self, btn, idx, split_line): + + hwnd = win32gui.GetForegroundWindow() # get the current window + c = self.Get_param(btn, 1) # get the value + + ClearClipboard() + + SetClipboard(str(c)) + + +scripts.Add_command(Win32_Copy_Var()) # register the command + + # ################################################## # ### CLASS W_WAIT ### # ################################################## From 9baae68019cbd3c06023289364474d0280a8e3af Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Thu, 10 Jun 2021 15:26:20 +0800 Subject: [PATCH 80/83] Faster loading for layouts. If layouts have a lot of subroutines, or the scripts in the buttons are huge, it can take a considerable amount of time to load them. An optional keyword has been added to the LAOAD_LAYOUT command to control the amount of validation and subroutine magic. The parameter "NORMAL" (default) does the normal thing, unloading subroutines, reloading the ones in the layout, loading the layout and checking both the subroutines and buttons. The keyword "FAST" retains the existing subroutines and does not load any specified in the new layout. However the loaded buttons are checked for correctness (and will fail at this if they call a subroutine that is not loaded. To use this successfully, you should ensure that all of your layouts have the same set of subroutines loaded within them. The final option "UNCHECKED" does the same as "FAST", but does NOT check buttons for correctness. Unchecked is significantly faster than "FAST" where there are a lot of buttons, or the buttons have long scripts.py. With "UNCHECKED" parts of the script are still checked. The parts that are checked are the headers since these are the lines that set titles and help strings. --- commands_file.py | 31 +++++++++++++++++++++++++++--- files.py | 17 ++++++++-------- scripts.py | 50 ++++++++++++++++++++++++++++-------------------- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/commands_file.py b/commands_file.py index 4c20858..32570ed 100644 --- a/commands_file.py +++ b/commands_file.py @@ -1,5 +1,5 @@ # This module contains commands that work on the filesystem -import os, command_base, ms, kb, scripts, traceback, pathlib +import os, command_base, ms, kb, scripts, traceback, pathlib, files from constants import * LIB = "cmds_file" # name of this library (for logging) @@ -195,6 +195,13 @@ def Process(self, btn, idx, split_line): # ### CLASS File_Load_Layout ### # ################################################## +# constants for LOAD_LAYOUT +LL_NORMAL = "NORMAL" +LL_FAST = "FAST" +LL_UNCHECKED = "UNCHECKED" + +LLG_ALL = [LL_NORMAL, LL_FAST, LL_UNCHECKED] + # Loads a new layout. Command rather than header format (doesn't have the F_ prefix for historical reasons) class File_Load_Layout(command_base.Command_Basic): def __init__( @@ -206,10 +213,12 @@ def __init__( ( # Desc Opt Var type p1_val p2_val ("Layout", False, AVV_YES, PT_STR, None, None), + ("Method", True, AVV_NO, PT_WORD, None, None), ), ( # num params, format string (trailing comma is important) (1, " loads layout {1}"), + (2, " loads layout {1} with option {2}"), ) ) self.doc = ["Replaces the current layout with a new one loaded from a layout file."] @@ -217,6 +226,7 @@ def __init__( def Process(self, btn, idx, split_line): layout_name = self.Get_param(btn, 1) + method = self.Get_param(btn, 2, LL_NORMAL) layout_path = os.path.join(files.LAYOUT_PATH, layout_name) if not os.path.isfile(layout_path): @@ -232,12 +242,27 @@ def Process(self, btn, idx, split_line): if files.layout_changed_since_load: files.save_lp_to_layout(files.curr_layout) - files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout) + if method == LL_NORMAL: + files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout) + elif method == LL_FAST: + files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout, load_subroutines=False) + elif method == LL_UNCHECKED: + files.load_layout_to_lp(layout_path, popups=False, save_converted=False, preload=layout, load_subroutines=False, check_subroutines=False) return idx+1 -scripts.Add_command(File_Load_Layout()) # register the header + def Partial_validate_step_pass_1(self, ret, btn, idx, split_line): + ret = super().Partial_validate_step_pass_1(ret, btn, idx, split_line) # perform the original pass 1 validation + if ret == None or ((type(ret) == bool) and ret): # if the original validation hasn't raised an error + if (len(split_line) > 2) and (not split_line[2] in LLG_ALL): # invalid subcommand + c_ok = ', '.join(LLG_ALL[:-1]) + ', or ' + LLG_ALL[-1] + s_err = f"Invalid subcommand {split_line[1]} when expecting {c_ok}." + return (s_err, btn.Line(idx)) + return ret + + +scripts.Add_command(File_Load_Layout()) # register the header diff --git a/files.py b/files.py index f754f2a..b1cd3e5 100644 --- a/files.py +++ b/files.py @@ -136,7 +136,7 @@ def save_lp_to_layout(name): save_layout(layout=layout, name=name) -def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): +def load_layout_to_lp(name, popups=True, save_converted=True, preload=None, load_subroutines=True, check_subroutines=True): global curr_layout global in_error global layout_changed_since_load @@ -144,7 +144,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): converted_to_rg = False scripts.Unbind_all() - scripts.Unload_all() # remove all existing subroutines when you load a new layout + scripts.Unload_all(unload_subroutines=load_subroutines) # remove all existing subroutines when you load a new layout window.Redraw(True) if preload == None: @@ -153,10 +153,11 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): layout = preload # load subroutines before buttons so you don't get errors on buttons using them - if layout["version"] >= FILE_VERSION_SUBS: # if it's a version that might have subroutines - if "subroutines" in layout: # and it has subroutines - for sub in layout["subroutines"]: # for all the subroutines that were saved - load_subroutine(sub, 0, 'LAYOUT')# load the subroutine + if load_subroutines: + if layout["version"] >= FILE_VERSION_SUBS: # if it's a version that might have subroutines + if "subroutines" in layout: # and it has subroutines + for sub in layout["subroutines"]: # for all the subroutines that were saved + load_subroutine(sub, 0, 'LAYOUT')# load the subroutine for x in range(9): for y in range(9): @@ -173,7 +174,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): script_validation = None try: btn = scripts.Button(x, y, script_text) - script_validation = btn.Validate_script() + script_validation = btn.Validate_script(full_validate=check_subroutines) except: new_layout_func = lambda: window.app.unbind_lp(prompt_save = False) if popups: @@ -181,7 +182,7 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None): else: print("[files] Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.") raise - if script_validation != True: + if check_subroutines and (script_validation != True): btn.invalid_on_load = True color = [0, 0, 0] else: diff --git a/scripts.py b/scripts.py index 00dae29..5eb9145 100644 --- a/scripts.py +++ b/scripts.py @@ -331,7 +331,7 @@ def running(self, set_to=None): # Do what is required to parse the script. Parsing does not output any information unless it is an error - def Parse_script(self): + def Parse_script(self, full_parse=True): if self.validated: # we don't want to repeat validation over and over return True @@ -358,18 +358,19 @@ def Parse_script(self): if cmd_txt in VALID_COMMANDS: # if first element is a command command = VALID_COMMANDS[cmd_txt]# get the command itself - split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately + if full_parse or isinstance(command, command_base.Command_Header): + split_line = self.Split_text(command, cmd_txt, line) # now split the line appropriately - if type(split_line) == tuple: - if err == True: - err = split_line - errors += 1 - else: - res = command.Parse(self, idx, split_line, pass_no); - if res != True: + if type(split_line) == tuple: if err == True: - err = res # note the error - errors += 1 # and 1 more error + err = split_line + errors += 1 + else: + res = command.Parse(self, idx, split_line, pass_no); + if res != True: + if err == True: + err = res # note the error + errors += 1 # and 1 more error else: msg = " Invalid command '" + cmd_txt + "' on line " + str(idx+1) + "." if err == True: @@ -378,10 +379,16 @@ def Parse_script(self): errors += 1 # and 1 more error if err != True: - if self.is_button: - print('Pass ' + str(pass_no) + ' complete for button ' + self.coords + '. ' + str(errors) + ' errors detected.') + if full_parse: + if self.is_button: + print('Pass ' + str(pass_no) + ' complete for button ' + self.coords + '. ' + str(errors) + ' errors detected.') + else: + print('Pass ' + str(pass_no) + ' complete for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') else: - print('Pass ' + str(pass_no) + ' complete for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') + if self.is_button: + print('Pass ' + str(pass_no) + ' (partial) for button ' + self.coords + '. ' + str(errors) + ' errors detected.') + else: + print('Pass ' + str(pass_no) + ' (partial) for subroutine ' + self.coords +'. ' + str(errors) + ' errors detected.') # should never happen! break # errors prevent next pass return err # success or failure @@ -754,14 +761,14 @@ def Main_logic(idx): # the main logic t # validating a script consists of doing the checks that we do prior to running, but # we won't run it afterwards. - def Validate_script(self): + def Validate_script(self, full_validate=True): if self.validated or self.script_str == "": # If valid or there is no script... self.validated = True return True # ...validation succeeds! - validation = self.Parse_script() # parse the script + validation = self.Parse_script(full_parse=full_validate) # parse the script if validation == True: # If parsing is OK - self.validated = True # Script is valid + self.validated = full_validate # Script is valid if len(self.script_lines) > 0: # look for async header and set flag cmd_txt = self.Split_cmd_text(self.script_lines[0]) @@ -952,7 +959,7 @@ def Unbind_all(): # Unload all subroutines. -def Unload_all(): +def Unload_all(unload_subroutines=True): kill_all() # stop everything running subs = [] # list of subroutines to remove @@ -960,11 +967,12 @@ def Unload_all(): if cmd.startswith(SUBROUTINE_PREFIX):# if this command is a subroutine subs += [cmd] # add the command to the list - for cmd in subs: # for each subroutine we've found - Remove_command(cmd) # remove it + if unload_subroutines: + for cmd in subs: # for each subroutine we've found + Remove_command(cmd) # remove it files.layout_changed_since_load = True # mark layout as changed - files.validate_all_buttons() # ensure buttons are valid + files.validate_all_buttons() # ensure buttons are valid From da20398b91390b45ad918822bde66600ea62a5b8 Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 11 Jun 2021 16:31:45 +0800 Subject: [PATCH 81/83] Adding a new header command for scripts to set the default colour of the button. This is currently experimental and does not limit the colours to those supported by the attached (or emulated) launchpad. The header @COLOUR allows the colour of the button to be described in the script. The command must be followed by a 3 digit hex constant xyz that is converted to the colour #xxyyzz. As an example, @COLOUR F00 makes the button colour bright red. --- commands_header.py | 58 ++++++++++++++++++++++++++++++++++++++++++++-- files.py | 2 ++ scripts.py | 6 +++++ window.py | 6 +++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/commands_header.py b/commands_header.py index efb3bfb..2dbecfa 100644 --- a/commands_header.py +++ b/commands_header.py @@ -1,4 +1,4 @@ -import command_base, kb, lp_events, scripts +import command_base, kb, lp_events, scripts, lp_colors # ################################################## @@ -417,4 +417,58 @@ def Validate( return True -scripts.Add_command(Header_Doc()) # register the header \ No newline at end of file +scripts.Add_command(Header_Doc()) # register the header + +# ################################################## +# ### CLASS Header_Colour ### +# ################################################## + +# The Deprecated header marks a routine as deprecated with any additional text +# placed in the "use" description. +class Header_Colour(command_base.Command_Header): + def __init__( + self, + ): + + super().__init__("@COLOUR, Sets (overrides) the default buton colour") + + self.doc = ["The `@COLOUR` header allows a routine to be assigned a default button colour.", + "", + "Some simple examples follows", + "", + " @COLOUR f00", + " @COLOUR fff", + "", + "When the routine is parsed, the default button colour is set to the RGB value specified by a 3 character hex constant.", + "", + "If repeated, this the colour will be set to the last specified colour"] + + + # Validate routine. Adds the deprecated information if the command is not already deprecated! + def Validate( + self, + btn, + idx: int, # The current line number + split_line, # The current line, split + pass_no # interpreter pass (1=gather symbols & check syntax, 2=check symbol references) + ): + + if pass_no == 1: + if len(split_line) <= 1: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' must have a colour specified.", btn.Line(idx)) + elif len(split_line) > 2: + return ("Line:" + str(idx+1) + " - The header '" + split_line[0] + "' must have only 1 colour specified.", btn.Line(idx)) + else: + if len(split_line[1]) != 3: + return (f"Line:{idx+1} - The header '{split_line[0]}' has a colour {split_line[1]} that is not a 3 character hex value for RGB.", btn.Line(idx)) + try: + cv = int('0x' + split_line[1], 16) + except: + return (f"Line:{idx+1} - The header '{split_line[0]}' has a colour {split_line[1]} that is not a 3 character hex value for RGB.", btn.Line(idx)) + c = split_line[1].lower() + btn.colour = lp_colors.Hex_to_RGB(c) + + return True + + +scripts.Add_command(Header_Colour()) # register the header \ No newline at end of file diff --git a/files.py b/files.py index b1cd3e5..d932c29 100644 --- a/files.py +++ b/files.py @@ -187,6 +187,8 @@ def load_layout_to_lp(name, popups=True, save_converted=True, preload=None, load color = [0, 0, 0] else: btn.invalid_on_load = False + if btn.colour != None: + color = btn.colour scripts.Bind(x, y, btn, color) else: lp_colors.setXY(x, y, color) diff --git a/scripts.py b/scripts.py index 5eb9145..af0d79f 100644 --- a/scripts.py +++ b/scripts.py @@ -280,6 +280,7 @@ def __init__( self.y = y self.is_button = x >= 0 and y >= 0 # It's a button if it has valid (non-negative) coordinates, otherwise it must be a subroutine self.script_str = script_str # The script + self.colour = None # default is no colour self.name = None self.Set_name(name) # only for subroutines at present, but useful to print a caption for a button? @@ -335,6 +336,8 @@ def Parse_script(self, full_parse=True): if self.validated: # we don't want to repeat validation over and over return True + self.colour = None # no colour from the script + if self.script_lines == None: # A little setup if the script lines are not created if isinstance(self.script_str, list): # Subroutines already have this as a list of lines self.script_lines = self.script_str # Copy the lines @@ -762,6 +765,9 @@ def Main_logic(idx): # the main logic t # validating a script consists of doing the checks that we do prior to running, but # we won't run it afterwards. def Validate_script(self, full_validate=True): + if not self.validated: # reset script-nominated colour before validation + self.colour = None + if self.validated or self.script_str == "": # If valid or there is no script... self.validated = True return True # ...validation succeeds! diff --git a/window.py b/window.py index b29d1e9..546e9a7 100644 --- a/window.py +++ b/window.py @@ -719,6 +719,10 @@ def validate_func(): self.save_script(w, x, y, text_string) else: btn.invalid_on_load = False + #if btn.colour != None: # there should be no reason to do this here unless we want an unsuccessful edit to return the button to its original colour + # colors_to_set[x][y] = btn.colour + # lp_colors.updateXY(x, y) + # print("set Colour", btn.colour) w.destroy() w.protocol("WM_DELETE_WINDOW", validate_func) @@ -910,6 +914,8 @@ def open_editor_func(): if script_validate == True: if script_text != "": script_text = files.strip_lines(script_text) + if btn.colour != None: # is there a script-coded colour for the button? + colors_to_set[x][y] = btn.colour # if so, override the user's choice of colour scripts.Bind(x, y, script_text, colors_to_set[x][y]) Redraw(x, y) lp_colors.updateXY(x, y) From 42e68314cf3d3ef8d8af1271090efc79acf784cc Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Fri, 11 Jun 2021 17:46:36 +0800 Subject: [PATCH 82/83] Oops, forgot to include the slightly modified lp_colors.py And now @COLOUR has an alias @COLOR --- commands_header.py | 17 ++++++++++++++++- lp_colors.py | 14 +++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/commands_header.py b/commands_header.py index 2dbecfa..8273598 100644 --- a/commands_header.py +++ b/commands_header.py @@ -471,4 +471,19 @@ def Validate( return True -scripts.Add_command(Header_Colour()) # register the header \ No newline at end of file +scripts.Add_command(Header_Colour()) # register the header + + +class Header_Color(Header_Colour): + def __init__( + self, + ): + + super().__init__() + self.name = '@COLOR' + self.desc = self.desc.replace('@COLOUR', '@COLOR').replace('olour', 'olor') + for i in range(len(self.doc)): + self.doc[i] = self.doc[i].replace('@COLOUR', '@COLOR').replace('olour', 'olor') + + +scripts.Add_command(Header_Color()) # register the header \ No newline at end of file diff --git a/lp_colors.py b/lp_colors.py index 8a1ca8d..ee88df9 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -11,6 +11,14 @@ def init(lp_object_in): global lp_object lp_object = lp_object_in # this function just stores the object in a global variable (what?!) +def Hex_to_RGB(code): + # Used to convert 3 hex digits to a colour + rgb = [] + for c in range(3): + val = code[c] + rgb.append(int(val + val, 16)) + return rgb + def code_to_RGB(code): # Used to convert old layouts to the new format only RGB = {0: "#000", @@ -44,11 +52,7 @@ def code_to_RGB(code): 48: "#96f", 49: "#64a", 50: "#325"} - rgb = [] - for c in range(3): - val = RGB[code][c + 1] - rgb.append(int(val + val, 16)) - return rgb + return Hex_to_RGB(RGB[code][1:]) def RGB_to_RG(rgb): if rgb[2] != 0: From 75f363d8d7c47ac63ec6533948aad2d2e328037e Mon Sep 17 00:00:00 2001 From: Steve-the-wonder-dog <69791362+Steve-the-wonder-dog@users.noreply.github.com> Date: Sun, 27 Jun 2021 16:45:31 +0800 Subject: [PATCH 83/83] Bug fixes and improvements to command interfaces * Created GOTO and deprecated GOTO_LABEL - it just makes sense! * fixed bugs in reporting in the commands_mouse routines * commands_win32 makes more extensive use of pyperclip and some other reliability improvements * rearranged order of parameters in the various window finding commands and making more parameters optional to make the simple use cases more simple to implement * rearranged parameters on W_COPY to make them more sensible & make the simple use case simpler * improved reliability of W_PASTE (test!) * improved reliability for W_COPY_VAR * make window handle optional for W_WAIT * fake launchpad now implements LedCtrlFlashXYByCode(self, x, y, z) (bug fix) * Safe_sleep now has a default time of DELAY_EXIT_CHECK. Note that configuring DELAY_EXIT_CHECK to be too brief will make copy/paste commands unreliable NOTE: if you have a launchpad attached, for some reason you can't run a button by mouse clicking on it. This may or may not be a feature, since clicking on a button changes focus and this will mess up your scripts unless they can deal with this. --- commands_control.py | 29 +++++++++ commands_mouse.py | 8 +-- commands_win32.py | 146 ++++++++++++++++++++++++++------------------ launchpad_fake.py | 3 + scripts.py | 4 +- 5 files changed, 124 insertions(+), 66 deletions(-) diff --git a/commands_control.py b/commands_control.py index 7d9a43a..2c3a60a 100644 --- a/commands_control.py +++ b/commands_control.py @@ -333,11 +333,40 @@ def __init__( # num params, format string (trailing comma is important) (1, " Goto label {1}"), ) ) # don't even need the additional parameters! + + self.deprecated = True + self.deprecated_use = "This command is not recommended for new scripts. Please use the more terse `GOTO` command instead." scripts.Add_command(Control_Goto_Label()) # register the command +# ################################################## +# ### CLASS Control_Goto ### +# ################################################## + +# class that defines the GOTO command +class Control_Goto(Control_Flow_Basic): + def __init__( + self + ): + + super().__init__( + "GOTO, Unconditional jump to label", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("Label", False, AVV_NO, PT_LABEL, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Goto {1}"), + ) ) # don't even need the additional parameters! + + +scripts.Add_command(Control_Goto()) # register the command + + # ################################################## # ### CLASS IF ### # ################################################## diff --git a/commands_mouse.py b/commands_mouse.py index bf0eae2..b1e99ef 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -288,9 +288,9 @@ def __init__( def Process(self, btn, idx, split_line): # while this looks like validation, it is just a warning if btn.symbols[SYM_MOUSE] == tuple(): - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) x1, y1 = btn.symbols[SYM_MOUSE] @@ -378,9 +378,9 @@ def __init__( def Process(self, btn, idx, split_line): # while this looks like validation, it is really just the info. Putting it here is easy if btn.symbols[SYM_MOUSE] == tuple(): - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: - print("[" + lib + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) + print("[" + LIB + "] " + btn.coords + " Line:" + str(idx+1) + " Recall mouse position " + str(btn.symbols[SYM_MOUSE])) ms.set_pos(btn.symbols[SYM_MOUSE][0], btn.symbols[SYM_MOUSE][1]) diff --git a/commands_win32.py b/commands_win32.py index 018afef..7270d94 100644 --- a/commands_win32.py +++ b/commands_win32.py @@ -1,6 +1,8 @@ # This module is VERY specific to Win32 import command_base, ms, kb, scripts, variables, win32gui, win32process, win32api, win32con, win32clipboard, win32event, re from constants import * +import pyperclip # pyperclip is cross-platform (better than using windows specific code) + LIB = "cmds_wn32" # name of this library (for logging) @@ -278,12 +280,14 @@ def __init__( # Desc Opt Var type p1_val p2_val ("Title", False, AVV_YES, PT_STR, None, None), # name to search for ("HWND", False, AVV_REQD, PT_INT, None, None), # variable to contain HWND - ("M", False, AVV_REQD, PT_INT, None, None), # number of matches found (if M 0: + r -= 1 + try: + n = win32clipboard.CountClipboardFormats() + break + except: + n = -1 + btn.Safe_sleep() + + btn.Safe_sleep() + + return n + + +def Wait_for_hwnd(btn, hwnd): + tid, pid = win32process.GetWindowThreadProcessId(hwnd) # find the pid + hproc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION , False, pid) # find the process id + + res = win32con.WAIT_TIMEOUT # set the failure mode to timeout + while res == win32con.WAIT_TIMEOUT: # while we're still timing out + res = win32event.WaitForInputIdle(hproc, 20) # wait a little while for window to become idle + if btn.Check_kill(): # check if we've been killed + return False # and die + + return True + # ################################################## # ### CLASS W_COPY ### # ################################################## @@ -572,12 +613,13 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("Success", False, AVV_REQD, PT_INT, None, None), # variable to contain success value ("Clipboard", True, AVV_REQD, PT_STR, None, None), # variable to contain cut item + ("Success", True, AVV_REQD, PT_INT, None, None), # variable to contain success value ), ( # num params, format string (trailing comma is important) - (1, " Copy into system clipboard returning success in {2}"), + (0, " Copy into system clipboard"), + (1, " Copy into system clipboard and {1}"), (2, " Copy into system clipboard and {1}, returning success in {2}"), ) ) @@ -591,36 +633,23 @@ def Process(self, btn, idx, split_line): kb.tap(kb.sp('c')) finally: kb.release(kb.sp('ctrl')) - - import pyperclip # pyperclip is cross-platform (better than using windows specific code) - - success = False - r = 20 - while r > 0: - r -= 1 + + n = -10 + while n < -1: try: n = win32clipboard.CountClipboardFormats() - if n > 0: - break except: - n = -1 - btn.Safe_sleep(0.05) - - if (n <= 0): - self.Set_param(btn, 1, -1) - else: - t = pyperclip.paste() + n += 1 + btn.Safe_sleep() if n <= 0: - print(f'fail {r}') - self.Set_param(btn, 2, None) - self.Set_param(btn, 1, -1) + self.Set_param(btn, 1, None) + self.Set_param(btn, 2, -1) else: t = pyperclip.paste() # get it again t = t.rstrip('\r\n') # remove any line terminators - print(f"success {r} `{t}`") - self.Set_param(btn, 2, t) - self.Set_param(btn, 1, 0) + self.Set_param(btn, 1, t) + self.Set_param(btn, 2, 0) scripts.Add_command(Win32_Copy()) # register the command @@ -651,19 +680,21 @@ def __init__( def Process(self, btn, idx, split_line): if self.Param_count(btn) > 0: # place variable into clipboard if required - hwnd = win32gui.GetForegroundWindow() # get the current window + #hwnd = win32gui.GetForegroundWindow() # get the current window c = self.Get_param(btn, 1) # get the value ClearClipboard() - SetClipboard(str(c)) + WaitClipboard(btn) - # win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste - try: - kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) - kb.tap(kb.sp('v')) - finally: - kb.release(kb.sp('ctrl')) + #win32api.SendMessage(hwnd, win32con.WM_PASTE, 0, 0) # do a paste + #win32gui.SetForegroundWindow(hwnd) + try: + kb.press(kb.sp('ctrl')) # do a ctrl v (because the message version isn't reliable) + kb.tap(kb.sp('v')) + finally: + kb.release(kb.sp('ctrl')) + btn.Safe_sleep() @@ -698,8 +729,8 @@ def Process(self, btn, idx, split_line): c = self.Get_param(btn, 1) # get the value ClearClipboard() - SetClipboard(str(c)) + WaitClipboard(btn) scripts.Add_command(Win32_Copy_Var()) # register the command @@ -719,24 +750,19 @@ def __init__( LIB, ( # Desc Opt Var type p1_val p2_val - ("HWND", False, AVV_YES, PT_INT, None, None), # variable to contain item to paste + ("HWND", True, AVV_YES, PT_INT, None, None), # variable to contain item to paste ), ( # num params, format string (trailing comma is important) - (0, " Wait until {1} is ready for input"), + (0, " Wait until current window is ready for input"), + (1, " Wait until {1} is ready for input"), ) ) def Process(self, btn, idx, split_line): - hwnd = self.Get_param(btn, 1) # get the window - tid, pid = win32process.GetWindowThreadProcessId(hwnd) # find the pid - hproc = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION , False, pid) # find the process id - - res = win32con.WAIT_TIMEOUT # set the failure mode to timeout - while res == win32con.WAIT_TIMEOUT: # while we're still timing out - res = win32event.WaitForInputIdle(hproc, 20) # wait a little while for window to become idle - if btn.Check_kill(): # check if we've been killed - return False # and die + hwnd = self.Get_param(btn, 1, win32gui.GetForegroundWindow()) # get the window + if not Wait_for_hwnd(btn, hwnd): + return False scripts.Add_command(Win32_Wait()) # register the command diff --git a/launchpad_fake.py b/launchpad_fake.py index b548908..c8f6995 100644 --- a/launchpad_fake.py +++ b/launchpad_fake.py @@ -35,6 +35,9 @@ def LedCtrlXY(self, x, y, z, t): def LedCtrlXYByCode(self, x, y, z): pass + def LedCtrlFlashXYByCode(self, x, y, z): + pass + def Close(self): pass diff --git a/scripts.py b/scripts.py index af0d79f..ae8cab9 100644 --- a/scripts.py +++ b/scripts.py @@ -437,8 +437,8 @@ def Check_kill(self, killfunc=None): # a sleep method that works with the multiple threads - def Safe_sleep(self, time, endfunc=None): - while time > DELAY_EXIT_CHECK: + def Safe_sleep(self, time=DELAY_EXIT_CHECK, endfunc=None): + while time >= DELAY_EXIT_CHECK: sleep(DELAY_EXIT_CHECK) time -= DELAY_EXIT_CHECK if self.Check_kill(endfunc):