PyMacroRecord/software.py

409 lines
15 KiB
Python

from json import load, dumps
from tkinter import *
from tkinter.ttk import *
from tkinter import filedialog
from tkinter import messagebox
from pynput import keyboard
from subprocess import Popen
from os import path, mkdir, getenv, remove
from webbrowser import open as OpenUrl
from atexit import register
from time import sleep
playbackStatement = False # Know if playback is active
recordStatement = False # Know if record is active
recordSet = False # Know if user set recorded so he can save it
fileAlreadySaved = False # Know if user already save is macro once so not neet to save as
closeWindow = False # Know if user is about to close the window
keyboardControl = keyboard.Controller() # Keyboard controller to detect keypress
appdata_local = getenv('LOCALAPPDATA') + "/MacroRecorder"
appdata_local = appdata_local.replace('\\', "/")
if path.isdir(appdata_local) == False:
mkdir(appdata_local) # Temp record to interact with macro.py
userSettings = {
"Playback": {
"Speed": 1,
"Repeat": {
"Mode": "OneTime",
"Times": 1,
"For": 0,
"Interval": 0
}
},
"Recordings": {
"Mouse_Move": True,
"Mouse_Click": True,
"Keyboard": True,
},
"Hotkeys": {
"Record_Start": "o",
"Record_Stop": str(keyboard.Key.esc),
"Playback_Start": "p",
"Playback_Stop": str(keyboard.Key.esc),
},
"Minimization": {
"When_Playing": False,
"When_Recording": False,
},
"Run_On_StartUp": False,
"After_Recording": "Idle" #Quit, Lock Computer, Lof off computer, Turn off computer, Standby, Hibernate
}
userSettings_json = dumps(userSettings, indent=4)
open(path.join(appdata_local + "/userSettings.json"), "w").write(userSettings_json)
userSettings = load(open(path.join(appdata_local+"/userSettings.json")))
def changeSettings(category, option=None, newValue=None):
"""Change settings of user"""
if newValue is None:
if option is None:
if userSettings[category]:
userSettings[category] = False
else:
userSettings[category] = True
else:
if userSettings[category][option]:
userSettings[category][option] = False
else:
userSettings[category][option] = True
if option is not None and newValue is not None:
if option == None:
userSettings[category] = newValue
else:
userSettings[category][option] = newValue
userSettings_json = dumps(userSettings, indent=4)
open(path.join(appdata_local + "/userSettings.json"), "w").write(userSettings_json)
def on_release(key):
"""
Detect key release to change buttons in the gui
"""
global recordStatement, playbackStatement, recordBtn, playBtn, recordSet
try:
if key.char == 'o':
if (recordStatement == False and playbackStatement == False):
startRecordingAndChangeImg(False)
if key.char == 'p':
if (recordStatement == False and playbackStatement == False and recordSet == True):
replay(False)
recordBtn.configure(state=DISABLED)
except AttributeError:
if key == keyboard.Key.esc:
if (recordStatement == True and playbackStatement == False):
stopRecordingAndChangeImg(False)
if (recordStatement == False and playbackStatement == True):
playbackStatement = False
sleep(0.1)
recordBtn.configure(state=NORMAL)
playBtn.configure(image=playImg)
file_menu.entryconfig('New', state=NORMAL)
file_menu.entryconfig('Load', state=NORMAL)
file_menu.entryconfig('Save', state=NORMAL)
file_menu.entryconfig('Save as', state=NORMAL)
def startRecordingAndChangeImg(pressKey=True):
"""
When this function is called, it presses for the user his keybind to start the recording if he pressed the button
instead of his keyboard, it changes the button image
"""
global stopBtn, recordStatement, playbackStatement
playBtn.configure(state=DISABLED)
if playbackStatement == False:
recordStatement = True
file_menu.entryconfig('Load', state=DISABLED)
if pressKey:
keyboardControl.press('o')
keyboardControl.release('o')
recordBtn.configure(image=stopImg, command=stopRecordingAndChangeImg)
file_menu.entryconfig('Save', state=DISABLED)
file_menu.entryconfig('Save as', state=DISABLED)
file_menu.entryconfig('New', state=DISABLED)
file_menu.entryconfig('Load', state=DISABLED)
def stopRecordingAndChangeImg(pressKey=True):
"""
When this function is called, it presses for the user his keybind to stop the recording if he pressed the button
instead of his keyboard, it changes the button image
"""
global recordBtn, recordStatement, recordSet
recordStatement = False
recordSet = True
if pressKey:
keyboardControl.press(keyboard.Key.esc)
keyboardControl.release(keyboard.Key.esc)
recordBtn.configure(image=recordImg, command=startRecordingAndChangeImg)
playBtn.configure(state=NORMAL)
file_menu.entryconfig('Save', state=NORMAL, command=saveMacro)
file_menu.entryconfig('Save as', state=NORMAL, command=saveMacroAs)
file_menu.entryconfig('New', state=NORMAL, command=newMacro)
file_menu.entryconfig('Load', state=NORMAL)
def replay(pressKey=True):
"""
Replay the last recorded macro or the loaded one
"""
global playbackStatement, recordBtn, recordSet
playBtn.configure(image=stopImg)
print('playback')
playbackStatement = True
file_menu.entryconfig('Save', state=DISABLED)
file_menu.entryconfig('Save as', state=DISABLED)
file_menu.entryconfig('New', state=DISABLED)
file_menu.entryconfig('Load', state=DISABLED)
if pressKey:
keyboardControl.press('p')
keyboardControl.release('p')
recordBtn.configure(state=DISABLED)
def saveMacroAs(e=None):
"""
Save the macro as a file name, I use temprecord.json to retrieve the last record and then save it on his file
"""
global macroPath, fileAlreadySaved
if recordStatement == False and playbackStatement == False and recordSet == True:
macroSaved = filedialog.asksaveasfile(filetypes=[('Json Files', '*.json')], defaultextension='.json')
if macroSaved is not None:
macroContent = open(path.join(appdata_local + "/temprecord.json"), "r")
macroEvents = load(macroContent)
json_macroEvents = dumps(macroEvents, indent=4)
open(macroSaved.name, "w").write(json_macroEvents)
macroPath = macroSaved.name
# macroPath serve to know where his last saved macro is, so the user can
# overwrite it if he did a new record
macroSaved.close()
fileAlreadySaved = True
def saveMacro(e=None):
"""
Save the last macro saved or loaded, I use temprecord.json to retrieve the last record and then save it on his file
"""
if recordStatement == False and playbackStatement == False and recordSet == True:
if fileAlreadySaved == True:
macroContent = open(path.join(appdata_local + "/temprecord.json"), "r")
macroSaved = open(path.join(macroPath), "w")
macroEvents = load(macroContent)
json_macroEvents = dumps(macroEvents, indent=4)
macroSaved.write(json_macroEvents)
print('saved')
else:
saveMacroAs()
if closeWindow:
window.destroy()
def loadMacro(e=None):
"""
Load a script that the user did, to play it or overwrite it (if he saved his new record of course)
"""
global macroPath, recordSet, fileAlreadySaved
if recordStatement == False and playbackStatement == False:
if recordSet == True:
wantToSave = warningPopUpSave()
if wantToSave == None:
return
elif wantToSave:
saveMacro()
macroFile = filedialog.askopenfile(filetypes=[('Json Files', '*.json')], defaultextension='.json')
if macroFile is not None:
macroContent = open(macroFile.name)
macroPath = macroFile.name
macroEvents = load(macroContent)
json_macroEvents = dumps(macroEvents, indent=4)
open(path.join(appdata_local + "/temprecord.json"), "w").write(json_macroEvents)
playBtn.configure(state=NORMAL)
file_menu.entryconfig('Save', state=NORMAL, command=saveMacro)
file_menu.entryconfig('Save as', state=NORMAL, command=saveMacroAs)
file_menu.entryconfig('New', state=NORMAL, command=newMacro)
macroFile.close()
print("loaded")
fileAlreadySaved = True
def newMacro(e=None):
"""
Load a script that the user did, to play it or overwrite it (if he saved his new record of course)
"""
global recordSet, fileAlreadySaved
if recordStatement == False and playbackStatement == False and recordSet == True:
wantToSave = warningPopUpSave()
if wantToSave == None:
return
elif wantToSave:
saveMacro()
recordBtn.configure(image=recordImg, command=startRecordingAndChangeImg)
file_menu.entryconfig('Save', state=DISABLED)
file_menu.entryconfig('Save as', state=DISABLED)
file_menu.entryconfig('New', state=DISABLED)
playBtn.configure(state=DISABLED)
recordSet = False
fileAlreadySaved = False
def warningPopUpSave():
return messagebox.askyesnocancel("Info", "Do you want to save your record?")
def stopProgram():
global closeWindow, macro_process
"""
When the users want to stop the software, the macro.py in the background is terminated
"""
if recordSet == True:
closeWindow = True
wantToSave = warningPopUpSave()
if wantToSave:
saveMacro()
remove_temprecord()
elif wantToSave == False:
remove_temprecord()
macro_process.terminate()
window.destroy()
else:
remove_temprecord()
macro_process.terminate()
window.destroy()
def remove_temprecord():
try:
remove(path.join(appdata_local + "/temprecord.json"))
except FileNotFoundError:
pass
def changeSpeed():
global speedWin, horizontal_line, speedTestVal
speedWin = Toplevel(window)
speedWin.geometry("300x150")
speedWin.title("Change Speed")
speedWin.grab_set()
speedWin.resizable(False, False)
speedWin.attributes("-toolwindow", 1)
speedTestVal = Label(speedWin, text="Speed 0")
speedTestVal.pack()
speedTestVal.pack(side=TOP)
horizontal_line = Scale(speedWin, from_=0.1, to=10, command=getSpeedNumber)
horizontal_line.set('1')
horizontal_line.pack(side=BOTTOM, pady=30)
def getSpeedNumber(val=0):
val = round(float(val))
speedTestVal.config(text = f"Speed {val}")
def cleanup():
"""
When the users want to stop the software, the macro.py in the background is terminated
"""
if 'macro_process' in globals():
macro_process.terminate()
register(cleanup)
macro_process = Popen(['pythonw',
'macro.py']) # it serves to run macro.py in the background because thread make the recording slower for some reasons
# Window Setup
window = Tk()
window.title("MacroRecorder")
window.geometry("350x200")
window.iconbitmap("assets/logo.ico")
window.resizable(False, False)
# Menu Setup
my_menu = Menu(window)
window.config(menu=my_menu)
# File Section
file_menu = Menu(my_menu, tearoff=0)
my_menu.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="New", state=DISABLED, accelerator="Ctrl+N")
file_menu.add_command(label="Load", command=loadMacro, accelerator="Ctrl+L")
file_menu.add_command(label="Save", state=DISABLED, accelerator="Ctrl+S")
file_menu.add_command(label="Save as", state=DISABLED, accelerator="Ctrl+Shift+S")
file_menu.add_separator()
file_menu.add_command(label="Exit", command=window.quit)
# Options Section
options_menu = Menu(my_menu, tearoff=0)
my_menu.add_cascade(label="Options", menu=options_menu)
# Playback Sub
playback_sub = Menu(options_menu, tearoff=0)
options_menu.add_cascade(label="Playback", menu=playback_sub)
playback_sub.add_command(label="Speed", command=changeSpeed)
playback_sub.add_command(label="Repeat")
# Recordings Sub
mouseMove = BooleanVar()
mouseClick = BooleanVar()
keyboardInput = BooleanVar()
mouseMove.set(userSettings["Recordings"]["Mouse_Move"])
mouseClick.set(userSettings["Recordings"]["Mouse_Click"])
keyboardInput.set(userSettings["Recordings"]["Keyboard"])
recordings_sub = Menu(options_menu, tearoff=0)
options_menu.add_cascade(label="Recordings", menu=recordings_sub)
recordings_sub.add_checkbutton(label="Mouse movement", variable=mouseMove, command=lambda: changeSettings("Recordings", "Mouse_Move"))
recordings_sub.add_checkbutton(label="Mouse click", variable=mouseClick, command=lambda: changeSettings("Recordings", "Mouse_Click"))
recordings_sub.add_checkbutton(label="Keyboard", variable=keyboardInput, command=lambda: changeSettings("Recordings", "Keyboard"))
# Options Sub
options_sub = Menu(options_menu, tearoff=0)
options_menu.add_cascade(label="Options", menu=options_sub)
options_sub.add_command(label="Hotkeys")
minimization_sub = Menu(options_sub, tearoff=0)
options_sub.add_cascade(label="Minimization", menu=minimization_sub)
minimization_playing = BooleanVar().set(userSettings["Minimization"]["When_Playing"])
minimization_record = BooleanVar().set(userSettings["Minimization"]["When_Recording"])
minimization_sub.add_checkbutton(label="Minimized when playing", variable=minimization_playing, command=lambda: changeSettings("Minimization", "When_Playing"))
minimization_sub.add_checkbutton(label="Minimized when recording", variable=minimization_record, command=lambda: changeSettings("Minimization", "When_Recording"))
runStartUp = BooleanVar().set(userSettings["Run_On_StartUp"])
options_sub.add_checkbutton(label="Run on startup", variable=runStartUp, command=lambda: changeSettings("Run_On_StartUp"))
options_sub.add_command(label="After recording")
help_section = Menu(my_menu, tearoff=0)
my_menu.add_cascade(label="Help", menu=help_section)
help_section.add_command(label="Github Page", command=lambda: OpenUrl("https://github.com/LOUDO56/macro-recorder/tree/main#readme"))
help_section.add_command(label="About")
# Play Button
playImg = PhotoImage(file=r"assets/button/play.png")
playBtn = Button(window, image=playImg, command=replay, state=DISABLED)
playBtn.pack(side=LEFT, padx=50)
# Record Button
recordImg = PhotoImage(file=r"assets/button/record.png")
recordBtn = Button(window, image=recordImg, command=startRecordingAndChangeImg)
recordBtn.pack(side=RIGHT, padx=50)
# Stop Button
stopImg = PhotoImage(file=r"assets/button/stop.png")
window.bind('<Control-Shift-S>', saveMacroAs)
window.bind('<Control-s>', saveMacro)
window.bind('<Control-l>', loadMacro)
window.bind('<Control-n>', newMacro)
keyboardListener = keyboard.Listener(on_release=on_release)
keyboardListener.start()
window.protocol("WM_DELETE_WINDOW", stopProgram)
window.mainloop()