diff --git a/.gitignore b/.gitignore index 1dd54e9..f6148da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# intellij/PyCharm +.idea/ + # Distribution / packaging .Python build/ diff --git a/INSTALL/environment-build.yml b/INSTALL/environment-build.yml index 4b78822..e7769a0 100644 --- a/INSTALL/environment-build.yml +++ b/INSTALL/environment-build.yml @@ -6,10 +6,11 @@ dependencies: - pip - tk - pip: - - keyboard - - launchpad-py + - git+git://github.com/FMMT666/launchpad.py.git@master - pillow - pygame - pynput - tkcolorpicker - - pyinstaller + - https://github.com/pyinstaller/pyinstaller/archive/develop.zip + - py-getch + - pyautogui diff --git a/INSTALL/environment.yml b/INSTALL/environment.yml index d16cec8..da930e0 100644 --- a/INSTALL/environment.yml +++ b/INSTALL/environment.yml @@ -6,9 +6,10 @@ dependencies: - pip - tk - pip: - - keyboard - - launchpad-py + - git+git://github.com/FMMT666/launchpad.py.git@master - pillow - pygame - pynput - tkcolorpicker + - py-getch + - pyautogui diff --git a/INSTALL/linux_beta/install_linux.bash b/INSTALL/linux_beta/install_linux.bash old mode 100644 new mode 100755 index 186ae2e..ad1528f --- a/INSTALL/linux_beta/install_linux.bash +++ b/INSTALL/linux_beta/install_linux.bash @@ -86,7 +86,7 @@ function uninstall_LPHK () { # If conda isn't found, prompt to install it as well -conda > /dev/null 2>1 && CONDAGOOD=1 || CONDAGOOD=0 +conda > /dev/null 2>&1 && CONDAGOOD=1 || CONDAGOOD=0 if [ $CONDAGOOD = 0 ]; then echo "No conda found. Install Miniconda3 and LPHK?" prompt_yn @@ -103,7 +103,7 @@ if [ $CONDAGOOD = 0 ]; then fi # If LPHK is already installed, offer to uninstall -LPHKENVDIR=$(cat ~/.conda/environments.txt | grep LPHK) > /dev/null 2>1 +LPHKENVDIR=$(cat ~/.conda/environments.txt | grep LPHK) > /dev/null 2>&1 if [ ! -z $LPHKENVDIR ]; then echo "LPHK already installed! Uninstall LPHK?" prompt_yn @@ -147,7 +147,7 @@ else echo "Installing LPHK..." install_LPHK # Get the new location after install - LPHKENVDIR=$(cat ~/.conda/environments.txt | grep LPHK) > /dev/null 2>1 + LPHKENVDIR=$(cat ~/.conda/environments.txt | grep LPHK) > /dev/null 2>&1 echo "LPHK environment set up. Run '[your lphk directory]/run.bash'" else echo "Not installing LPHK, exiting..." diff --git a/INSTALL/requirements.txt b/INSTALL/requirements.txt new file mode 100644 index 0000000..58ad978 --- /dev/null +++ b/INSTALL/requirements.txt @@ -0,0 +1,17 @@ +MouseInfo==0.1.3 +Pillow==7.1.1 +py-getch==1.0.1 +PyAutoGUI==0.9.50 +pygame==1.9.6 +PyGetWindow==0.0.8 +PyMsgBox==1.0.7 +pynput==1.6.8 +pyperclip==1.8.0 +PyRect==0.1.4 +PyScreeze==0.1.26 +python-xlib==0.27 +python3-xlib==0.15 +PyTweening==1.0.3 +six==1.14.0 +tkcolorpicker==2.1.3 +-e git+git://github.com/FMMT666/launchpad.py.git@3e3fc3b2589d9ced80bc6c9b218dd90c8a2dd485#egg=launchpad-py diff --git a/LPHK.py b/LPHK.py index 6467366..efd96b9 100755 --- a/LPHK.py +++ b/LPHK.py @@ -55,10 +55,12 @@ def get_first_textfile_line(file_path): import logger logger.start(LOG_PATH) + # Start printing output def datetime_str(): - now = datetime.now() - return now.strftime("%d/%m/%Y %H:%M:%S") + now = datetime.now() + return now.strftime("%d/%m/%Y %H:%M:%S") + print("---------------- BEGIN LOG", datetime_str(), "----------------") print("LPHK - LaunchPad HotKey - A Novation Launchpad Macro Scripting System") @@ -83,10 +85,13 @@ def datetime_str(): print("") import lp_events, scripts, kb, files, sound, window +from utils import launchpad_connector lp = launchpad.Launchpad() EXIT_ON_WINDOW_CLOSE = True + + def init(): global EXIT_ON_WINDOW_CLOSE if len(sys.argv) > 1: @@ -112,7 +117,7 @@ def shutdown(): if window.lp_connected: scripts.unbind_all() lp_events.timer.cancel() - lp.Close() + launchpad_connector.disconnect(lp) window.lp_connected = False logger.stop() if window.restart: @@ -122,10 +127,12 @@ def shutdown(): 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) if EXIT_ON_WINDOW_CLOSE: shutdown() + main() diff --git a/README.md b/README.md index 395064e..6cb62fc 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ I have specifically chosen to do my best to develop this using as many cross pla ## Installation [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) *Note: Files used in the installation are named with a version number, and will change with each new release. The word `VERSION` is used in the below filenames and paths to denote where this version number will be. When going to [https://github.com/nimaid/LPHK/releases/latest](https://github.com/nimaid/LPHK/releases/latest), it will redirect to the page with the latest versions of these files, so the correct value of `VERSION` should be plainly obvious.* +*Note: Because pyautogui is used you may need to install some extra libraries to your machine, more info on this page: [https://pyautogui.readthedocs.io/en/latest/install.html](https://pyautogui.readthedocs.io/en/latest/install.html)* ### Windows Install/Run Instructions [[Table of Contents]](https://github.com/nimaid/LPHK#table-of-contents) *Is these pre-built binaries do not work for you, please share the issue in the Discord or as a GitHub issue. In the meantime, advanced users can use `INSTALL\environment.yml` to install the LPHK conda environment, and then run `python LPHK.py` after activating it.* @@ -503,4 +504,4 @@ In order of priority: * ~~Make a special color picker for Classic/Mini/S that only has the 16 possible colors~~ * ~~Re-write `files.py` to use a JSON format for layouts.~~ * ~~Make an installer for Windows~~ -* ~~Simply strip comments and empty lines before the first real command. That way, first line can be a comment and second a header, etc.~~ \ No newline at end of file +* ~~Simply strip comments and empty lines before the first real command. That way, first line can be a comment and second a header, etc.~~ diff --git a/RUN.bash b/RUN.bash index 129c55c..7cc83c2 100644 --- a/RUN.bash +++ b/RUN.bash @@ -1,6 +1,4 @@ #!/bin/bash -i DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# https://ubuntuforums.org/showthread.php?t=2290602 -xhost + python $DIR/LPHK.py conda deactivate diff --git a/VERSION b/VERSION index d156ab4..0550e0f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.10 \ No newline at end of file +0.3.0_pr35_07042020 \ No newline at end of file diff --git a/kb.py b/kb.py index b446db8..054a9be 100644 --- a/kb.py +++ b/kb.py @@ -1,18 +1,17 @@ -import keyboard import ms +import sys -media_keys = {"vol_up" : 57392, "vol_down" : 57390, "mute" : 57376, "play_pause" : 57378, "prev_track" : 57360, "next_track" : 57369, "mouse_left" : "mouse_left","mouse_middle" : "mouse_middle", "mouse_right" : "mouse_right"} +if sys.platform == 'win32': + import system_apis.keyboard_win as keyboard_api +else: + import system_apis.keyboard_unix as keyboard_api pressed = set() + def sp(name): - try: - return keyboard.key_to_scan_codes(str(name))[0] - except ValueError: - try: - return media_keys[str(name)] - except KeyError: - return None + return keyboard_api.sp(name) + def press(key): pressed.add(key) @@ -20,7 +19,8 @@ def press(key): if "mouse_" in key: ms.press(key[6:]) return - keyboard.press(key) + keyboard_api.press(key) + def release(key): pressed.discard(key) @@ -28,12 +28,14 @@ def release(key): if "mouse_" in key: ms.release(key[6:]) return - keyboard.release(key) + keyboard_api.release(key) + def release_all(): for key in pressed.copy(): release(key) + def tap(key): if type(key) == str: if "mouse_" in key: @@ -42,3 +44,6 @@ def tap(key): press(key) release(key) + +def write(string): + keyboard_api.write(string) diff --git a/lp_colors.py b/lp_colors.py index 39463a9..9f7ec9e 100644 --- a/lp_colors.py +++ b/lp_colors.py @@ -145,6 +145,6 @@ def raw_clear(): for x in range(9): for y in range(9): if window.lp_mode == "Mk1": - lp_object.LedCtrlXY(x, y, 0, 0) + lp_object.LedCtrlXY(x, y, 0, 0) else: lp_object.LedCtrlXYByCode(x, y, 0) diff --git a/ms.py b/ms.py index f0fe601..42e71ad 100644 --- a/ms.py +++ b/ms.py @@ -2,44 +2,46 @@ from bresenham import bresenham controller = Controller() +buttons = ["left", "middle", "right"] -def getXY(): + +def get_pos(): return controller.position -def setXY(x, y): + +def set_pos(x, y): global controller controller.position = (x, y) -def moveXY(x, y): + +def move_to_pos(x, y): controller.move(x, y) + def click(button="left", clicks=1): - if button == "left": - controller.click(Button.left, clicks) - elif button == "middle": - controller.click(Button.middle, clicks) - elif button == "right": - controller.click(Button.right, clicks) + _check_button(button) + controller.click(Button[button], clicks) + def press(button="left"): - if button == "left": - controller.press(Button.left) - elif button == "middle": - controller.press(Button.middle) - elif button == "right": - controller.press(Button.right) + _check_button(button) + controller.press(Button[button]) + def release(button="left"): - if button == "left": - controller.release(Button.left) - elif button == "middle": - controller.release(Button.middle) - elif button == "right": - controller.release(Button.right) + _check_button(button) + controller.release(Button[button]) + def scroll(x, y): controller.scroll(x, y) + def line_coords(x1, y1, x2, y2): return list(bresenham(x1, y1, x2, y2)) + +def _check_button(btn): + if btn not in buttons: + raise ValueError('The mouse button specified is not valid') + diff --git a/scripts.py b/scripts.py index 6c2942d..6dacd0c 100644 --- a/scripts.py +++ b/scripts.py @@ -168,7 +168,7 @@ def main_logic(idx): if split_line[0] == "STRING": type_string = " ".join(split_line[1:]) print("[scripts] " + coords + " Type out string " + type_string) - kb.keyboard.write(type_string) + kb.write(type_string) elif split_line[0] == "DELAY": print("[scripts] " + coords + " Delay for " + split_line[1] + " seconds") delay = float(split_line[1]) @@ -232,13 +232,13 @@ def main_logic(idx): return idx + 1 elif split_line[0] == "M_STORE": print("[scripts] " + coords + " Store mouse position") - m_pos = ms.getXY() + m_pos = ms.get_pos() elif split_line[0] == "M_RECALL": if m_pos == tuple(): print("[scripts] " + coords + " No 'M_STORE' command has been run, cannot do 'M_RECALL'") else: print("[scripts] " + coords + " Recall mouse position " + str(m_pos)) - ms.setXY(m_pos[0], m_pos[1]) + ms.set_pos(m_pos[0], m_pos[1]) elif split_line[0] == "M_RECALL_LINE": x1, y1 = m_pos @@ -255,25 +255,25 @@ def main_logic(idx): else: print("[scripts] " + coords + " Recall mouse position " + str(m_pos) + " in a line by " + str(skip) + " pixels per step and wait " + split_line[1] + " milliseconds between each step") - x_C, y_C = ms.getXY() + x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) for x_M, y_M in points[::skip]: if check_kill(x, y, is_async): return -1 - ms.setXY(x_M, y_M) + ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): if not safe_sleep(delay, x, y, is_async): return -1 elif split_line[0] == "M_MOVE": if len(split_line) >= 3: print("[scripts] " + coords + " Relative mouse movement (" + split_line[1] + ", " + str(split_line[2]) + ")") - ms.moveXY(float(split_line[1]), float(split_line[2])) + ms.move_to_pos(float(split_line[1]), float(split_line[2])) else: print("[scripts] " + coords + " Both X and Y are required for mouse movement, skipping...") elif split_line[0] == "M_SET": if len(split_line) >= 3: print("[scripts] " + coords + " Set mouse position to (" + split_line[1] + ", " + str(split_line[2]) + ")") - ms.setXY(float(split_line[1]), float(split_line[2])) + ms.set_pos(float(split_line[1]), float(split_line[2])) else: print("[scripts] " + coords + " Both X and Y are required for mouse positioning, skipping...") elif split_line[0] == "M_SCROLL": @@ -306,7 +306,7 @@ def main_logic(idx): for x_M, y_M in points[::skip]: if check_kill(x, y, is_async): return -1 - ms.setXY(x_M, y_M) + ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): if not safe_sleep(delay, x, y, is_async): return -1 @@ -327,13 +327,13 @@ def main_logic(idx): else: print("[scripts] " + coords + " Mouse line move relative (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step and wait " + split_line[3] + " milliseconds between each step") - x_C, y_C = ms.getXY() + x_C, y_C = ms.get_pos() x_N, y_N = x_C + x1, y_C + y1 points = ms.line_coords(x_C, y_C, x_N, y_N) for x_M, y_M in points[::skip]: if check_kill(x, y, is_async): return -1 - ms.setXY(x_M, y_M) + ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): if not safe_sleep(delay, x, y, is_async): return -1 @@ -354,12 +354,12 @@ def main_logic(idx): else: print("[scripts] " + coords + " Mouse line set (" + split_line[1] + ", " + split_line[2] + ") by " + str(skip) + " pixels per step and wait " + split_line[3] + " milliseconds between each step") - x_C, y_C = ms.getXY() + x_C, y_C = ms.get_pos() points = ms.line_coords(x_C, y_C, x1, y1) for x_M, y_M in points[::skip]: if check_kill(x, y, is_async): return -1 - ms.setXY(x_M, y_M) + ms.set_pos(x_M, y_M) if (delay != None) and (delay > 0): if not safe_sleep(delay, x, y, is_async): return -1 @@ -426,7 +426,7 @@ def main_logic(idx): print("[scripts] " + coords + " Simple keybind: " + split_line[1]) #PRESS key = kb.sp(split_line[1]) - releasefunc = kb.release(key) + releasefunc = lambda: kb.release(key) kb.press(key) #WAIT_UNPRESSED while lp_events.pressed[x][y]: @@ -564,7 +564,7 @@ def unbind_all(): to_run = [] for x in range(9): for y in range(9): - if threads[x][y] != None: + if threads[x][y] is not None: if threads[x][y].isAlive(): threads[x][y].kill.set() files.curr_layout = None diff --git a/system_apis/keyboard_unix.py b/system_apis/keyboard_unix.py new file mode 100644 index 0000000..b2c4b6e --- /dev/null +++ b/system_apis/keyboard_unix.py @@ -0,0 +1,92 @@ +from pynput import keyboard +from pynput.keyboard import KeyCode +# from pynput.keyboard import Controller as KeyboardController +import pyautogui +from pyautogui import KEY_NAMES as pyautogui_keys + +# keyboard_controller = KeyboardController() + +media_key_map = { + "vol_up": "media_volume_up", + "vol_down": "media_volume_down", + "mute": "media_volume_mute", + "play_pause": "media_play_pause", + "prev_track": "media_previous", + "next_track": "media_next", + "mouse_left": "left", + "mouse_middle": "middle", + "mouse_right": "right", + "num0": 0x60, + "num1": 0x61, + "num2": 0x62, + "num3": 0x63, + "num4": 0x64, + "num5": 0x65, + "num6": 0x66, + "num7": 0x67, + "num8": 0x68, + "num9": 0x69, +} + +media_key_map_pyautogui = { + "alt": "alt", + "alt_gr": "altright", + "shift_r": "shiftright", + "scroll_lock": "scrolllock", + "print_screen": "printscreen", + "page_up": "pgup", + "page_down": "pgdn", + "num_lock": "numlock", + "vol_up": "volumeup", + "vol_down": "volumedown", + "mute": "volumemute", + "play_pause": "playpause", + "prev_track": "prevtrack", + "next_track": "nexttrack", + "mouse_left": "left", + "mouse_middle": "middle", + "mouse_right": "right" +} + + +def sp(name): + return _sp_pyautogui(name) + + +def _sp_pyautogui(name): + if name in media_key_map_pyautogui: + name = media_key_map_pyautogui[name] + + if name in pyautogui_keys: + return name + + return None + + +def _sp_pynput(name): + # This is safe because we know the names in the pynput lib + if name in media_key_map: + name = media_key_map[name] + + try: + return keyboard.Key[name] + except KeyError: + try: + return KeyCode.from_char(name) + except KeyError: + return None + + +def press(key): + # keyboard_controller.press(key) + pyautogui.keyDown(key) + + +def release(key): + # keyboard_controller.release(key) + pyautogui.keyUp(key) + + +def write(string): + # keyboard_controller.type(string) + pyautogui.write(string) diff --git a/system_apis/keyboard_win.py b/system_apis/keyboard_win.py new file mode 100644 index 0000000..6df071e --- /dev/null +++ b/system_apis/keyboard_win.py @@ -0,0 +1,238 @@ +import ctypes +from ctypes import wintypes +import pyautogui + +# code in this file comes from https://gist.github.com/Aniruddha-Tapas/1627257344780e5429b10bc92eb2f52a + +user32 = ctypes.WinDLL('user32', use_last_error=True) + +INPUT_MOUSE = 0 +INPUT_KEYBOARD = 1 +INPUT_HARDWARE = 2 + +KEYEVENTF_EXTENDEDKEY = 0x0001 +KEYEVENTF_KEYUP = 0x0002 +KEYEVENTF_UNICODE = 0x0004 +KEYEVENTF_SCANCODE = 0x0008 + +MAPVK_VK_TO_VSC = 0 + +# C struct definitions + +wintypes.ULONG_PTR = wintypes.WPARAM + + +class MOUSEINPUT(ctypes.Structure): + _fields_ = (("dx", wintypes.LONG), + ("dy", wintypes.LONG), + ("mouseData", wintypes.DWORD), + ("dwFlags", wintypes.DWORD), + ("time", wintypes.DWORD), + ("dwExtraInfo", wintypes.ULONG_PTR)) + + +class KEYBDINPUT(ctypes.Structure): + _fields_ = (("wVk", wintypes.WORD), + ("wScan", wintypes.WORD), + ("dwFlags", wintypes.DWORD), + ("time", wintypes.DWORD), + ("dwExtraInfo", wintypes.ULONG_PTR)) + + def __init__(self, *args, **kwds): + super(KEYBDINPUT, self).__init__(*args, **kwds) + # some programs use the scan code even if KEYEVENTF_SCANCODE + # isn't set in dwFflags, so attempt to map the correct code. + if not self.dwFlags & KEYEVENTF_UNICODE: + self.wScan = user32.MapVirtualKeyExW(self.wVk, + MAPVK_VK_TO_VSC, 0) + + +class HARDWAREINPUT(ctypes.Structure): + _fields_ = (("uMsg", wintypes.DWORD), + ("wParamL", wintypes.WORD), + ("wParamH", wintypes.WORD)) + + +class INPUT(ctypes.Structure): + class _INPUT(ctypes.Union): + _fields_ = (("ki", KEYBDINPUT), + ("mi", MOUSEINPUT), + ("hi", HARDWAREINPUT)) + + _anonymous_ = ("_input",) + _fields_ = (("type", wintypes.DWORD), + ("_input", _INPUT)) + + +LPINPUT = ctypes.POINTER(INPUT) + + +# Functions + +def press_key(hexKeyCode): + x = INPUT(type=INPUT_KEYBOARD, + ki=KEYBDINPUT(wVk=hexKeyCode)) + user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x)) + + +def release_key(hexKeyCode): + x = INPUT(type=INPUT_KEYBOARD, + ki=KEYBDINPUT(wVk=hexKeyCode, + dwFlags=KEYEVENTF_KEYUP)) + user32.SendInput(1, ctypes.byref(x), ctypes.sizeof(x)) + + +# List of all codes for keys: +# # msdn.microsoft.com/en-us/library/dd375731 +key_map = { + "alt": 0x12, + "alt_gr": 0x12, # TODO: find a way to send two keys + + "apps": 0x5D, + + "backspace": 0x08, + "caps_lock": 0x14, + + # cmd and win keys are the same + "cmd": 0x5B, + "win": 0x5B, + "win_r": 0x5C, + + "ctrl": 0x11, + "ctrl_l": 0xA2, + "ctrl_r": 0xA3, + + "delete": 0x2E, + + "down": 0x28, + "end": 0x23, + "enter": 0x0D, + "esc": 0x1B, + + "f1": 0x70, + "f2": 0x71, + "f3": 0x72, + "f4": 0x73, + "f5": 0x74, + "f6": 0x75, + "f7": 0x76, + "f8": 0x77, + "f9": 0x78, + "f10": 0x79, + "f11": 0x7A, + "f12": 0x7B, + "f13": 0x7C, + "f14": 0x7D, + "f15": 0x7E, + "f16": 0x7F, + "f17": 0x80, + "f18": 0x81, + "f19": 0x82, + "f20": 0x83, + "f21": 0x84, + "f22": 0x85, + "f23": 0x86, + "f24": 0x87, + + "home": 0x24, + "insert": 0x2D, + "left": 0x25, + + "menu": 0xA4, # left menu key + "menu_r": 0xA5, + + "mute": 0xAD, + "next_track": 0xB0, + "num_lock": 0x90, + "page_down": 0x22, + "page_up": 0x21, + "pause": 0x13, + "play_pause": 0xB3, + "prev_track": 0xB1, + "print_screen": 0x2C, + "right": 0x27, + "scroll_lock": 0x91, + + "shift": 0x10, + "shift_l": 0xA0, + "shift_r": 0xA1, + + "space": 0x20, + "tab": 0x09, + "up": 0x26, + "vol_down": 0xAE, + "vol_up": 0xAF, + + "0": 0x30, + "1": 0x31, + "2": 0x32, + "3": 0x33, + "4": 0x34, + "5": 0x35, + "6": 0x36, + "7": 0x37, + "8": 0x38, + "9": 0x39, + + "num0": 0x60, + "num1": 0x61, + "num2": 0x62, + "num3": 0x63, + "num4": 0x64, + "num5": 0x65, + "num6": 0x66, + "num7": 0x67, + "num8": 0x68, + "num9": 0x69, + + "a": 0x41, + "b": 0x42, + "c": 0x43, + "d": 0x44, + "e": 0x45, + "f": 0x46, + "g": 0x47, + "h": 0x48, + "i": 0x49, + "j": 0x4A, + "k": 0x4B, + "l": 0x4C, + "m": 0x4D, + "n": 0x4E, + "o": 0x4F, + "p": 0x50, + "q": 0x51, + "r": 0x52, + "s": 0x53, + "t": 0x54, + "u": 0x55, + "v": 0x56, + "w": 0x57, + "x": 0x58, + "y": 0x59, + "z": 0x5A, + + "+": 0xBB, + "-": 0xBD, + ".": 0xBE, + ",": 0xBC, +} + + +def sp(name): + if name in key_map: + return key_map[name] + + return None + + +def press(key): + press_key(key) + + +def release(key): + release_key(key) + + +def write(string): + pyautogui.write(string) diff --git a/utils/GET_KEYCODES.py b/utils/GET_KEYCODES.py index b9df6d4..1accb65 100644 --- a/utils/GET_KEYCODES.py +++ b/utils/GET_KEYCODES.py @@ -1,12 +1,27 @@ -import keyboard +import pyautogui +from pynput import keyboard from time import sleep -def press_callback(event): - print("Name: '" + event.name + "', Scan Code: '" + str(keyboard.key_to_scan_codes(event.name)[0]) + "'") -keyboard.on_press(press_callback) +def press_callback(key): + # print("Name: '" + event.name + "', Scan Code: '" + str(keyboard.key_to_scan_codes(event.name)[0]) + "'") + print('{0} pressed'.format(key)) + + if hasattr(key, 'vk'): + print('vk code {0} pressed'.format(key.vk)) + + +def release_callback(key): + print('{0} released'.format(key)) + + +listener = keyboard.Listener( + on_press=press_callback, + on_release=release_callback) +listener.start() print("Reading keys. Press CTRL-C to exit...") -while(True): - sleep(60) \ No newline at end of file + +while True: + sleep(60) diff --git a/utils/LIST_PADS.py b/utils/LIST_PADS.py index 4143bfe..427c2ff 100644 --- a/utils/LIST_PADS.py +++ b/utils/LIST_PADS.py @@ -1,3 +1,5 @@ +import sys + try: import launchpad_py as launchpad except ImportError: diff --git a/utils/RAW_CONNECT.py b/utils/RAW_CONNECT.py index e4922f9..902f476 100644 --- a/utils/RAW_CONNECT.py +++ b/utils/RAW_CONNECT.py @@ -1,10 +1,6 @@ -try: - import launchpad_py as launchpad -except ImportError: - try: - import launchpad - except ImportError: - sys.exit("[LPHK] Error loading launchpad.py") +# Shush pycharm +# noinspection PyUnresolvedReferences +import launchpad_connector as lpcon from getch import pause @@ -17,34 +13,19 @@ print("\nTrying to connect to launchpad...") -lp = launchpad.Launchpad() +launchpad = lpcon.get_launchpad() -if lp.Check( 0, MK2_NAME ): - lp = launchpad.LaunchpadMk2() - if lp.Open( 0, MK2_NAME ): - print('Connected to MkII! Yay!') - else: - print('MkII detected, but connection failed!') -if lp.Check( 0, MK3MINI_NAME ): - lp = launchpad.LaunchpadMk2() - if lp.Open( 0, MK3MINI_NAME ): - print('Connected to Mini Mk3! Yay!') - else: - print('Mini Mk3 detected, but connection failed!') -elif lp.Check( 0, PRO_NAME ): - lp = launchpad.LaunchpadPro() - if lp.Open( 0, PRO_NAME ): - print('Connected to Pro! Yay!') - else: - print('Pro detected, but connection failed!') -elif lp.Check( 0, CTRL_XL_NAME ) or lp.Check( 0, LAUNCHKEY_NAME ) or lp.Check( 0, DICER_NAME ): +if launchpad is -1: print('Unsupported device detected!') -elif lp.Check(): - if lp.Open(): - print('Connected to Classic/Mini/S! Yay!') - else: - print('Classic/Mini/S detected, but connection failed!') -else: +elif launchpad is None: print('Launchpad appears to be unplugged!') +else: + name = lpcon.get_display_name(launchpad) + if lpcon.connect(launchpad): + print(f'Connected to {name}! Yay!') + else: + print(f'{name} detected, but connection failed!') + pause("\nPress any key to exit...") +lpcon.disconnect(launchpad) diff --git a/utils/launchpad_connector.py b/utils/launchpad_connector.py new file mode 100644 index 0000000..b07c8a6 --- /dev/null +++ b/utils/launchpad_connector.py @@ -0,0 +1,79 @@ +import launchpad_py as launchpad + +MK2_NAME = "Launchpad MK2" +# MK3MINI_NAME = "LPMiniMK3" +MK3MINI_NAME = "mk3" +PRO_NAME = "Launchpad Pro" +LPX_NAME = "lpx" +CTRL_XL_NAME = "control xl" +LAUNCHKEY_NAME = "launchkey" +DICER_NAME = "dicer" + +PAD_MODES = { + launchpad.Launchpad: "Mk1", + launchpad.LaunchpadMk2: "Mk2", + launchpad.LaunchpadMk3: "Mk3", + launchpad.LaunchpadPro: "Pro", + launchpad.LaunchpadLPX: "Mk3" +} +PAD_TEXT = { + launchpad.Launchpad: "Classic/Mini/S", + launchpad.LaunchpadMk2: "MkII", + launchpad.LaunchpadMk3: "Mk3", + launchpad.LaunchpadPro: "Pro (BETA)", + launchpad.LaunchpadLPX: "LPX" +} + + +def get_launchpad(): + lp = launchpad.Launchpad() + + if lp.Check(0, MK2_NAME): + return launchpad.LaunchpadMk2() + # the MK3 has two midi devices, we need the second one + if lp.Check(1, MK3MINI_NAME): + return launchpad.LaunchpadMk3() + if lp.Check(0, PRO_NAME): + return launchpad.LaunchpadPro() + if lp.Check(1, LPX_NAME): + return launchpad.LaunchpadLPX() + + # unsupported pads + if lp.Check(0, CTRL_XL_NAME) or lp.Check(0, LAUNCHKEY_NAME) or lp.Check(0, DICER_NAME): + return -1 + + if lp.Check(): + return lp + + return None + + +def get_mode(pad): + cls = type(pad) + + if cls not in PAD_MODES: + return None + + return PAD_MODES[cls] + + +def get_display_name(pad): + cls = type(pad) + + if cls not in PAD_TEXT: + return "Unsupported" + + return PAD_TEXT[cls] + + +def connect(pad): + mode = get_mode(pad) + + if mode == "Mk3": + return pad.Open(1) + + return pad.Open() + + +def disconnect(pad): + pad.Close() diff --git a/window.py b/window.py index ee2b3fb..990dea1 100644 --- a/window.py +++ b/window.py @@ -1,743 +1,728 @@ -import tkinter as tk -import tkinter.filedialog, tkinter.scrolledtext, tkinter.messagebox, tkcolorpicker -from PIL import ImageTk, Image -import os, sys -from functools import partial -import webbrowser - -import scripts, files, lp_colors, lp_events - -BUTTON_SIZE = 40 -HS_SIZE = 200 -V_WIDTH = 50 -STAT_ACTIVE_COLOR = "#080" -STAT_INACTIVE_COLOR = "#444" -SELECT_COLOR = "#f00" -DEFAULT_COLOR = [0, 0, 255] -MK1_DEFAULT_COLOR = [0, 255, 0] -INDICATOR_BPM = 480 -BUTTON_FONT = ("helvetica", 11, "bold") - -MK2_NAME = "Launchpad MK2" -MK3MINI_NAME = "LPMiniMK3" -PRO_NAME = "Launchpad Pro" -CTRL_XL_NAME = "control xl" -LAUNCHKEY_NAME = "launchkey" -DICER_NAME = "dicer" - -PATH = None -PROG_PATH = None -USER_PATH = None - -VERSION = None - -PLATFORM = None - -MAIN_ICON = None - -launchpad = None - -root = None -app = None -root_destroyed = None -restart = False -lp_object = None - - -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])] - -save_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT])] -save_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT])] - -lp_connected = False -lp_mode = None -colors_to_set = [[DEFAULT_COLOR for y in range(9)] for x in range(9)] - -def init(lp_object_in, launchpad_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 - global VERSION - 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: - MAIN_ICON = os.path.join(PATH, "resources", "LPHK.gif") - - make() - -class Main_Window(tk.Frame): - 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")) - self.error_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/error.png")) - self.alert_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/alert.png")) - 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.last_clicked = None - self.outline_box = None - - def init_window(self): - global root - - self.master.title("LPHK - Novation Launchpad Macro Scripting System") - self.pack(fill="both", expand=1) - - self.m = tk.Menu(self.master) - self.master.config(menu=self.m) - - 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_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.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) - open_scripting = lambda: webbrowser.open("https://github.com/nimaid/LPHK#what-is-lphkscript-table-of-contents") - self.m_Help.add_command(label="Scripting Help...", command=open_scripting) - open_user_folder = lambda: files.open_file_folder(USER_PATH) - self.m_Help.add_command(label="User Folder...", command=open_user_folder) - open_prog_folder = lambda: files.open_file_folder(PROG_PATH) - self.m_Help.add_command(label="Program Folder...", command=open_prog_folder) - display_info = lambda: self.popup(self, "About LPHK", self.about_image, "A Novation Launchpad Macro Scripting System\nMade by Ella Jameson (nimaid)\n\nVersion: " + VERSION + "\nFile format version: " + files.FILE_VERSION, "Done") - self.m_Help.add_command(label="About LPHK", command=display_info) - self.m.add_cascade(label="Help", menu=self.m_Help) - - 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.stat = tk.Label(self, text="No Launchpad Connected", bg=STAT_INACTIVE_COLOR, fg="#fff") - self.stat.grid(row=1, column=0, sticky=tk.EW) - self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) - - 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") - - def connect_lp(self): - global lp_connected - global lp_mode - global lp_object - try: - if lp_object.Check( 0, MK2_NAME ): - lp_object = launchpad.LaunchpadMk2() - if lp_object.Open( 0, MK2_NAME ): - lp_connected = True - lp_mode = "Mk2" - lp_object.ButtonFlush() - lp_object.LedCtrlBpm(INDICATOR_BPM) - lp_events.start(lp_object) - self.draw_canvas() - self.enable_menu("Layout") - - self.stat["text"] = "Connected to Launchpad MkII" - self.stat["bg"] = STAT_ACTIVE_COLOR - elif lp_object.Check( 0, MK3MINI_NAME ): - lp_object = launchpad.LaunchpadMk2() - if lp_object.Open( 0, MK3MINI_NAME ): - lp_connected = True - lp_mode = "Mk2" - lp_object.ButtonFlush() - lp_object.LedCtrlBpm(INDICATOR_BPM) - lp_events.start(lp_object) - self.draw_canvas() - self.enable_menu("Layout") - - self.stat["text"] = "Connected to Launchpad Mini Mk3" - self.stat["bg"] = STAT_ACTIVE_COLOR - elif lp_object.Check( 0, PRO_NAME ): - lp_object = launchpad.LaunchpadPro() - if lp_object.Open( 0, PRO_NAME ): - 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.\nI (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)\nYou 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\nupper left corner, then release the 'Setup' key. Please only continue once this step is completed.", "I am in Live mode.") - lp_connected = True - lp_mode = "Pro" - lp_object.ButtonFlush() - lp_object.LedCtrlBpm(INDICATOR_BPM) - lp_events.start(lp_object) - self.draw_canvas() - self.enable_menu("Layout") - - self.stat["text"] = "Connected to Launchpad Pro (BETA)" - self.stat["bg"] = STAT_ACTIVE_COLOR - elif lp_object.Check( 0, CTRL_XL_NAME ) or lp_object.Check( 0, LAUNCHKEY_NAME ) or lp_object.Check( 0, DICER_NAME ): - 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.\nPlease voice your feature requests on the Discord or on GitHub.", "OK") - elif lp_object.Check(): - if lp_object.Open(): - lp_connected = True - lp_mode = "Mk1" - lp_object.ButtonFlush() - lp_events.start(lp_object) - self.draw_canvas() - self.enable_menu("Layout") - self.stat["text"] = "Connected to Launchpad Classic/Mini/S" - self.stat["bg"] = STAT_ACTIVE_COLOR - else: - raise Exception() - except: - self.popup_choice(self, "No Launchpad Detected...", self.error_image, "Could not detect any connected Launchpads!\nDisconnect and reconnect your USB cable,\nthen click 'Redetect Now'.", [["Ignore", None], ["Redetect Now", self.redetect_lp]]) - - def disconnect_lp(self): - global lp_connected - try: - scripts.unbind_all() - lp_events.timer.cancel() - lp_object.Close() - except: - self.redetect_lp() - lp_connected = False - - self.clear_canvas() - - self.disable_menu("Layout") - - self.stat["text"] = "No Launchpad Connected" - self.stat["bg"] = STAT_INACTIVE_COLOR - - def redetect_lp(self): - global restart - restart = True - close() - - def unbind_lp(self, prompt_save=True): - if prompt_save: - self.modified_layout_save_prompt() - scripts.unbind_all() - files.curr_layout = None - self.draw_canvas() - - def load_layout(self): - self.modified_layout_save_prompt() - name = tk.filedialog.askopenfilename(parent=app, - initialdir=files.LAYOUT_PATH, - title="Load layout", - filetypes=load_layout_filetypes) - if name: - files.load_layout_to_lp(name) - - def save_layout_as(self): - name = tk.filedialog.asksaveasfilename(parent=app, - initialdir=files.LAYOUT_PATH, - title="Save layout as...", - filetypes=save_layout_filetypes) - if name: - if files.LAYOUT_EXT not in name: - name += files.LAYOUT_EXT - files.save_lp_to_layout(name) - files.load_layout_to_lp(name) - - def save_layout(self): - if files.curr_layout == None: - self.save_layout_as() - else: - files.save_lp_to_layout(files.curr_layout) - files.load_layout_to_lp(files.curr_layout) - - def click(self, event): - gap = int(BUTTON_SIZE // 4) - - - column = min(8, int(event.x // (BUTTON_SIZE + gap))) - row = min(8, int(event.y // (BUTTON_SIZE + gap))) - - if self.grid_drawn: - if(column, row) == (8, 0): - #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" - else: - self.button_mode = "edit" - self.draw_canvas() - else: - if self.button_mode == "edit": - self.last_clicked = (column, row) - self.draw_canvas() - self.script_entry_window(column, row) - self.last_clicked = None - 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": - if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): - self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to move a button to an already\nbound button. What would you like to do?", [["Overwrite", move_func], ["Swap", swap_func], ["Cancel", None]]) - else: - move_func() - elif self.button_mode == "copy": - if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): - 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": - swap_func() - self.last_clicked = None - self.draw_canvas() - - def draw_button(self, column, row, color="#000000", shape="square"): - gap = int(BUTTON_SIZE // 4) - - x_start = round((BUTTON_SIZE * column) + (gap * column) + (gap / 2)) - y_start = round((BUTTON_SIZE * row) + (gap * row) + (gap / 2)) - x_end = x_start + BUTTON_SIZE - y_end = y_start + BUTTON_SIZE - - if shape == "square": - return self.c.create_rectangle(x_start, y_start, x_end, y_end, fill=color, outline="") - elif shape == "circle": - 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): - 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: - self.outline_box = self.c.create_rectangle(x_start, y_start, x_end, y_end, fill=SELECT_COLOR, outline="") - self.c.tag_lower(self.outline_box) - else: - 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)) - - for x in range(8): - for y in range(1, 9): - self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) - - self.c.itemconfig(self.grid_rects[8][0], text=self.button_mode.capitalize()) - else: - for x in range(8): - y = 0 - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") - - for y in range(1, 9): - x = 8 - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") - - for x in range(8): - for y in range(1, 9): - self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y)) - - gap = int(BUTTON_SIZE // 4) - text_x = round((BUTTON_SIZE * 8) + (gap * 8) + (BUTTON_SIZE / 2) + (gap / 2)) - text_y = round((BUTTON_SIZE / 2) + (gap / 2)) - self.grid_rects[8][0] = self.c.create_text(text_x, text_y, text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) - - self.grid_drawn = True - - def clear_canvas(self): - self.c.delete("all") - self.grid_rects = [[None for y in range(9)] for x in range(9)] - self.grid_drawn = False - - 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) - if MAIN_ICON != None: - if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": - dummy = None - #w.call('wm', 'iconphoto', w._w, tk.PhotoImage(file=MAIN_ICON)) - else: - w.iconbitmap(MAIN_ICON) - - def validate_func(): - nonlocal x, y, t - - text_string = t.get(1.0, tk.END) - try: - script_validate = scripts.validate_script(text_string) - except: - #self.save_script(w, x, y, text_string) # This will fail and throw a popup error - self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") - raise - if script_validate != True and files.in_error: - self.save_script(w, x, y, text_string) - else: - w.destroy() - w.protocol("WM_DELETE_WINDOW", validate_func) - - e_m = tk.Menu(w) - w.config(menu=e_m) - - e_m_Script = tk.Menu(e_m, tearoff=False) - - 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.text[x][y]) - else: - t.insert(tk.INSERT, text_override) - t.bind("<>", self.custom_paste) - t.bind("", self.select_all) - - import_script_func = lambda: self.import_script(t, w) - e_m_Script.add_command(label="Import script", command=import_script_func) - export_script_func = lambda: self.export_script(t, w) - e_m_Script.add_command(label="Export script", command=export_script_func) - e_m.add_cascade(label="Script", menu=e_m_Script) - - if color_override == None: - colors_to_set[x][y] = lp_colors.getXY(x, y) - else: - colors_to_set[x][y] = color_override - - if type(colors_to_set[x][y]) == int: - colors_to_set[x][y] = lp_colors.code_to_RGB(colors_to_set[x][y]) - - if all(c < 4 for c in colors_to_set[x][y]): - if lp_mode == "Mk1": - colors_to_set[x][y] = MK1_DEFAULT_COLOR - else: - colors_to_set[x][y] = DEFAULT_COLOR - - ask_color_func = lambda: self.ask_color(w, color_button, x, y, colors_to_set[x][y]) - color_button = tk.Button(w, text="Select Color", command=ask_color_func) - color_button.grid(column=1, row=0, padx=(0, 10), pady=(10, 50), sticky="nesw") - color_button.config(font=BUTTON_FONT) - start_color_str = lp_colors.list_RGB_to_string(colors_to_set[x][y]) - self.button_color_with_text_update(color_button, start_color_str) - - save_script_func = lambda: self.save_script(w, x, y, t.get(1.0, tk.END)) - save_button = tk.Button(w, text="Bind Button (" + str(x) + ", " + str(y) + ")", command=save_script_func) - save_button.grid(column=1, row=1, padx=(0,10), sticky="nesw") - save_button.config(font=BUTTON_FONT) - save_button.config(bg="#c3d9C3") - - unbind_func = lambda: self.unbind_destroy(x, y, w) - unbind_button = tk.Button(w, text="Unbind Button (" + str(x) + ", " + str(y) + ")", command=unbind_func) - unbind_button.grid(column=1, row=2, padx=(0,10), pady=10, sticky="nesw") - unbind_button.config(font=BUTTON_FONT) - unbind_button.config(bg="#d9c3c3") - - w.wait_visibility() - w.grab_set() - t.focus_set() - w.wait_window() - - def classic_askcolor(self, color=(255, 0, 0), title="Color Chooser"): - w = tk.Toplevel(self) - w.winfo_toplevel().title(title) - w.resizable(False, False) - if MAIN_ICON != None: - if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": - dummy = None - #w.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) - else: - w.iconbitmap(MAIN_ICON) - - w.protocol("WM_DELETE_WINDOW", w.destroy) - - color = "" - - def return_color(col): - nonlocal color - color = col - w.destroy() - - button_frame = tk.Frame(w) - button_frame.grid(padx=(10, 0), pady=(10, 0)) - - def make_grid_button(column, row, color_hex, func=None, size=100): - nonlocal w - f = tk.Frame(button_frame, width=size, height=size) - - b = tk.Button(f, command=func) - - f.rowconfigure(0, weight = 1) - f.columnconfigure(0, weight = 1) - f.grid_propagate(0) - - f.grid(column=column, row=row) - b.grid(padx=(0,10), pady=(0,10), sticky="nesw") - b.config(bg=color_hex) - - def make_color_button(button_color, column, row, size=100): - button_color_hex = "#%02x%02x%02x" % button_color - - b_func = lambda: return_color(button_color) - make_grid_button(column, row, button_color_hex, b_func, size) - - for c in range(4): - for r in range(4): - if not (c == 0 and r == 3): - red = int(c * (255 / 3)) - green = int((3 - r) * (255 / 3)) - - make_color_button((red, green, 0), c, r, size=75) - - w.wait_visibility() - w.grab_set() - w.wait_window() - - if color: - hex = "#%02x%02x%02x" % color - return color, hex - else: - return None, None - - def ask_color(self, window, button, x, y, default_color): - global colors_to_set - - if lp_mode == "Mk1": - color = self.classic_askcolor(color=tuple(default_color), title="Select Color for Button (" + str(x) + ", " + str(y) + ")") - else: - color = tkcolorpicker.askcolor(color=tuple(default_color), parent=window, title="Select Color for Button (" + str(x) + ", " + str(y) + ")") - if color[0] != None: - color_to_set = [int(min(255, max(0, c))) for c in color[0]] - if all(c < 4 for c in color_to_set): - rerun = lambda: self.ask_color(window, button, x, y, default_color) - self.popup(window, "Invalid Color", self.warning_image, "That color is too dark to see.", "OK", rerun) - else: - colors_to_set[x][y] = color_to_set - self.button_color_with_text_update(button, color[1]) - - def button_color_with_text_update(self, button, color): - button.configure(bg=color, activebackground=color) - color_rgb = [] - for c in range(3): - start_index = c * 2 - val = color[start_index + 1:start_index + 3] - color_rgb.append(int(val, 16)) - luminance = lp_colors.luminance(color_rgb[0], color_rgb[1], color_rgb[2]) - if luminance > 0.5: - button.configure(fg="black", activeforeground="black") - else: - button.configure(fg="white", activeforeground="white") - - def custom_paste(self, event): - try: - event.widget.delete("sel.first", "sel.last") - except: - pass - event.widget.insert("insert", event.widget.clipboard_get()) - return "break" - - def select_all(self, event): - event.widget.tag_add(tk.SEL, "1.0", tk.END) - event.widget.mark_set(tk.INSERT, "1.0") - event.widget.see(tk.INSERT) - return "break" - - def unbind_destroy(self, x, y, window): - scripts.unbind(x, y) - self.draw_canvas() - 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: - script_validate = scripts.validate_script(script_text) - except: - self.popup(window, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = open_editor_func) - raise - if script_validate == True: - if script_text != "": - script_text = files.strip_lines(script_text) - scripts.bind(x, y, script_text, colors_to_set[x][y]) - self.draw_canvas() - lp_colors.updateXY(x, y) - window.destroy() - else: - self.popup(window, "No Script Entered", self.info_image, "Please enter a script to bind.", "OK", end_command = open_editor_func) - else: - self.popup(window, "(" + str(x) + ", " + str(y) + ") Syntax Error", self.error_image, "Error in line: " + script_validate[1] + "\n" + script_validate[0], "OK", end_command = open_editor_func) - - def import_script(self, textbox, window): - name = tk.filedialog.askopenfilename(parent=window, - initialdir=files.SCRIPT_PATH, - title="Import script", - filetypes=load_script_filetypes) - if name: - text = files.import_script(name) - text = files.strip_lines(text) - textbox.delete("1.0", tk.END) - textbox.insert(tk.INSERT, text) - - def export_script(self, textbox, window): - name = tk.filedialog.asksaveasfilename(parent=window, - initialdir=files.SCRIPT_PATH, - title="Export script", - filetypes=save_script_filetypes) - if name: - if files.SCRIPT_EXT not in name: - name += files.SCRIPT_EXT - text = textbox.get("1.0", tk.END) - text = files.strip_lines(text) - files.export_script(name, text) - - def popup(self, window, title, image, text, button_text, end_command=None): - popup = tk.Toplevel(window) - popup.resizable(False, False) - if MAIN_ICON != None: - if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": - dummy = None - #popup.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) - else: - popup.iconbitmap(MAIN_ICON) - popup.wm_title(title) - popup.tkraise(window) - - def run_end(): - popup.destroy() - if end_command != None: - end_command() - - picture_label = tk.Label(popup, image=image) - picture_label.photo = image - picture_label.grid(column=0, row=0, rowspan=2, padx=10, pady=10) - tk.Label(popup, text=text, justify=tk.CENTER).grid(column=1, row=0, padx=10, pady=10) - tk.Button(popup, text=button_text, command=run_end).grid(column=1, row=1, padx=10, pady=10) - 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) - if MAIN_ICON != None: - if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": - dummy = None - #popup.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) - else: - popup.iconbitmap(MAIN_ICON) - popup.wm_title(title) - popup.tkraise(window) - - def run_end(func): - popup.destroy() - if func != None: - func() - - picture_label = tk.Label(popup, image=image) - picture_label.photo = image - picture_label.grid(column=0, row=0, rowspan=2, padx=10, pady=10) - tk.Label(popup, text=text, justify=tk.CENTER).grid(column=1, row=0, columnspan=len(choices), padx=10, pady=10) - for idx, choice in enumerate(choices): - run_end_func = partial(run_end, choice[1]) - tk.Button(popup, text=choice[0], command=run_end_func).grid(column=1 + idx, row=1, padx=10, pady=10) - 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 - for x_texts in scripts.text: - for text in x_texts: - if text != "": - 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]]) - -def make(): - global root - global app - global root_destroyed - global redetect_before_start - root = tk.Tk() - root_destroyed = False - root.protocol("WM_DELETE_WINDOW", close) - root.resizable(False, False) - if MAIN_ICON != None: - if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": - root.call('wm', 'iconphoto', root._w, tk.PhotoImage(file=MAIN_ICON)) - else: - root.iconbitmap(MAIN_ICON) - app = Main_Window(root) - app.raise_above_all() - app.after(100, app.connect_lp) - app.mainloop() - -def close(): - global root_destroyed - app.modified_layout_save_prompt() - app.disconnect_lp() - if not root_destroyed: - root.destroy() - root_destroyed = True +import tkinter as tk +import tkinter.filedialog, tkinter.scrolledtext, tkinter.messagebox, tkcolorpicker +from PIL import ImageTk, Image +import os, sys +from functools import partial +import webbrowser + +import scripts, files, lp_colors, lp_events +from utils import launchpad_connector as lpcon + +BUTTON_SIZE = 40 +HS_SIZE = 200 +V_WIDTH = 50 +STAT_ACTIVE_COLOR = "#080" +STAT_INACTIVE_COLOR = "#444" +SELECT_COLOR = "#f00" +DEFAULT_COLOR = [0, 0, 255] +MK1_DEFAULT_COLOR = [0, 255, 0] +INDICATOR_BPM = 480 +BUTTON_FONT = ("helvetica", 11, "bold") + +PATH = None +PROG_PATH = None +USER_PATH = None + +VERSION = None + +PLATFORM = None + +MAIN_ICON = None + +launchpad = None + +root = None +app = None +root_destroyed = None +restart = False +lp_object = None + + +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])] + +save_layout_filetypes = [('LPHK layout files', [files.LAYOUT_EXT])] +save_script_filetypes = [('LPHK script files', [files.SCRIPT_EXT])] + +lp_connected = False +lp_mode = None +colors_to_set = [[DEFAULT_COLOR for y in range(9)] for x in range(9)] + + +def init(lp_object_in, launchpad_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 + global VERSION + 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: + MAIN_ICON = os.path.join(PATH, "resources", "LPHK.gif") + + make() + + +class Main_Window(tk.Frame): + 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")) + self.error_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/error.png")) + self.alert_image = ImageTk.PhotoImage(Image.open(PATH + "/resources/alert.png")) + 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.last_clicked = None + self.outline_box = None + + def init_window(self): + global root + + self.master.title("LPHK - Novation Launchpad Macro Scripting System") + self.pack(fill="both", expand=1) + + self.m = tk.Menu(self.master) + self.master.config(menu=self.m) + + 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_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.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) + open_scripting = lambda: webbrowser.open("https://github.com/nimaid/LPHK#what-is-lphkscript-table-of-contents") + self.m_Help.add_command(label="Scripting Help...", command=open_scripting) + open_user_folder = lambda: files.open_file_folder(USER_PATH) + self.m_Help.add_command(label="User Folder...", command=open_user_folder) + open_prog_folder = lambda: files.open_file_folder(PROG_PATH) + self.m_Help.add_command(label="Program Folder...", command=open_prog_folder) + display_info = lambda: self.popup(self, "About LPHK", self.about_image, "A Novation Launchpad Macro Scripting System\nMade by Ella Jameson (nimaid)\n\nVersion: " + VERSION + "\nFile format version: " + files.FILE_VERSION, "Done") + self.m_Help.add_command(label="About LPHK", command=display_info) + self.m.add_cascade(label="Help", menu=self.m_Help) + + 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.stat = tk.Label(self, text="No Launchpad Connected", bg=STAT_INACTIVE_COLOR, fg="#fff") + self.stat.grid(row=1, column=0, sticky=tk.EW) + self.stat.config(font=("Courier", BUTTON_SIZE // 3, "bold")) + + 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") + + def connect_lp(self): + global lp_connected + global lp_mode + global lp_object + + lp = lpcon.get_launchpad() + + if lp is -1: + self.popup(self, "Connect to Unsupported Device", self.error_image, + """The device you are attempting to use is not currently supported by LPHK, + and there are no plans to add support for it. + Please voice your feature requests on the Discord or on GitHub.""", + "OK") + + if lp is None: + self.popup_choice(self, "No Launchpad Detected...", self.error_image, + """Could not detect any connected Launchpads! + Disconnect and reconnect your USB cable, + then click 'Redetect Now'.""", + [["Ignore", None], ["Redetect Now", self.redetect_lp]] + ) + return + + 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_object.ButtonFlush() + + # special case? + if lp_mode is not "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 + + def disconnect_lp(self): + global lp_connected + try: + scripts.unbind_all() + lp_events.timer.cancel() + lpcon.disconnect(lp_object) + except: + self.redetect_lp() + lp_connected = False + + self.clear_canvas() + + self.disable_menu("Layout") + + self.stat["text"] = "No Launchpad Connected" + self.stat["bg"] = STAT_INACTIVE_COLOR + + def redetect_lp(self): + global restart + restart = True + close() + + def unbind_lp(self, prompt_save=True): + if prompt_save: + self.modified_layout_save_prompt() + scripts.unbind_all() + files.curr_layout = None + self.draw_canvas() + + def load_layout(self): + self.modified_layout_save_prompt() + name = tk.filedialog.askopenfilename(parent=app, + initialdir=files.LAYOUT_PATH, + title="Load layout", + filetypes=load_layout_filetypes) + if name: + files.load_layout_to_lp(name) + + def save_layout_as(self): + name = tk.filedialog.asksaveasfilename(parent=app, + initialdir=files.LAYOUT_PATH, + title="Save layout as...", + filetypes=save_layout_filetypes) + if name: + if files.LAYOUT_EXT not in name: + name += files.LAYOUT_EXT + files.save_lp_to_layout(name) + files.load_layout_to_lp(name) + + def save_layout(self): + if files.curr_layout == None: + self.save_layout_as() + else: + files.save_lp_to_layout(files.curr_layout) + files.load_layout_to_lp(files.curr_layout) + + def click(self, event): + gap = int(BUTTON_SIZE // 4) + + + column = min(8, int(event.x // (BUTTON_SIZE + gap))) + row = min(8, int(event.y // (BUTTON_SIZE + gap))) + + if self.grid_drawn: + if(column, row) == (8, 0): + #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" + else: + self.button_mode = "edit" + self.draw_canvas() + else: + if self.button_mode == "edit": + self.last_clicked = (column, row) + self.draw_canvas() + self.script_entry_window(column, row) + self.last_clicked = None + 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": + if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): + self.popup_choice(self, "Button Already Bound", self.warning_image, "You are attempting to move a button to an already\nbound button. What would you like to do?", [["Overwrite", move_func], ["Swap", swap_func], ["Cancel", None]]) + else: + move_func() + elif self.button_mode == "copy": + if scripts.is_bound(column, row) and ((self.last_clicked) != (column, row)): + 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": + swap_func() + self.last_clicked = None + self.draw_canvas() + + def draw_button(self, column, row, color="#000000", shape="square"): + gap = int(BUTTON_SIZE // 4) + + x_start = round((BUTTON_SIZE * column) + (gap * column) + (gap / 2)) + y_start = round((BUTTON_SIZE * row) + (gap * row) + (gap / 2)) + x_end = x_start + BUTTON_SIZE + y_end = y_start + BUTTON_SIZE + + if shape == "square": + return self.c.create_rectangle(x_start, y_start, x_end, y_end, fill=color, outline="") + elif shape == "circle": + 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): + 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: + self.outline_box = self.c.create_rectangle(x_start, y_start, x_end, y_end, fill=SELECT_COLOR, outline="") + self.c.tag_lower(self.outline_box) + else: + 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)) + + for x in range(8): + for y in range(1, 9): + self.c.itemconfig(self.grid_rects[x][y], fill=lp_colors.getXY_RGB(x, y)) + + self.c.itemconfig(self.grid_rects[8][0], text=self.button_mode.capitalize()) + else: + for x in range(8): + y = 0 + self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") + + for y in range(1, 9): + x = 8 + self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y), shape="circle") + + for x in range(8): + for y in range(1, 9): + self.grid_rects[x][y] = self.draw_button(x, y, color=lp_colors.getXY_RGB(x, y)) + + gap = int(BUTTON_SIZE // 4) + text_x = round((BUTTON_SIZE * 8) + (gap * 8) + (BUTTON_SIZE / 2) + (gap / 2)) + text_y = round((BUTTON_SIZE / 2) + (gap / 2)) + self.grid_rects[8][0] = self.c.create_text(text_x, text_y, text=self.button_mode.capitalize(), font=("Courier", BUTTON_SIZE // 3, "bold")) + + self.grid_drawn = True + + def clear_canvas(self): + self.c.delete("all") + self.grid_rects = [[None for y in range(9)] for x in range(9)] + self.grid_drawn = False + + 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) + + if MAIN_ICON != None: + if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": + dummy = None + #w.call('wm', 'iconphoto', w._w, tk.PhotoImage(file=MAIN_ICON)) + else: + w.iconbitmap(MAIN_ICON) + + def validate_func(): + nonlocal x, y, t + + text_string = t.get(1.0, tk.END) + try: + script_validate = scripts.validate_script(text_string) + except: + #self.save_script(w, x, y, text_string) # This will fail and throw a popup error + self.popup(w, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK") + raise + if script_validate != True and files.in_error: + self.save_script(w, x, y, text_string) + else: + w.destroy() + w.protocol("WM_DELETE_WINDOW", validate_func) + + e_m = tk.Menu(w) + w.config(menu=e_m) + + e_m_Script = tk.Menu(e_m, tearoff=False) + + 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.text[x][y]) + else: + t.insert(tk.INSERT, text_override) + t.bind("<>", self.custom_paste) + t.bind("", self.select_all) + + import_script_func = lambda: self.import_script(t, w) + e_m_Script.add_command(label="Import script", command=import_script_func) + export_script_func = lambda: self.export_script(t, w) + e_m_Script.add_command(label="Export script", command=export_script_func) + e_m.add_cascade(label="Script", menu=e_m_Script) + + if color_override == None: + colors_to_set[x][y] = lp_colors.getXY(x, y) + else: + colors_to_set[x][y] = color_override + + if type(colors_to_set[x][y]) == int: + colors_to_set[x][y] = lp_colors.code_to_RGB(colors_to_set[x][y]) + + if all(c < 4 for c in colors_to_set[x][y]): + if lp_mode == "Mk1": + colors_to_set[x][y] = MK1_DEFAULT_COLOR + else: + colors_to_set[x][y] = DEFAULT_COLOR + + ask_color_func = lambda: self.ask_color(w, color_button, x, y, colors_to_set[x][y]) + color_button = tk.Button(w, text="Select Color", command=ask_color_func) + color_button.grid(column=1, row=0, padx=(0, 10), pady=(10, 50), sticky="nesw") + color_button.config(font=BUTTON_FONT) + start_color_str = lp_colors.list_RGB_to_string(colors_to_set[x][y]) + self.button_color_with_text_update(color_button, start_color_str) + + save_script_func = lambda: self.save_script(w, x, y, t.get(1.0, tk.END)) + save_button = tk.Button(w, text="Bind Button (" + str(x) + ", " + str(y) + ")", command=save_script_func) + save_button.grid(column=1, row=1, padx=(0,10), sticky="nesw") + save_button.config(font=BUTTON_FONT) + save_button.config(bg="#c3d9C3") + + unbind_func = lambda: self.unbind_destroy(x, y, w) + unbind_button = tk.Button(w, text="Unbind Button (" + str(x) + ", " + str(y) + ")", command=unbind_func) + unbind_button.grid(column=1, row=2, padx=(0,10), pady=10, sticky="nesw") + unbind_button.config(font=BUTTON_FONT) + unbind_button.config(bg="#d9c3c3") + + w.wait_visibility() + w.grab_set() + t.focus_set() + w.wait_window() + + def classic_askcolor(self, color=(255, 0, 0), title="Color Chooser"): + w = tk.Toplevel(self) + w.winfo_toplevel().title(title) + w.resizable(False, False) + if MAIN_ICON != None: + if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": + dummy = None + #w.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) + else: + w.iconbitmap(MAIN_ICON) + + w.protocol("WM_DELETE_WINDOW", w.destroy) + + color = "" + + def return_color(col): + nonlocal color + color = col + w.destroy() + + button_frame = tk.Frame(w) + button_frame.grid(padx=(10, 0), pady=(10, 0)) + + def make_grid_button(column, row, color_hex, func=None, size=100): + nonlocal w + f = tk.Frame(button_frame, width=size, height=size) + + b = tk.Button(f, command=func) + + f.rowconfigure(0, weight = 1) + f.columnconfigure(0, weight = 1) + f.grid_propagate(0) + + f.grid(column=column, row=row) + b.grid(padx=(0,10), pady=(0,10), sticky="nesw") + b.config(bg=color_hex) + + def make_color_button(button_color, column, row, size=100): + button_color_hex = "#%02x%02x%02x" % button_color + + b_func = lambda: return_color(button_color) + make_grid_button(column, row, button_color_hex, b_func, size) + + for c in range(4): + for r in range(4): + if not (c == 0 and r == 3): + red = int(c * (255 / 3)) + green = int((3 - r) * (255 / 3)) + + make_color_button((red, green, 0), c, r, size=75) + + w.wait_visibility() + w.grab_set() + w.wait_window() + + if color: + hex = "#%02x%02x%02x" % color + return color, hex + else: + return None, None + + def ask_color(self, window, button, x, y, default_color): + global colors_to_set + + if lp_mode == "Mk1": + color = self.classic_askcolor(color=tuple(default_color), title="Select Color for Button (" + str(x) + ", " + str(y) + ")") + else: + color = tkcolorpicker.askcolor(color=tuple(default_color), parent=window, title="Select Color for Button (" + str(x) + ", " + str(y) + ")") + if color[0] != None: + color_to_set = [int(min(255, max(0, c))) for c in color[0]] + if all(c < 4 for c in color_to_set): + rerun = lambda: self.ask_color(window, button, x, y, default_color) + self.popup(window, "Invalid Color", self.warning_image, "That color is too dark to see.", "OK", rerun) + else: + colors_to_set[x][y] = color_to_set + self.button_color_with_text_update(button, color[1]) + + def button_color_with_text_update(self, button, color): + button.configure(bg=color, activebackground=color) + color_rgb = [] + for c in range(3): + start_index = c * 2 + val = color[start_index + 1:start_index + 3] + color_rgb.append(int(val, 16)) + luminance = lp_colors.luminance(color_rgb[0], color_rgb[1], color_rgb[2]) + if luminance > 0.5: + button.configure(fg="black", activeforeground="black") + else: + button.configure(fg="white", activeforeground="white") + + def custom_paste(self, event): + try: + event.widget.delete("sel.first", "sel.last") + except: + pass + event.widget.insert("insert", event.widget.clipboard_get()) + return "break" + + def select_all(self, event): + event.widget.tag_add(tk.SEL, "1.0", tk.END) + event.widget.mark_set(tk.INSERT, "1.0") + event.widget.see(tk.INSERT) + return "break" + + def unbind_destroy(self, x, y, window): + scripts.unbind(x, y) + self.draw_canvas() + 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: + script_validate = scripts.validate_script(script_text) + except: + self.popup(window, "Script Validation Error", self.error_image, "Fatal error while attempting to validate script.\nPlease see LPHK.log for more information.", "OK", end_command = open_editor_func) + raise + if script_validate == True: + if script_text != "": + script_text = files.strip_lines(script_text) + scripts.bind(x, y, script_text, colors_to_set[x][y]) + self.draw_canvas() + lp_colors.updateXY(x, y) + window.destroy() + else: + self.popup(window, "No Script Entered", self.info_image, "Please enter a script to bind.", "OK", end_command = open_editor_func) + else: + self.popup(window, "(" + str(x) + ", " + str(y) + ") Syntax Error", self.error_image, "Error in line: " + script_validate[1] + "\n" + script_validate[0], "OK", end_command = open_editor_func) + + def import_script(self, textbox, window): + name = tk.filedialog.askopenfilename(parent=window, + initialdir=files.SCRIPT_PATH, + title="Import script", + filetypes=load_script_filetypes) + if name: + text = files.import_script(name) + text = files.strip_lines(text) + textbox.delete("1.0", tk.END) + textbox.insert(tk.INSERT, text) + + def export_script(self, textbox, window): + name = tk.filedialog.asksaveasfilename(parent=window, + initialdir=files.SCRIPT_PATH, + title="Export script", + filetypes=save_script_filetypes) + if name: + if files.SCRIPT_EXT not in name: + name += files.SCRIPT_EXT + text = textbox.get("1.0", tk.END) + text = files.strip_lines(text) + files.export_script(name, text) + + def popup(self, window, title, image, text, button_text, end_command=None): + popup = tk.Toplevel(window) + popup.resizable(False, False) + if MAIN_ICON != None: + if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": + dummy = None + #popup.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) + else: + popup.iconbitmap(MAIN_ICON) + popup.wm_title(title) + popup.tkraise(window) + + def run_end(): + popup.destroy() + if end_command != None: + end_command() + + picture_label = tk.Label(popup, image=image) + picture_label.photo = image + picture_label.grid(column=0, row=0, rowspan=2, padx=10, pady=10) + tk.Label(popup, text=text, justify=tk.CENTER).grid(column=1, row=0, padx=10, pady=10) + tk.Button(popup, text=button_text, command=run_end).grid(column=1, row=1, padx=10, pady=10) + 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) + if MAIN_ICON != None: + if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": + dummy = None + #popup.call('wm', 'iconphoto', popup._w, tk.PhotoImage(file=MAIN_ICON)) + else: + popup.iconbitmap(MAIN_ICON) + popup.wm_title(title) + popup.tkraise(window) + + def run_end(func): + popup.destroy() + if func != None: + func() + + picture_label = tk.Label(popup, image=image) + picture_label.photo = image + picture_label.grid(column=0, row=0, rowspan=2, padx=10, pady=10) + tk.Label(popup, text=text, justify=tk.CENTER).grid(column=1, row=0, columnspan=len(choices), padx=10, pady=10) + for idx, choice in enumerate(choices): + run_end_func = partial(run_end, choice[1]) + tk.Button(popup, text=choice[0], command=run_end_func).grid(column=1 + idx, row=1, padx=10, pady=10) + 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 + for x_texts in scripts.text: + for text in x_texts: + if text != "": + 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]]) + +def make(): + global root + global app + global root_destroyed + global redetect_before_start + root = tk.Tk() + root_destroyed = False + root.protocol("WM_DELETE_WINDOW", close) + root.resizable(False, False) + if MAIN_ICON != None: + if os.path.splitext(MAIN_ICON)[1].lower() == ".gif": + root.call('wm', 'iconphoto', root._w, tk.PhotoImage(file=MAIN_ICON)) + else: + root.iconbitmap(MAIN_ICON) + app = Main_Window(root) + app.raise_above_all() + app.after(100, app.connect_lp) + app.mainloop() + + +def close(): + global root_destroyed, launchpad + app.modified_layout_save_prompt() + app.disconnect_lp() + + if not root_destroyed: + root.destroy() + root_destroyed = True