diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index dcf8656..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 @@ -18,3 +17,8 @@ dependencies: - pytesseract - imagehash - dhash + - selenium + - pyperclip + - python-dateutil + - launchpad-py + - launchpad diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index 76817ef..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 @@ -17,3 +16,8 @@ dependencies: - pytesseract - imagehash - dhash + - selenium + - pyperclip + - python-dateutil + - launchpad-py + - launchpad diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt index a91d50d..9024173 100644 --- a/INSTALL/requirements.txt +++ b/INSTALL/requirements.txt @@ -18,4 +18,8 @@ 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 +selenium==3.141.0 +pyperclip==1.8.0 +python-dateutil==2.8.1 +launchpad-py +launchpad diff --git a/LPHK.py b/LPHK.py index 26b14bb..5bf9a9f 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,71 @@ 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") - - window.ARGS = vars(ap.parse_args()) # store the arguments in a place anything can get to - - if window.ARGS['debug']: + ap.add_argument( # option to start without connecting to a Launchpad + "-s", "--standalone", + 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_DEL, LM_RUN], default=LM_EDIT) + ap.add_argument( # turn of unnecessary verbosity + "-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 + + 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: + + if window.lp_connected or window.IsStandalone: 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/NewCommands.md b/NewCommands.md index 845051c..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. @@ -644,7 +652,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/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 a2268e2..9fc80f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.1 \ No newline at end of file +0.3.2 \ No newline at end of file diff --git a/command_base.py b/command_base.py index a995e77..2cd22ea 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,57 @@ 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 + + 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.doc = [] # no multi-line documentation - 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 - - + + 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 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 +83,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 +115,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 +128,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 +137,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,10 +157,10 @@ 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)) + ret = ("SYSTEM ERROR PARSING LINE " + str(idx) + ". '" + btn.Line(idx) + "' on pass " + str(pass_no), btn.Line(idx)) if ret[0]: print(ret[0]) @@ -158,17 +169,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 +188,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 +218,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 +234,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 @@ -243,16 +254,24 @@ 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. - + # 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 + + 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: @@ -280,7 +299,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 +309,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 +317,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 +334,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,46 +353,57 @@ 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. # This function improves efficiency. - vmp = self.valid_max_params - if (vmp == None) or (vmp < 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 == 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 + else: + return vnp[-1] # use the actual maximum + else: + v = vmp # vmp is preferred though + + 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): - # 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 +418,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,13 +454,13 @@ 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 # 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 @@ -442,27 +472,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 +500,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 +514,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 +523,83 @@ 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) - + + 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 + + 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 + return v elif pass_no == 2: ok = ret - + 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 + # 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): + 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): + 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] + m = min(n, avl) + av = self.auto_validate[m-1] + + if self.Param_count(btn) < n: + param = None + else: + param = btn.symbols[SYM_PARAMS][n] + 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]) + 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 val[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 - - 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): + 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: variables.Auto_store(btn.symbols[SYM_PARAMS][n], val, btn.symbols) # return result in variable - + # ################################################## # ### CLASS Command_Text_Basic ### @@ -563,20 +608,23 @@ 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, - (), + ( + # 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 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: @@ -588,16 +636,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, + 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 ): @@ -613,9 +662,29 @@ 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) +# ################################################## +# ### 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/command_list.py b/command_list.py index 94eeb50..69206fb 100644 --- a/command_list.py +++ b/command_list.py @@ -12,13 +12,25 @@ commands_keys, \ commands_mouse, \ commands_pause, \ - commands_external + commands_external, \ + commands_subroutines, \ + commands_dialog, \ + commands_browser_automation, \ + commands_file, \ + commands_documentation + +# @@@ a test command set for testing things! Will be removed for production +try: + import commands_test +except: + traceback.print_exc() + pass # 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") + print("[LPHK] INFO: RPN_EVAL command is not available") traceback.print_exc() # This library could be considered optional, and is also platform specific @@ -26,20 +38,16 @@ 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() -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") +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 if IMPORT_FATAL: # Not using this at present diff --git a/commands_browser_automation.py b/commands_browser_automation.py new file mode 100644 index 0000000..f252c56 --- /dev/null +++ b/commands_browser_automation.py @@ -0,0 +1,628 @@ +# 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" +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 ### +# ################################################## + +# 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 -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: + 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 = -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 + + +# ################################################## +# ### 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, Navigate controlled browser to a new URL", + 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}"), + ) ) + + 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) + url = self.Get_param(btn, 2) + try: + auto.get(url) + except: + traceback.print_exc() + + +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}"), + ) ) + + 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) + try: + auto.quit() + except: + traceback.print_exc() + + +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_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_LABEL, BAG_LINK_TEXT, BAG_CLASS_NAME, BAG_CSS_SELECTOR] + +# ################################################## +# ### 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:", + "", + "~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 " + "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[2] in BAGG_ALL: # invalid subcommand + c_ok = ', '.join(BAGG_ALL[:-1]) + ', or ' + BAGG_ALL[-1] + s_err = f"Invalid subcommand {split_line[2]} 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}"), + ) ) + + 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) + + try: + element.click() + + except: + traceback.print_exc() + + +scripts.Add_command(Bauto_Click_Element()) # register the command + + +# ################################################## +# ### CLASS BAUTO_SEND_TEXT ### +# ################################################## + +# class that defines the BA_SEND_TEXT command to send keys to an element +class Bauto_Send_Text(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}"), + ) ) + + 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 " + "escape characters:", + "", + "~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."] + + + 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) + + except: + traceback.print_exc() + + +scripts.Add_command(Bauto_Send_Text()) # 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_control.py b/commands_control.py index 8ca55b0..2c3a60a 100644 --- a/commands_control.py +++ b/commands_control.py @@ -12,10 +12,10 @@ # 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 + super().__init__("-, Comment", LIB, "-" ) @@ -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 + "LABEL, Target for jumps (goto, loops, etc)", 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,37 +143,174 @@ 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 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 + + 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 + + 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, 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 + + try: + if a == b: + return True + + except: + return False + + + 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 + + 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, 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 + + try: + if a > b: + return True + + except: + return False + + + 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 + + try: + if a < b: + return True + + except: + return False + + + 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 + + try: + if a >= b: + return True + + except: + return False + + + 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 + + try: + if a <= b: + return True + + except: + return False + # ################################################## # ### CLASS Control_Goto_Label ### @@ -182,25 +319,466 @@ 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 + "GOTO_LABEL, Unconditional jump to label", 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! + + 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 ### +# ################################################## + +# 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, 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 + ("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}"), + ), + ) + + 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) + + 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 ### +# ################################################## + +# 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_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) + (3, " if {2} == {3} Goto {1}"), + ), + "a == b", + 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 + + +# ################################################## +# ### 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_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} != {3} Goto {1}"), + ), + "a != b", + 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 + + +# ################################################## +# ### 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_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} > {3} Goto {1}"), + ), + "a > b", + 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 + + +# ################################################## +# ### 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_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) + (3, " if {2} >= {3} Goto {1}"), + ), + "a >= b", + 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 + + +# ################################################## +# ### 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_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} < {3} Goto {1}"), + ), + "a < b", + 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 + + +# ################################################## +# ### 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_ANY, None, None), + ("B", False, AVV_YES,PT_ANY, None, None), + ), + ( + # num params, format string (trailing comma is important) + (3, " if {2} <= {3} Goto {1}"), + ), + "a <= b", + 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 + + # ################################################## # ### CLASS Control_If_Pressed_Goto_Label ### # ################################################## @@ -208,19 +786,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 + "IF_PRESSED_GOTO_LABEL, Jump to label if initiating button still pressed", 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 +815,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 + "IF_UNPRESSED_GOTO_LABEL, Jump to label if initiating button is NOT pressed", 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 +844,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 + "REPEAT_LABEL, Jump to label a fixed number of times", 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 +876,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 + "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 - ("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 +916,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 + "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 - ("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 +948,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 + "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 - ("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 +988,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 + "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 - ("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 +1020,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 + "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 - ("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 +1048,7 @@ def __init__( self.Next_decrement, self.Test_func_ge_zero ) - + scripts.Add_command(Control_If_Unpressed_Repeat()) # register the command @@ -481,14 +1059,14 @@ 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 + super().__init__("RESET_REPEATS, Resets all repeats to their initial values") def Validate( @@ -523,6 +1101,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, Returns from a subroutine or exits a script", + 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 +1135,20 @@ 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, + 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.desc = "Ends an execution unconditionally (including if called from a subroutine)" + self.info_msg = "SCRIPT ENDED" + + def Process(self, btn, idx, split_line): + btn.root.thread.kill.set() return -1 @@ -558,12 +1164,13 @@ 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" + self.desc = "Aborts an execution unconditionally (including if called from a subroutine)" self.info_msg = "SCRIPT ABORTED" diff --git a/commands_dialog.py b/commands_dialog.py new file mode 100644 index 0000000..679f354 --- /dev/null +++ b/commands_dialog.py @@ -0,0 +1,128 @@ +import command_base, commands_header, scripts, dialog +from constants import * + +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__( + self, + ): + + super().__init__("DIALOG_OK_CANCEL, A simple OK/Cancel dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("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 + # num params, format string (trailing comma is important) + (2, " Dialog OK/Cancel '{1}' - abort on cancel"), + (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 " + "return value, a cancel will result in a button abort."] + + + def Process(self, btn, idx, split_line): + 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 + + 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, 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 + + +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__( + self, + ): + + super().__init__("DIALOG_INFO, A simple informational dialog", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("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) + (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 " + "will never cause an abort"] + + + 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 + +# ################################################## +# ### 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_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 + # 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_documentation.py b/commands_documentation.py new file mode 100644 index 0000000..1db6cca --- /dev/null +++ b/commands_documentation.py @@ -0,0 +1,118 @@ +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_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_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 +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:", + "", + "~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"] + + + 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 in [DD_HEADERS, DD_ALL]: # add requestsa as per the parameters + doc_set += [D_HEADERS] + if p in [DD_COMMANDS, DD_ALL]: + doc_set += [D_COMMANDS] + if p in [DD_SUBROUTINES, DD_ALL]: + doc_set += [D_SUBROUTINES] + if p in [DD_BUTTONS, DD_ALL]: + doc_set += [D_BUTTONS] + if p in [DD_COMMAND_BASE, DD_ALL]: + doc_set += [D_COMMAND_BASE] + if p in [DD_DEBUG, DD_ALL]: + doc_set += [D_DEBUG] + 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, 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 + + + 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_external.py b/commands_external.py index abe43a7..506e4b2 100644 --- a/commands_external.py +++ b/commands_external.py @@ -10,11 +10,11 @@ # 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__( - "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" ) @@ -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,15 +44,16 @@ 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__() - 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" @@ -70,11 +71,11 @@ 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__( - "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}'" ) @@ -93,13 +94,13 @@ 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 + super().__init__("SOUND, Play a sound file") def Validate( self, @@ -142,29 +143,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 + "SOUND_STOP, Stop all sound", 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,10 +182,10 @@ 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 + super().__init__("CODE, Run a command") def Validate( @@ -224,35 +225,35 @@ def Run( # ################################################## -# ### CLASS External_Code_NOWAIT ### +# ### 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 + ): + + super().__init__("CODE_NOWAIT, Run a command but don't wait for it to finish", 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 + ("Command", False, AVV_YES, 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)) @@ -263,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_file.py b/commands_file.py new file mode 100644 index 0000000..32570ed --- /dev/null +++ b/commands_file.py @@ -0,0 +1,268 @@ +# This module contains commands that work on the filesystem +import os, command_base, ms, kb, scripts, traceback, pathlib, files +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}"), + ) ) + + 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 + + +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_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) + (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, 2) + try: + 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 + + +# ################################################## +# ### 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__( + 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), + ("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."] + + + 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): + 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) + + 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 + + + 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/commands_header.py b/commands_header.py index eafb295..8273598 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 # ################################################## @@ -7,11 +7,11 @@ 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 - True) # You also define if the header causes the script to be asynchronous + True) # This must be specified for async headers def Validate( self, @@ -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)) @@ -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, + 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,14 +107,16 @@ 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, + 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 + 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, @@ -178,3 +169,321 @@ 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") + + 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", + " s of", + " text", + "", + "Shorter text strings are are displayed using larger fonts for greater " + "readability.", + "", + "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 subroutine!) + 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") + + 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( + 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") + + 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( + 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 + + +# ################################################## +# ### 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 without adding a line break") + + 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 + +# ################################################## +# ### 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 + + +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/commands_keys.py b/commands_keys.py index ad1e7f2..d2db39c 100644 --- a/commands_keys.py +++ b/commands_keys.py @@ -10,11 +10,11 @@ # 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__( - "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, (), () ) @@ -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 + "TAP, Tap (and release) a key", 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, None, None), #@@@ this also doesn't work 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,35 +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 = 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: 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 @@ -108,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 + "PRESS, Press (and hold) a key", 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}"), ) ) @@ -139,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 + "RELEASE, Release a PRESSed key", 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}"), ) ) @@ -170,16 +168,16 @@ 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__( - "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, (), ( # num params, format string (trailing comma is important) - (0, " Release all keys"), + (0, " Release all keys"), ) ) @@ -198,8 +196,8 @@ 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 + + super().__init__("STRING, Type a series of characters", LIB, "Type out string" ) @@ -219,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_mouse.py b/commands_mouse.py index 6d82eb3..b1e99ef 100644 --- a/commands_mouse.py +++ b/commands_mouse.py @@ -13,25 +13,25 @@ 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) - ("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 @@ -46,23 +46,23 @@ 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 + # 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) @@ -79,24 +79,24 @@ 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 + # 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) @@ -113,22 +113,22 @@ 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 from one position to another", 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"), ) ) @@ -169,20 +169,20 @@ 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, Move the mouse along a line from the current position", 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) @@ -208,7 +208,6 @@ def Process(self, btn, idx, split_line): return -1 - scripts.Add_command(Mouse_Line_Move()) # register the command @@ -222,10 +221,10 @@ 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, set the mouse position", 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 +232,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"), ) ) @@ -271,27 +270,27 @@ 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 + # 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"), ) ) 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] @@ -328,23 +327,23 @@ 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, optionally into named variables", 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]) @@ -365,7 +364,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 ), @@ -379,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_pause.py b/commands_pause.py index a387fb3..4e187ce 100644 --- a/commands_pause.py +++ b/commands_pause.py @@ -9,10 +9,10 @@ # 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 + super().__init__("DELAY, Pause the script") def Validate( self, diff --git a/commands_rpncalc.py b/commands_rpncalc.py index 25f08f7..e25faff 100644 --- a/commands_rpncalc.py +++ b/commands_rpncalc.py @@ -1,10 +1,11 @@ -import command_base, lp_events, scripts, variables, sys +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) # 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,36 @@ # 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 + 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 @@ -38,21 +57,36 @@ 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): # 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 +105,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 +142,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 +161,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 @@ -147,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) # ** @@ -165,6 +200,7 @@ def Register_operators(self): self.operators["Y?"] = (self.x_gt_y, 0) # is x > y? @@ -177,32 +213,40 @@ 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) + 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, + 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 + """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 - + 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): + """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) @@ -212,13 +256,15 @@ 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 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) @@ -228,13 +274,15 @@ 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 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) @@ -244,13 +292,15 @@ 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): + """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) @@ -260,13 +310,15 @@ 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): + """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) @@ -276,46 +328,56 @@ 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): + """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 - + return ret 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 - + return ret 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 - + return ret 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 print('Global = ', symbols[SYM_GLOBAL][1]) - + return ret 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 @@ -324,11 +386,13 @@ 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): + """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) @@ -338,11 +402,13 @@ 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): + """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) @@ -352,11 +418,13 @@ 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): + """Changes the sign of the value on the top of the stack. LASTX is not modified.""" + ret = 1 a = variables.pop(symbols) @@ -364,11 +432,28 @@ 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 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.""" + # calculates the square ret = 1 a = variables.pop(symbols) @@ -378,13 +463,15 @@ def sqr(self, symbols, cmd, cmds): c = a**2 except: raise Exception("Error in squaring: " + str(a)) - + variables.push(symbols, c) - + return ret 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) @@ -397,64 +484,78 @@ 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 def dup(self, symbols, cmd, cmds): + """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 variables.push(symbols, variables.top(symbols, 1)) - + return ret 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) - + return ret def clst(self, symbols, cmd, cmds): + """Clears the entire stack. LASTX is not modified""" + # clears the stack ret = 1 symbols[SYM_STACK].clear() - + return ret 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: 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 def cl_l(self, symbols, cmd, cmds): + """Clears all local variables, including LASTX""" + # clears the stack ret = 1 symbols[SYM_LOCAL].clear() - + return ret 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. LASTX is not modified.""" + # returns stack length ret = 1 variables.push(symbols, len(symbols[SYM_STACK])) - + return ret 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 @@ -468,69 +569,83 @@ 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 stack 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? + 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 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 - 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 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 - 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 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) - 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], param_convs._any) # as an integer variables.push(symbols, a) - + return ret 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) - a = variables.get(v, symbols[SYM_LOCAL], None) + ret, v = variables.next_cmd(ret, cmds) + a = variables.get(v, symbols[SYM_LOCAL], None, param_convs._any) # as an integer variables.push(symbols, a) - + return ret - + 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) - 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], param_convs._any)# 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): + """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 @@ -539,6 +654,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 @@ -546,7 +663,19 @@ 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.""" + # only continues eval if the two top values are equal if variables.top(symbols, 1) == variables.top(symbols, 2): return 1 @@ -555,6 +684,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 @@ -563,6 +694,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 @@ -571,6 +704,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 @@ -579,6 +714,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 @@ -587,6 +724,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 @@ -595,6 +734,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) @@ -606,6 +747,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) @@ -617,6 +760,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) @@ -627,6 +772,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) @@ -637,6 +784,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) @@ -648,6 +797,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) @@ -659,10 +810,81 @@ 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. The previous value of X is placed in LASTX""" + + # does a substring + x = variables.pop(symbols) + y = variables.pop(symbols) + z = variables.pop(symbols) + + 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. 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) + dt = parser.parse(d) + + 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. 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) + dt = datetime.date.fromordinal(j) + + 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 + + scripts.Add_command(Rpn_Eval()) # register the command @@ -676,16 +898,16 @@ 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 + # 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 +915,95 @@ 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 + + +# 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 ### +# ################################################## + +# class that defines the RPN_CLEAR command -- clears variables +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_WORD, None, None), + ("Variable", True, AVV_NO, PT_WORDS, None, None), + ), + ( + # num params, format string (trailing comma is important) + (1, " Clear {1}"), + (2, " Clear {1}: {2}"), + ) ) + + 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", + 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 RCG_GLOBAL: + with btn.symbols[SYM_GLOBAL][0]: + btn.symbols[SYM_GLOBAL][1].clear() # clear all global variables + if f in RCG_LOCAL: + btn.symbols[SYM_LOCAL].clear() # clear all local variables + if f in RCG_STACK: + btn.symbols[SYM_STACK].clear() # clear the stack + if f in RCG_G_VAR: + for i in range(2, self.Param_count(btn)+1): + with btn.symbols[SYM_GLOBAL][0]: + 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(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_Set()) # register the command \ No newline at end of file +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 ed25f61..03eda54 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, colorsys 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' +T_PATH = shutil.which('tesseract') +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): - 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 @@ -23,111 +25,361 @@ 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 + # scrapes an image from the clipboard + 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) + + return -1 # 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 + # 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_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}"), ) ) + + 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 - + 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, 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 + # 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), + ("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}"), ) ) + 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 - + 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 + 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}"), + ) ) + + 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 + + self.Set_param(btn, 1, image) # pass the text back + + +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__( + 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}"), + ) ) + + 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 + + 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, returns a hash value that (almost) uniquely identifies an image", + 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}"), + ) ) + + 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 + 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 - + self.Set_param(btn, 2, hash) # pass the hash back + + scripts.Add_command(Scrape_Image_Hash()) # register the command +# ################################################## +# ### 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_Image_Colour(Command_Scrape): + def __init__( + self, + ): + + super().__init__("S_COLOUR, determines the average R, G, and B values of an 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), + ("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 " + "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 + method = self.Get_param(btn, 5, SIC_MEAN) + + pixels = image.load() # create the pixel map + + 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) + + + 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 + + # ################################################## # ### CLASS S_IMAGE_FINGERPRINT ### # ################################################## @@ -138,44 +390,37 @@ 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), + # Desc Opt Var type p1_val p2_val + ("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}"), ) ) + 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): - 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 - - 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 @@ -189,27 +434,222 @@ 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 + # 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}"), ) ) + 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 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 + + +# ################################################## +# ### 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 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): + 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 + + +# ################################################## +# ### 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 diff --git a/commands_subroutines.py b/commands_subroutines.py new file mode 100644 index 0000000..4025a49 --- /dev/null +++ b/commands_subroutines.py @@ -0,0 +1,374 @@ +import command_base, commands_header, scripts, variables, param_convs +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_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(command_base.Command_Header): + def __init__( + self, + ): + + 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!) + 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) + ): + + 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:", + "", + "~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", + " `!` 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:", + "", + " @SUB DO_MORE a+I b+FO c+R$", + "", + "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 " + "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 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 + ", A user-defined subroutine", + 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): + 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 = 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! + #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: + 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 + + +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): + # 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] != 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 + + # 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] + + if not variables.valid_var_name(var): # if there's no '+', look for trailing modifiers without one + l = len(var) + while l > 1: + if not variables.valid_var_name(var): + mods += var[-1] + var = var[:-1] + else: + 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 + 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 + + 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 + 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, err + + 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 isinstance(script_validation, bool) and script_validation: # if validation is OK + err = True + else: + 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, err diff --git a/commands_test.py b/commands_test.py new file mode 100644 index 0000000..f227513 --- /dev/null +++ b/commands_test.py @@ -0,0 +1,431 @@ +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 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("============================START") + print("=", self.name, split_line) + 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}', {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 + self.Set_param(btn, 1, a) + 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): + def __init__( + self, + ): + + super().__init__("TEST_01, Test for single optional integer constant parameter", + 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}"), + ) ) + + 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 + + +class Test_02(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_02, Test for single optional float constant parameter", + 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}"), + ) ) + + 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 + + +class Test_03(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_03, Test for single optional string constant parameter", + 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}"), + ) ) + + 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 + + +class Test_04(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_04, Test for single optional multi-string constant parameter", + 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}"), + ) ) + + 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 + + +class Test_11(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_11, Test for single optional integer constant/variable parameter", + 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}"), + ) ) + + 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 + + +class Test_12(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_12, Test for single optional float constant/variable parameter", + 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}"), + ) ) + + 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 + + +class Test_13(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_13, Test for single optional string constant/variable parameter", + 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}"), + ) ) + + 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 + + +class Test_14(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_14, Test for single optional multi-string constant/variable parameter", + 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}"), + ) ) + + 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 + + +class Test_21(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_21, Test for single optional integer by-ref parameter", + 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}"), + ) ) + + 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 + + +class Test_22(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_22, Test for single optional float by-ref parameter", + 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}"), + ) ) + + 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 + + +class Test_23(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_23, Test for single optional string by-ref parameter", + 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}"), + ) ) + + 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 + + +class Test_24(Test_XX): + def __init__( + self, + ): + + super().__init__("TEST_24, Test for single optional multi-string by-ref parameter", + 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}"), + ) ) + + 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 + + +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" + 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 + + +# 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, Test to display a simple dialog", + 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"), + ) ) + + 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 + ret = dialog.CommentBox(btn, "this is a test") + + +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"), + ) ) + + 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() + + +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"), + ) ) + + 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]) + + +scripts.Add_command(Test_Dump_Debug()) # register the command diff --git a/commands_win32.py b/commands_win32.py index 87f95b5..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 +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) @@ -15,41 +17,46 @@ 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 + 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: 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 + # 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 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) + hwnds.sort() return hwnds @@ -63,16 +70,16 @@ 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 + # 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 +87,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 @@ -112,21 +119,21 @@ 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 + # 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 @@ -143,31 +150,31 @@ 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 + # 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 @@ -181,30 +188,30 @@ 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 + # 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: @@ -224,32 +231,32 @@ 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 + # 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,53 +272,333 @@ 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 + ): + + super().__init__("W_FIND_HWND, Returns the handle of the nth exactly matching window", LIB, ( - # Desc Opt Var type p1_val p2_val - ("Title", False, AVV_NO, PT_STR, None, None), # name to search for + # 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 ### # ################################################## @@ -320,43 +607,49 @@ def CheckWindow(hwnd, data): 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 + ): + + super().__init__("W_COPY, Copy data from the current window", 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 cut item + ("Success", True, AVV_REQD, PT_INT, None, None), # variable to contain success value ), ( # num params, format string (trailing comma is important) - (0, " Copy into system clipboard"), - (1, " Copy into system clipboard and {1}"), + (0, " Copy into system clipboard"), + (1, " Copy into system clipboard and {1}"), + (2, " Copy into system clipboard and {1}, returning success in {2}"), ) ) - + 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() - + + ClearClipboard() + 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')) + kb.press(kb.sp('ctrl')) + kb.tap(kb.sp('c')) finally: - kb.release(kb.sp('ctrl')) - - if self.Param_count(btn) > 0: # save to variable if required + kb.release(kb.sp('ctrl')) + + n = -10 + while n < -1: try: - win32clipboard.OpenClipboard(hwnd) - t = win32clipboard.GetClipboardData(win32con.CF_TEXT) - self.Set_param(btn, 1, t) - finally: - win32clipboard.CloseClipboard() + n = win32clipboard.CountClipboardFormats() + except: + n += 1 + btn.Safe_sleep() + + if n <= 0: + 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 + self.Set_param(btn, 1, t) + self.Set_param(btn, 2, 0) scripts.Add_command(Win32_Copy()) # register the command @@ -366,48 +659,83 @@ 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, - ): - - 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 + # Desc Opt Var type p1_val p2_val + ("Clipboard", True, AVV_YES, 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 - + #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 + + ClearClipboard() + SetClipboard(str(c)) + WaitClipboard(btn) + + #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')) + kb.tap(kb.sp('v')) finally: - kb.release(kb.sp('ctrl')) + kb.release(kb.sp('ctrl')) + btn.Safe_sleep() + + 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)) + WaitClipboard(btn) + + +scripts.Add_command(Win32_Copy_Var()) # register the command + + # ################################################## # ### CLASS W_WAIT ### # ################################################## @@ -416,30 +744,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 + ): + + 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 - ("HWND", False, AVV_YES, PT_INT, None, None), # variable to contain item to paste + # Desc Opt Var type p1_val p2_val + ("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 @@ -453,22 +776,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 + ): + + 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 + # 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: @@ -478,3 +801,78 @@ def Process(self, btn, idx, split_line): scripts.Add_command(Win32_Pid_To_Hwnd()) # register the command + + +# ################################################## +# ### CLASS W_WINDOW_SIZE ### +# ################################################## + +# class that defines the W_WINDOW_SIZE command - returns the size of a window +class Win32_Window_Size(Command_Win32): + def __init__( + self, + ): + + super().__init__("W_WINDOW_SIZE, Return the size of a window", + LIB, + ( + # Desc Opt Var type p1_val p2_val + ("x", False, AVV_REQD, PT_INT, None, None), # variable containing pid + ("y", False, AVV_REQD, PT_INT, None, None), # variable containing pid + ("hwnd", True, AVV_REQD, PT_INT, None, None), # variable to contain hwnd + ), + ( + # num params, format string (trailing comma is important) + (2, " return size of current window in ({1}, {2})"), + (3, " return size of window {3} in ({1}, {2})"), + ) ) + + 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 = 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 99561fb..40d5d8e 100644 --- a/constants.py +++ b/constants.py @@ -1,5 +1,7 @@ # Constants used all over the place. An excuse to use "from constants import *" +import param_convs + # Get platform information PLATFORMS = [ {"search_string": "win", "name_string": "windows"}, {"search_string": "linux", "name_string": "linux"}, @@ -57,22 +59,27 @@ 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 # 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 (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 @@ -87,7 +94,46 @@ 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 DELAY_EXIT_CHECK = 0.025 + +# subroutine related +SUBROUTINE_PREFIX = "CALL:" +SUBROUTINE_HEADER = "@SUB" + +# Launchpad constants +LP_MK1 = "Mk1" +LP_MK2 = "Mk2" +LP_PRO = "Pro" +LP_MINI = "Mini" + +LM_EDIT = "edit" +LM_MOVE = "move" +LM_SWAP = "swap" +LM_COPY = "copy" +LM_DEL = "del" +LM_RUN = "run" + +# Dump constants +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 +D_NO_SRC_DOC = 8 # hide irrelevant source documentation + +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 +DLG_ERROR = 3 # a simple titled box with Cancel diff --git a/dialog.py b/dialog.py new file mode 100644 index 0000000..5352189 --- /dev/null +++ b/dialog.py @@ -0,0 +1,271 @@ +# a routine that allows a single script at a time to access dialogs +import threading, tkinter as tk +from constants import * + +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_PARAM = 3 # dialog parameters (title, message) + + +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") + msg.pack(padx=8, pady=8) + + 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) + + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + + + 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(): + 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: + 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 + + 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 + + + # 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, 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()) # 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 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 +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(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(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(parent) # act to close the dialog + + else: # there is no dialog open + + 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 + + +# Close any open dialog +def CloseDialog(parent): + 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 + + 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 + DIALOG_REQUEST = None # no request + + +# Open a new dialog +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[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 + 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) + 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/files.py b/files.py index fb83e4e..d932c29 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: @@ -43,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() @@ -57,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(",") @@ -65,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) @@ -75,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: @@ -97,55 +104,77 @@ 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() - layout["version"] = FILE_VERSION - + + 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) + + 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): +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 - + converted_to_rg = False - + scripts.Unbind_all() - window.app.draw_canvas() - + scripts.Unload_all(unload_subroutines=load_subroutines) # remove all existing subroutines when you load a new layout + window.Redraw(True) + 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 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): button = layout["buttons"][x][y] 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 - + if script_text != "": 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: @@ -153,19 +182,20 @@ 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: - lp_colors.update_all() - window.app.draw_canvas() - in_error = True - window.app.save_script(window.app, x, y, script_text, open_editor = True, color = color) - in_error = False + if check_subroutines and (script_validation != True): + btn.invalid_on_load = True + color = [0, 0, 0] else: - scripts.Bind(x, y, script_text, color) + 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) + lp_colors.update_all() - window.app.draw_canvas() - + window.Redraw(True) + curr_layout = name if converted_to_rg: if popups: @@ -176,6 +206,89 @@ 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') + + loaded = [] + not_loaded = [] + s_tot = 0 + s_ok = 0 + s_fail = 0 + for i, sub in enumerate(subs): + 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 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 + + 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: + err = False + + return err, name + def import_script(name): with open(name, "r") as f: text = f.read() diff --git a/global_vars.py b/global_vars.py new file mode 100644 index 0000000..53c262e --- /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..c8f6995 --- /dev/null +++ b/launchpad_fake.py @@ -0,0 +1,69 @@ +# A fake connector to allow operation without a launchpad connected + +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): + pass + + def ButtonFlush(self): + pass + + def LedCtrlBpm(self, x): + pass + + def ButtonStateXY(self): + return Pop() + + def LedCtrlXYByRGB(self, x, y, z): + pass + + def LedCtrlXY(self, x, y, z, t): + pass + + def LedCtrlXYByCode(self, x, y, z): + pass + + def LedCtrlFlashXYByCode(self, x, y, z): + pass + + def Close(self): + 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 global_vars.ARGS['standalone'] + + def get_display_name(self, lp): + return "Emulated Launchpad " + global_vars.ARGS['standalone'] + + 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..ee88df9 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 :-( @@ -10,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", @@ -43,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: @@ -84,7 +89,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): @@ -100,8 +105,11 @@ 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 + 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? if is_func_key: @@ -113,7 +121,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) @@ -135,7 +143,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(): @@ -149,7 +158,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/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..49297eb --- /dev/null +++ b/param_convs.py @@ -0,0 +1,63 @@ +# 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 + 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 + 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 str(x) + + +# no conversion +def _None(x): + 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/run.bat b/run.bat new file mode 100644 index 0000000..dba08cc --- /dev/null +++ b/run.bat @@ -0,0 +1,2 @@ +cls +python LPHK.py -l user_layouts\testing.lpl -s Mk1 -M run -q diff --git a/scripts.py b/scripts.py index 9bad0b2..ae8cab9 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. @@ -11,55 +12,235 @@ 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 got the globals to prevent simultaneous access - - +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 + return p # return any replaced 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 - - -# 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) - - if command_name in VALID_COMMANDS: # if it already exists - HEADERS.pop(command_name) # remove it - - + 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 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 + 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): + l = f" {c_type} \"{get_name(c)}\"" + desc = get_desc(c) + if desc == "": + print(l) + else: + 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 = [] + 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") + indent = 12 + for n in doc: + indent, skip = checkindent(n, indent, 12) + if not skip: + print(wrap_line(" "*12 + n, indent)) + + 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_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 [['@NAME'], ['@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(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, D_NO_SRC_DOC in style) + + 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 @@ -69,12 +250,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,61 +268,112 @@ 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 + 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.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? + 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._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): + + 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 + self.root = self # then we are the root + 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): + 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, 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 - 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 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 - errors += 1 - else: - res = command.Parse(self, idx, split_line, pass_no); - if res != True: + 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 = 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: @@ -150,34 +382,63 @@ 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 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: + 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 + 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 + new_btn.validated = self.validated # Need to copy over the validation flag (it should be True at this point) + + + # 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() + 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 - # 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) - def Safe_sleep(self, time, endfunc=None): - while time > DELAY_EXIT_CHECK: + + # a sleep method that works with the multiple threads + 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): @@ -204,8 +465,9 @@ 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 @@ -215,6 +477,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))] @@ -224,10 +489,10 @@ 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: + 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() @@ -235,7 +500,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) @@ -255,8 +520,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]) @@ -287,10 +552,10 @@ 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. 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 @@ -309,9 +574,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 @@ -327,41 +592,46 @@ 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? + 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 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 + 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... + 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: + 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(" ") @@ -369,29 +639,30 @@ 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: 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 + 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 + 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 @@ -402,13 +673,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...") @@ -421,32 +692,100 @@ 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 - + 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 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 # we won't run it afterwards. - def Validate_script(self): + 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! - if self.Parse_script(): # If parsing is OK - self.validated = True # Script is valid + validation = self.Parse_script(full_parse=full_validate) # parse the script + if validation == True: # If parsing is OK + 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]) - 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 + self.symbols = None # otherwise destroy symbol table + return validation - 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 @@ -454,16 +793,27 @@ 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... - 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? @@ -472,6 +822,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 @@ -480,44 +831,46 @@ 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 + files.layout_changed_since_load = True # Mark the layout as changed 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 - + files.layout_changed_since_load = True # Mark the layout as changed + Redraw(x, y) -# 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_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_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 script_1 != "": - Bind(x2, y2, script_1, color_1) + 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 @@ -526,32 +879,47 @@ 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 - + + 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 - - 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_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 @@ -565,15 +933,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 +947,38 @@ 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 + + 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 + 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(unload_subroutines=True): + 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 + + 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 + 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/user_layouts/Testing.lpl b/user_layouts/Testing.lpl new file mode 100644 index 0000000..e470e80 --- /dev/null +++ b/user_layouts/Testing.lpl @@ -0,0 +1,698 @@ +{ + "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\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": [ + 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/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 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/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() diff --git a/variables.py b/variables.py index a7a8e28..fcace08 100644 --- a/variables.py +++ b/variables.py @@ -1,9 +1,13 @@ from constants import * +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! +# 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 @@ -15,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): @@ -34,20 +38,29 @@ 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): +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. # 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") @@ -57,7 +70,7 @@ def put(name, val, vbls): # 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 @@ -67,15 +80,15 @@ 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 def error_msg(idx, name, desc, p, param, err): ret = "Line:" + str(idx+1) + " -" - + if name: ret += " '" + name + "'" if desc: @@ -91,32 +104,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)) @@ -127,8 +140,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)) @@ -141,7 +154,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: @@ -153,7 +166,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 @@ -165,7 +178,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: @@ -176,7 +189,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 @@ -185,49 +198,91 @@ 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, a, symbols): - # automatically stores the variable in the "right" place +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..546e9a7 100644 --- a/window.py +++ b/window.py @@ -5,13 +5,17 @@ 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 V_WIDTH = 50 STAT_ACTIVE_COLOR = "#080" +STAT_RUN_COLOR = "#D00" STAT_INACTIVE_COLOR = "#444" SELECT_COLOR = "#f00" DEFAULT_COLOR = [0, 0, 255] @@ -29,30 +33,50 @@ 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])] 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" +MENU_LAUNCHPAD = "Launchpad" + +LPCON = None + + +# Are we in standalone mode? +def IsStandalone(): + return not (global_vars.ARGS['standalone'] == None) + + +def lpcon(): + global LPCON + + if LPCON == None: + if IsStandalone(): + LPCON = launchpad_fake_connector + else: + LPCON = launchpad_connector + + return LPCON + -def init(lp_object_in, launchpad_in, path_in, prog_path_in, user_path_in, version_in, platform_in): +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 @@ -60,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: @@ -80,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")) @@ -89,13 +112,16 @@ 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 + self._redraw = False + self.over_x = -1 # The button the user is over + self.over_y = -1 def init_window(self): global root - + self.master.title("LPHK - Novation Launchpad Macro Scripting System") self.pack(fill="both", expand=1) @@ -104,17 +130,27 @@ 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) 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(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.disable_menu("Layout") - 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) @@ -131,102 +167,161 @@ 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.bind("", self.click) - self.c.grid(row=0, column=0, padx=round(c_gap/2), pady=round(c_gap/2)) + 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=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): + 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) - + 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() - self.enable_menu("Layout") + Redraw() + self.enable_menu(MENU_LAYOUT) + self.enable_menu(MENU_SUBROUTINES) def connect_lp(self): global lp_connected global lp_mode global lp_object - lp = lpcon.get_launchpad() + 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.""", +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, - 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 lpcon().connect(lp): lp_connected = True lp_object = 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 am in Live mode.") + lp_mode = lpcon().get_mode(lp) 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) - self.draw_canvas() - self.enable_menu("Layout") - self.stat["text"] = f"Connected to {lpcon.get_display_name(lp)}" - self.stat["bg"] = STAT_ACTIVE_COLOR + Redraw() + 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 + + if IsStandalone(): + 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 clicking on them in 'Run' mode.""", + [["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): - 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! + + # process idle requests (dialogs and button redraws) + def idle(self): + from dialog import IdleProcess + try: + IdleProcess(self) + 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) 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() - 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 @@ -241,7 +336,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() @@ -252,6 +347,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, @@ -269,56 +377,97 @@ def save_layout(self): else: 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) - - + 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_DEL + elif self.button_mode == LM_DEL: + self.button_mode = LM_RUN else: - self.button_mode = "edit" - self.draw_canvas() + self.button_mode = LM_EDIT 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) 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) - - if self.button_mode == "move": + 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)): 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_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 - self.draw_canvas() - + + Redraw(column, row) + def draw_button(self, column, row, color="#000000", shape="square"): gap = int(BUTTON_SIZE // 4) @@ -333,16 +482,108 @@ 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): + 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 + + 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, round=False): + 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) + + 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)) + + def fmt_str(t, lines=2, cols=55): + 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]) > 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]) < cols: + tl[n] = tl[n] + ' ' + tl[n+1] + del tl[n+1] + else: + n += 1 + + tl = tl[0:lines] # 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: + 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) + + 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: - 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) 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: @@ -352,39 +593,95 @@ 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 - self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(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)) + if self.grid_drawn: + 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][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): + if bx == None or (by == y): + 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): - 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()) + 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(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) + 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") + 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 + 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()) + 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 - 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"), + 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.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"), + 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.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), 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.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")), \ + None, None + ] + 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")), \ + None, None + ] + self.grid_drawn = True def clear_canvas(self): @@ -394,7 +691,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) @@ -404,14 +701,15 @@ 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) + 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 @@ -420,7 +718,13 @@ 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 + #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) e_m = tk.Menu(w) @@ -430,7 +734,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: @@ -443,21 +747,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": + if lp_mode == LP_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") @@ -492,61 +796,61 @@ 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": + + 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) + ")") @@ -588,19 +892,19 @@ 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): 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() @@ -610,8 +914,10 @@ 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]) - self.draw_canvas() + Redraw(x, y) lp_colors.updateXY(x, y) window.destroy() else: @@ -667,7 +973,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) @@ -679,7 +985,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: @@ -695,7 +1001,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 @@ -704,20 +1010,28 @@ 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]]) +# 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 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 @@ -733,12 +1047,13 @@ 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() def close(): - global root_destroyed, launchpad + global root_destroyed app.modified_layout_save_prompt() app.disconnect_lp()