Skip to content

Commit

Permalink
Merge pull request #150 from tanjeffreyz/dev
Browse files Browse the repository at this point in the history
Feature/Keybind Editor UI
  • Loading branch information
tanjeffreyz authored Dec 30, 2022
2 parents 996ff67 + d56c821 commit a569cdb
Show file tree
Hide file tree
Showing 16 changed files with 408 additions and 242 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ __pycache__/
assets/models/
.idea/
.settings/
*.iml
9 changes: 0 additions & 9 deletions Auto Maple.iml

This file was deleted.

2 changes: 1 addition & 1 deletion resources
125 changes: 125 additions & 0 deletions src/command_book/command_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import os
import inspect
import importlib
import traceback
from os.path import basename, splitext
from src.common import config, utils
from src.routine import components
from src.common.interfaces import Configurable


CB_KEYBINDING_DIR = os.path.join('resources', 'keybindings')


class CommandBook(Configurable):
def __init__(self, file):
self.name = splitext(basename(file))[0]
self.buff = components.Buff()
self.DEFAULT_CONFIG = {}
result = self.load_commands(file)
if result is None:
raise ValueError(f"Invalid command book at '{file}'")
self.dict, self.module = result
super().__init__(self.name, directory=CB_KEYBINDING_DIR)

def load_commands(self, file):
"""Prompts the user to select a command module to import. Updates config's command book."""

utils.print_separator()
print(f"[~] Loading command book '{basename(file)}':")

ext = splitext(file)[1]
if ext != '.py':
print(f" ! '{ext}' is not a supported file extension.")
return

new_step = components.step
new_cb = {}
for c in (components.Wait, components.Walk, components.Fall):
new_cb[c.__name__.lower()] = c

# Import the desired command book file
target = '.'.join(['resources', 'command_books', self.name])
try:
module = importlib.import_module(target)
module = importlib.reload(module)
except ImportError: # Display errors in the target Command Book
print(' ! Errors during compilation:\n')
for line in traceback.format_exc().split('\n'):
line = line.rstrip()
if line:
print(' ' * 4 + line)
print(f"\n ! Command book '{self.name}' was not loaded")
return

# Load key map
if hasattr(module, 'Key'):
default_config = {}
for key, value in module.Key.__dict__.items():
if not key.startswith('__') and not key.endswith('__'):
default_config[key] = value
self.DEFAULT_CONFIG = default_config
else:
print(f" ! Error loading command book '{self.name}', keymap class 'Key' is missing")
return

# Check if the 'step' function has been implemented
step_found = False
for name, func in inspect.getmembers(module, inspect.isfunction):
if name.lower() == 'step':
step_found = True
new_step = func

# Populate the new command book
for name, command in inspect.getmembers(module, inspect.isclass):
if issubclass(command, components.Command):
new_cb[name.lower()] = command

# Check if required commands have been implemented and overridden
required_found = True
for command in (components.Buff,):
name = command.__name__.lower()
if name not in new_cb:
required_found = False
new_cb[name] = command
print(f" ! Error: Must implement required command '{name}'.")

# Look for overridden movement commands
movement_found = True
for command in (components.Move, components.Adjust):
name = command.__name__.lower()
if name not in new_cb:
movement_found = False
new_cb[name] = command

if not step_found and not movement_found:
print(f" ! Error: Must either implement both 'Move' and 'Adjust' commands, "
f"or the function 'step'")
if required_found and (step_found or movement_found):
self.buff = new_cb['buff']()
components.step = new_step
config.gui.menu.file.enable_routine_state()
config.gui.view.status.set_cb(basename(file))
config.routine.clear()
print(f" ~ Successfully loaded command book '{self.name}'")
return new_cb, module
else:
print(f" ! Command book '{self.name}' was not loaded")

def __getitem__(self, item):
return self.dict[item]

def __contains__(self, item):
return item in self.dict

def load_config(self):
super().load_config()
self._set_keybinds()

def save_config(self):
self._set_keybinds()
super().save_config()

def _set_keybinds(self):
for k, v in self.config.items():
setattr(self.module.Key, k, v)
Binary file added src/command_book/resources/keybindings/kanna
Binary file not shown.
18 changes: 10 additions & 8 deletions src/common/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import os
import pickle

SETTINGS_DIR = '.settings'


class Configurable:
TARGET = 'default_configurable'
DEFAULT_CONFIG = {
DIRECTORY = '.settings'
DEFAULT_CONFIG = { # Must be overridden by subclass
'Default configuration': 'None'
}

def __init__(self, target):
def __init__(self, target, directory='.settings'):
assert self.DEFAULT_CONFIG != Configurable.DEFAULT_CONFIG, 'Must override Configurable.DEFAULT_CONFIG'
self.TARGET = target
self.config = self.DEFAULT_CONFIG.copy()
self.DIRECTORY = directory
self.config = self.DEFAULT_CONFIG.copy() # Shallow copy, should only contain primitives
self.load_config()

def load_config(self):
path = os.path.join(SETTINGS_DIR, self.TARGET)
path = os.path.join(self.DIRECTORY, self.TARGET)
if os.path.isfile(path):
with open(path, 'rb') as file:
self.config = pickle.load(file)
loaded = pickle.load(file)
self.config = {key: loaded.get(key, '') for key in self.DEFAULT_CONFIG}
else:
self.save_config()

def save_config(self):
path = os.path.join(SETTINGS_DIR, self.TARGET)
path = os.path.join(self.DIRECTORY, self.TARGET)
directory = os.path.dirname(path)
if not os.path.exists(directory):
os.makedirs(directory)
Expand Down
2 changes: 2 additions & 0 deletions src/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def multi_match(frame, template, threshold=0.95):
:return: An array of matches that exceed THRESHOLD.
"""

if template.shape[0] > frame.shape[0] or template.shape[1] > frame.shape[1]:
return []
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
result = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED)
locations = np.where(result >= threshold)
Expand Down
162 changes: 162 additions & 0 deletions src/gui/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Interfaces that are used by various GUI pages."""

import tkinter as tk
import keyboard as kb
from tkinter import ttk
from src.common import utils
from src.common.interfaces import Configurable


class Frame(tk.Frame):
Expand All @@ -28,3 +31,162 @@ class MenuBarItem(tk.Menu):
def __init__(self, parent, label, **kwargs):
super().__init__(parent, **kwargs)
parent.add_cascade(label=label, menu=self)


class KeyBindings(LabelFrame):
def __init__(self, parent, label, target, **kwargs):
super().__init__(parent, label, **kwargs)
if target is not None:
assert isinstance(target, Configurable)
self.target = target
self.long = False

self.displays = {} # Holds each action's display variable
self.forward = {} # Maps actions to keys
self.backward = {} # Maps keys to actions
self.prev_a = ''
self.prev_k = ''

self.contents = None
self.container = None
self.canvas = None
self.scrollbar = None
self.reset = None
self.save = None
self.create_edit_ui()

def create_edit_ui(self):
self.displays = {}
self.forward = {}
self.backward = {}
self.prev_a = ''
self.prev_k = ''

if self.target is None:
self.contents = Frame(self)
self.create_disabled_entry()
self.contents.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
return

if len(self.target.config) > 27:
self.long = True
self.container = Frame(self, width=354, height=650)
self.container.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=(5, 0))
self.container.pack_propagate(False)
self.canvas = tk.Canvas(self.container, bd=0, highlightthickness=0)
self.scrollbar = tk.Scrollbar(self.container, command=self.canvas.yview)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.contents = Frame(self.canvas)
self.contents.bind(
'<Configure>',
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
self.canvas.create_window((0, 0), window=self.contents, anchor=tk.NW)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
else:
self.contents = Frame(self)
self.contents.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)

for action, key in self.target.config.items():
self.forward[action] = key
self.backward[key] = action
self.create_entry(action, key)
self.focus()

self.reset = tk.Button(self, text='Reset', command=self.refresh_edit_ui, takefocus=False)
self.reset.pack(side=tk.LEFT, padx=5, pady=5)
self.save = tk.Button(self, text='Save', command=self.save_keybindings, takefocus=False)
self.save.pack(side=tk.RIGHT, padx=5, pady=5)

def refresh_edit_ui(self):
self.destroy_contents()
self.create_edit_ui()

def destroy_contents(self):
self.contents.destroy()
self.reset.destroy()
self.save.destroy()
if self.long:
self.container.destroy()
self.canvas.destroy()
self.scrollbar.destroy()

@utils.run_if_disabled('\n[!] Cannot save key bindings while Auto Maple is enabled')
def save_keybindings(self):
utils.print_separator()
print(f"[~] Saving key bindings to '{self.target.TARGET}':")

failures = 0
for action, key in self.forward.items():
if key != '':
self.target.config[action] = key
else:
print(f" ! Action '{action}' was not bound to a key")
failures += 1

self.target.save_config()
if failures == 0:
print(' ~ Successfully saved all key bindings')
else:
print(f' ~ Successfully saved all except for {failures} key bindings')
self.refresh_edit_ui()

def create_entry(self, action, key):
"""
Creates an input row for a single key bind. ACTION is assigned to KEY.
"""

display_var = tk.StringVar(value=key)
self.displays[action] = display_var

row = Frame(self.contents, highlightthickness=0)
row.pack(expand=True, fill='x')

label = tk.Entry(row)
label.grid(row=0, column=0, sticky=tk.EW)
label.insert(0, action)
label.config(state=tk.DISABLED)

def on_key_press(_):
k = kb.read_key()
if action != self.prev_a:
self.prev_k = ''
self.prev_a = action
if k != self.prev_k:
prev_key = self.forward[action]
self.backward.pop(prev_key, None)
if k in self.backward:
prev_action = self.backward[k]
self.forward[prev_action] = ''
self.displays[prev_action].set('')
display_var.set(k)
self.forward[action] = k
self.backward[k] = action
self.prev_k = k

def validate(d):
"""Blocks user insertion, but allows StringVar set()."""

if d == '-1':
return True
return False

reg = (self.register(validate), '%d')
entry = tk.Entry(row, textvariable=display_var,
validate='key', validatecommand=reg,
takefocus=False)
entry.bind('<KeyPress>', on_key_press)
entry.grid(row=0, column=1, sticky=tk.EW)

def create_disabled_entry(self):
row = Frame(self.contents, highlightthickness=0)
row.pack(expand=True, fill='x')

label = tk.Entry(row)
label.grid(row=0, column=0, sticky=tk.EW)
label.config(state=tk.DISABLED)

entry = tk.Entry(row)
entry.grid(row=0, column=1, sticky=tk.EW)
entry.config(state=tk.DISABLED)
2 changes: 1 addition & 1 deletion src/gui/menu/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def _load_commands():


def get_routines_dir():
target = os.path.join(config.RESOURCES_DIR, 'routines', config.bot.module_name)
target = os.path.join(config.RESOURCES_DIR, 'routines', config.bot.command_book.name)
if not os.path.exists(target):
os.makedirs(target)
return target
Loading

0 comments on commit a569cdb

Please sign in to comment.