Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import tkinter as tk
- from tkinter import ttk, messagebox, simpledialog
- import ctypes
- import ctypes.wintypes as wintypes
- import time
- import vgamepad as vg
- import threading
- import os
- import re
- import ast
- try:
- import wmi
- WMI_AVAILABLE = True
- except ImportError:
- WMI_AVAILABLE = False
- # --- Default Configuration ---
- DEFAULT_SENSITIVITY = 50.0
- BASE_INPUT_RANGE = 50.0 # Mouse delta for 100% joystick output at 1.00x sensitivity
- DEFAULT_RESET_KEY = ';'
- DECAY_TIMEOUT = 0.05
- DEFAULT_CURVE_POINTS = [[0.0, 0.0], [0.25, 0.25], [0.5, 0.5], [0.75, 0.75], [1.0, 1.0]]
- # --- WinAPI Structures (Defined before use) ---
- class POINT(ctypes.Structure):
- _fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
- class MSLLHOOKSTRUCT(ctypes.Structure):
- _fields_ = [("pt", POINT), ("mouseData", wintypes.DWORD), ("flags", wintypes.DWORD),
- ("time", wintypes.DWORD), ("dwExtraInfo", ctypes.POINTER(wintypes.ULONG))]
- class RAWINPUTDEVICE(ctypes.Structure):
- _fields_ = [("usUsagePage", wintypes.USHORT), ("usUsage", wintypes.USHORT),
- ("dwFlags", wintypes.DWORD), ("hwndTarget", wintypes.HWND)]
- class RAWMOUSE(ctypes.Structure):
- _fields_ = [("usFlags", wintypes.USHORT), ("ulButtons", wintypes.ULONG),
- ("usButtonFlags", wintypes.USHORT), ("usButtonData", wintypes.USHORT),
- ("ulRawButtons", wintypes.ULONG), ("lLastX", wintypes.LONG),
- ("lLastY", wintypes.LONG), ("ulExtraInformation", wintypes.ULONG)]
- class RAWINPUTHEADER(ctypes.Structure):
- _fields_ = [("dwType", wintypes.DWORD), ("dwSize", wintypes.DWORD),
- ("hDevice", wintypes.HANDLE), ("wParam", wintypes.WPARAM)]
- class RAWINPUT(ctypes.Structure):
- _fields_ = [("header", RAWINPUTHEADER), ("mouse", RAWMOUSE)]
- class RAWINPUTDEVICELIST(ctypes.Structure):
- _fields_ = [('hDevice', wintypes.HANDLE), ('dwType', wintypes.DWORD)]
- WNDPROC = ctypes.WINFUNCTYPE(wintypes.LPARAM, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
- class WNDCLASSW(ctypes.Structure):
- _fields_ = [("style", wintypes.UINT), ("lpfnWndProc", WNDPROC), ("cbClsExtra", ctypes.c_int),
- ("cbWndExtra", ctypes.c_int), ("hInstance", wintypes.HINSTANCE),
- ("hIcon", wintypes.HANDLE),
- ("hCursor", wintypes.HANDLE),
- ("hbrBackground", wintypes.HANDLE),
- ("lpszMenuName", wintypes.LPCWSTR),
- ("lpszClassName", wintypes.LPCWSTR)]
- class MSG(ctypes.Structure):
- _fields_ = [("hwnd", wintypes.HWND), ("message", wintypes.UINT), ("wParam", wintypes.WPARAM),
- ("lParam", wintypes.LPARAM), ("time", wintypes.DWORD), ("pt", POINT)]
- # --- WinAPI Function Prototypes ---
- WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
- HOOKPROC = ctypes.WINFUNCTYPE(wintypes.LPARAM, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM)
- user32 = ctypes.WinDLL('user32', use_last_error=True)
- kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
- user32.GetAsyncKeyState.restype = wintypes.SHORT
- user32.GetAsyncKeyState.argtypes = [ctypes.c_int]
- user32.EnumWindows.argtypes = [WNDENUMPROC, wintypes.LPARAM]
- user32.IsWindowVisible.argtypes = [wintypes.HWND]
- user32.GetWindowTextLengthW.argtypes = [wintypes.HWND]
- user32.GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
- user32.GetWindowThreadProcessId.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)]
- user32.GetForegroundWindow.restype = wintypes.HWND
- user32.GetForegroundWindow.argtypes = []
- user32.DefWindowProcW.restype = wintypes.LPARAM
- user32.DefWindowProcW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
- user32.GetRawInputDeviceInfoW.restype = wintypes.UINT
- user32.GetRawInputDeviceInfoW.argtypes = [wintypes.HANDLE, wintypes.UINT, wintypes.LPVOID, ctypes.POINTER(wintypes.UINT)]
- user32.SetWindowsHookExW.restype = wintypes.HHOOK
- user32.SetWindowsHookExW.argtypes = [ctypes.c_int, HOOKPROC, wintypes.HINSTANCE, wintypes.DWORD]
- user32.CallNextHookEx.restype = wintypes.LPARAM
- user32.CallNextHookEx.argtypes = [wintypes.HHOOK, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM]
- user32.UnhookWindowsHookEx.argtypes = [wintypes.HHOOK]
- user32.WindowFromPoint.argtypes = [POINT]
- user32.GetRawInputDeviceList.argtypes = [ctypes.POINTER(RAWINPUTDEVICELIST), ctypes.POINTER(wintypes.UINT), wintypes.UINT]
- user32.RegisterRawInputDevices.argtypes = [ctypes.POINTER(RAWINPUTDEVICE), wintypes.UINT, wintypes.UINT]
- user32.GetRawInputData.argtypes = [wintypes.LPARAM, wintypes.UINT, wintypes.LPVOID, ctypes.POINTER(wintypes.UINT), wintypes.UINT]
- user32.RegisterClassW.argtypes = [ctypes.POINTER(WNDCLASSW)]
- user32.CreateWindowExW.restype = wintypes.HWND
- user32.CreateWindowExW.argtypes = [wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID]
- user32.DestroyWindow.argtypes = [wintypes.HWND]
- user32.UnregisterClassW.argtypes = [wintypes.LPCWSTR, wintypes.HINSTANCE]
- user32.PeekMessageW.argtypes = [ctypes.POINTER(MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT, wintypes.UINT]
- user32.TranslateMessage.argtypes = [ctypes.POINTER(MSG)]
- user32.DispatchMessageW.argtypes = [ctypes.POINTER(MSG)]
- # --- Helper Functions ---
- def list_windows():
- windows = []
- def enum_windows_proc(hwnd, lparam):
- if user32.IsWindowVisible(hwnd) and user32.GetWindowTextLengthW(hwnd) > 0:
- buffer = ctypes.create_unicode_buffer(user32.GetWindowTextLengthW(hwnd) + 1)
- user32.GetWindowTextW(hwnd, buffer, len(buffer))
- windows.append({'title': buffer.value, 'hwnd': hwnd})
- return True
- user32.EnumWindows(WNDENUMPROC(enum_windows_proc), 0)
- return windows
- def check_and_install_vgamepad():
- #"""
- #Checks if vgamepad is installed. If not, prompts the user to install it.
- #Returns True if the application can proceed, False otherwise.
- #"""
- try:
- import vgamepad
- return True
- except ImportError:
- import tkinter as tk
- from tkinter import messagebox
- import subprocess
- import sys
- import threading
- # Create a hidden root window for messageboxes
- root = tk.Tk()
- root.withdraw()
- # Ask the user for permission to install
- should_install = messagebox.askyesno(
- "Dependency Missing",
- "The required 'vgamepad' library is not installed.\n\n"
- "This library is essential for creating the virtual joystick. "
- "Would you like to install it now?"
- )
- if should_install:
- # Show a simple "Installing..." window
- progress_window = tk.Toplevel(root)
- progress_window.title("Installing...")
- progress_window.geometry("250x100")
- progress_window.resizable(False, False)
- ttk.Label(progress_window, text="Installing vgamepad, please wait...").pack(pady=20, padx=20)
- progress_window.grab_set()
- root.update_idletasks()
- install_success = [False] # Use a list to make it mutable for the thread
- def do_install():
- try:
- # Use sys.executable to ensure pip installs for the correct Python version
- command = [sys.executable, "-m", "pip", "install", "vgamepad", "--quiet"]
- subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- install_success[0] = True
- except (subprocess.CalledProcessError, FileNotFoundError):
- install_success[0] = False
- finally:
- # Safely destroy the window from the main thread
- root.after(0, progress_window.destroy)
- # Run installation in a separate thread to prevent the UI from freezing
- install_thread = threading.Thread(target=do_install)
- install_thread.start()
- # Keep the progress window running
- root.wait_window(progress_window)
- if install_success[0]:
- messagebox.showinfo("Success", "'vgamepad' has been installed. The application will now start.")
- root.destroy()
- return True
- else:
- messagebox.showerror(
- "Installation Failed",
- "Failed to automatically install 'vgamepad'.\n\n"
- "Please install it manually by opening a command prompt and running:\n"
- "pip install vgamepad\n"
- "Or\n"
- "py pip -m install vgamepad\n"
- "in Powershell on Windows."
- )
- root.destroy()
- return False
- else:
- messagebox.showwarning(
- "Installation Skipped",
- "You have chosen not to install 'vgamepad'.\n\n"
- "The application cannot function without it and will now close."
- )
- root.destroy()
- return False
- def list_raw_mice():
- mice = []
- wmi_mice = []
- if WMI_AVAILABLE:
- try:
- wmi_mice = wmi.WMI().Win32_PointingDevice()
- except Exception:
- pass
- num_devices = wintypes.UINT(0)
- user32.GetRawInputDeviceList(None, ctypes.byref(num_devices), ctypes.sizeof(RAWINPUTDEVICELIST))
- if num_devices.value == 0: return []
- device_list = (RAWINPUTDEVICELIST * num_devices.value)()
- user32.GetRawInputDeviceList(device_list, ctypes.byref(num_devices), ctypes.sizeof(RAWINPUTDEVICELIST))
- for i in range(num_devices.value):
- device = device_list[i]
- if device.dwType == 0: # RIM_TYPEMOUSE
- name_size = wintypes.UINT(0)
- user32.GetRawInputDeviceInfoW(device.hDevice, 0x20000007, None, ctypes.byref(name_size))
- if name_size.value <= 0: continue
- name_buffer = ctypes.create_unicode_buffer(name_size.value)
- user32.GetRawInputDeviceInfoW(device.hDevice, 0x20000007, name_buffer, ctypes.byref(name_size))
- raw_name = name_buffer.value
- display_name = raw_name
- match = re.search(r'VID_([0-9a-fA-F]{4})&PID_([0-9a-fA-F]{4})', raw_name, re.IGNORECASE)
- if match and wmi_mice:
- vid_pid_str = f"VID_{match.group(1)}&PID_{match.group(2)}".upper()
- for wmi_mouse in wmi_mice:
- if wmi_mouse.PNPDeviceID and vid_pid_str in wmi_mouse.PNPDeviceID.upper():
- display_name = wmi_mouse.Name
- break
- mice.append({'display_name': display_name, 'raw_name': raw_name, 'handle': device.hDevice})
- return mice
- class Mouse2JoystickApp:
- def __init__(self, root):
- self.root = root
- self.root.title("Mouse2Joystick Utility")
- self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
- if not WMI_AVAILABLE:
- messagebox.showinfo("Optional Dependency Missing",
- "The 'WMI' Python package was not found.\n\n"
- "Device names may be less descriptive. To fix this, run:\n"
- "pip install wmi\n"
- "or\n"
- "py pip -m install wmi\n"
- "in Powershell on Windows.")
- self.sensitivity = tk.DoubleVar(value=DEFAULT_SENSITIVITY)
- self.sensitivity_display = tk.StringVar()
- self.reset_key = tk.StringVar(value=DEFAULT_RESET_KEY)
- self.selected_profile = tk.StringVar()
- self.active_profile_display = tk.StringVar(value="N/A")
- self.reverse_output = tk.BooleanVar(value=False)
- self.joystick_x, self.joystick_y = 0.0, 0.0
- self.mouse_dx, self.mouse_dy = 0, 0
- self.last_mouse_event_time = 0.0
- self.selected_mouse = None
- self.selected_window = None
- self.is_running = False
- self.conversion_thread = None
- self.loaded_profile_name = None
- self.settings_changed = True
- self.hook_id = None
- self.target_pid = wintypes.DWORD(0)
- self.low_level_mouse_proc_ptr = HOOKPROC(self._low_level_mouse_proc)
- self.wnd_proc_ptr = WNDPROC(self._wnd_proc)
- self.curve_points = [p[:] for p in DEFAULT_CURVE_POINTS]
- self.dragged_point_index = None
- self.point_handles = []
- self.curve_margin = 20
- self.mice_data, self.windows_data = [], []
- self.selection_controls = []
- self.controller_name_var = tk.StringVar(value="Emulating: None")
- self.mouse_coords_var = tk.StringVar(value="Mouse Δ: X=0, Y=0")
- self.joystick_coords_var = tk.StringVar(value="Joystick: X=0.000, Y=0.000")
- ttk.Style().configure('Italic.TLabel', font=('Segoe UI', 9, 'italic'))
- self._create_menu()
- self.create_widgets()
- self.populate_mice_list()
- self.populate_windows_list()
- self.populate_profiles_list()
- self._update_sensitivity_label(self.sensitivity.get())
- self._update_profile_status_display()
- self.root.after(100, self._draw_curve)
- #DISCLAIMER ON STARTUP
- self.root.after(200, self._show_about_dialog)
- self.sensitivity.trace_add("write", self.on_setting_change)
- self.reset_key.trace_add("write", self.on_setting_change)
- self.reverse_output.trace_add("write", self.on_setting_change)
- def _create_menu(self):
- menubar = tk.Menu(self.root)
- self.root.config(menu=menubar)
- help_menu = tk.Menu(menubar, tearoff=0)
- menubar.add_cascade(label="Help", menu=help_menu)
- help_menu.add_command(label="About", command=self._show_about_dialog)
- def _show_about_dialog(self):
- about_window = tk.Toplevel(self.root)
- about_window.title("About & Disclaimer")
- self.root.update_idletasks()
- main_x = self.root.winfo_x()
- main_y = self.root.winfo_y()
- main_w = self.root.winfo_width()
- main_h = self.root.winfo_height()
- about_w, about_h = 450, 320
- pos_x = main_x + (main_w // 2) - (about_w // 2)
- pos_y = main_y + (main_h // 2) - (about_h // 2)
- about_window.geometry(f"{about_w}x{about_h}+{pos_x}+{pos_y}")
- about_window.resizable(False, False)
- about_window.transient(self.root)
- about_window.grab_set()
- about_window.protocol("WM_DELETE_WINDOW", lambda: None)
- main_frame = ttk.Frame(about_window, padding=20)
- main_frame.pack(expand=True, fill=tk.BOTH)
- about_text = ("This utility translates raw input from a selected mouse device into "
- "virtual Xbox 360 joystick output. "
- "V 1.0, programmed 99.9% with Gemini 2.5 Pro.")
- ttk.Label(main_frame, text=about_text, wraplength=400, justify=tk.LEFT).pack(pady=(0, 10))
- warning_text = ("WARNING: Pretty much every anti-cheat will think that this is cheats. Don't use this "
- "software for online games. This may result in account suspension or bans. "
- "By using this software, you acknowledge and assume any and all risks involved.")
- warning_label = ttk.Label(main_frame, text=warning_text, wraplength=400, justify=tk.LEFT, font=("Segoe UI", 9, "bold"), foreground="red")
- warning_label.pack(pady=10)
- disclaimer_text = ("This software was coded almost entirely by a large language model and is provided 'as is' "
- "without warranty of any kind. It is not copyrighted and is free to be modified and distributed.")
- ttk.Label(main_frame, text=disclaimer_text, wraplength=400, justify=tk.LEFT, style='Italic.TLabel').pack(pady=10)
- ack_button = tk.Button(main_frame, text="Acknowledge", command=about_window.destroy)
- ack_button.pack(pady=(15, 0))
- self.root.wait_window(about_window)
- def create_widgets(self):
- self.root.columnconfigure(0, weight=1)
- self.root.columnconfigure(1, weight=0)
- self.root.rowconfigure(0, weight=1)
- left_frame = ttk.Frame(self.root, padding="10")
- left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
- left_frame.rowconfigure(0, weight=1)
- left_frame.rowconfigure(1, weight=1)
- mouse_frame = ttk.LabelFrame(left_frame, text="1. Select Mouse Device", padding="10")
- mouse_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5, pady=5)
- mouse_frame.columnconfigure(0, weight=1)
- mouse_frame.rowconfigure(0, weight=1)
- listbox_frame_mouse = ttk.Frame(mouse_frame)
- listbox_frame_mouse.grid(row=0, column=0, columnspan=2, sticky="nsew")
- mouse_scrollbar = ttk.Scrollbar(listbox_frame_mouse, orient=tk.VERTICAL)
- self.mouse_listbox = tk.Listbox(listbox_frame_mouse, height=5, yscrollcommand=mouse_scrollbar.set)
- mouse_scrollbar.config(command=self.mouse_listbox.yview)
- mouse_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
- self.mouse_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- self.selection_controls.append(self.mouse_listbox)
- mouse_refresh_btn = ttk.Button(mouse_frame, text="Refresh", command=self.populate_mice_list)
- mouse_refresh_btn.grid(row=1, column=0, pady=(5,0), sticky=(tk.W, tk.E))
- self.selection_controls.append(mouse_refresh_btn)
- mouse_select_btn = ttk.Button(mouse_frame, text="Select", command=self.select_mouse)
- mouse_select_btn.grid(row=1, column=1, pady=(5,0), sticky=(tk.W, tk.E))
- self.selection_controls.append(mouse_select_btn)
- app_frame = ttk.LabelFrame(left_frame, text="2. Select Target Application", padding="10")
- app_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5, pady=5)
- app_frame.columnconfigure(0, weight=1)
- app_frame.rowconfigure(0, weight=1)
- listbox_frame_app = ttk.Frame(app_frame)
- listbox_frame_app.grid(row=0, column=0, columnspan=2, sticky="nsew")
- app_scrollbar = ttk.Scrollbar(listbox_frame_app, orient=tk.VERTICAL)
- self.app_listbox = tk.Listbox(listbox_frame_app, height=5, yscrollcommand=app_scrollbar.set)
- app_scrollbar.config(command=self.app_listbox.yview)
- app_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
- self.app_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- self.selection_controls.append(self.app_listbox)
- app_refresh_btn = ttk.Button(app_frame, text="Refresh", command=self.populate_windows_list)
- app_refresh_btn.grid(row=1, column=0, pady=(5,0), sticky=(tk.W, tk.E))
- self.selection_controls.append(app_refresh_btn)
- app_select_btn = ttk.Button(app_frame, text="Select", command=self.select_app)
- app_select_btn.grid(row=1, column=1, pady=(5,0), sticky=(tk.W, tk.E))
- self.selection_controls.append(app_select_btn)
- settings_frame = ttk.LabelFrame(left_frame, text="3. Settings", padding="10")
- settings_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), padx=5, pady=5)
- settings_frame.columnconfigure(1, weight=1)
- ttk.Label(settings_frame, text="Sensitivity:").grid(row=0, column=0, sticky=tk.W, padx=5)
- ttk.Scale(settings_frame, from_=0, to=100, orient=tk.HORIZONTAL, variable=self.sensitivity, command=self._update_sensitivity_label).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
- ttk.Label(settings_frame, textvariable=self.sensitivity_display, width=7).grid(row=0, column=2, sticky=tk.W, padx=5)
- ttk.Label(settings_frame, text="Reset Key:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
- ttk.Entry(settings_frame, textvariable=self.reset_key, width=5).grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
- ttk.Button(settings_frame, text="Reset Settings", command=self.reset_defaults).grid(row=1, column=2, sticky=tk.W, padx=5, pady=5)
- ttk.Checkbutton(settings_frame, text="Reverse Joystick Output", variable=self.reverse_output).grid(row=2, column=0, columnspan=3, sticky=tk.W, padx=5)
- curve_frame = ttk.LabelFrame(left_frame, text="4. Acceleration Curve", padding="10")
- curve_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), padx=5, pady=5)
- curve_frame.columnconfigure(0, weight=1)
- self.curve_canvas = tk.Canvas(curve_frame, width=220, height=220, bg='#FFFFFF', highlightthickness=1, highlightbackground="grey")
- self.curve_canvas.grid(row=0, column=0, sticky='nsew', pady=5)
- self.curve_canvas.bind("<Button-1>", self._on_canvas_press)
- self.curve_canvas.bind("<B1-Motion>", self._on_canvas_drag)
- self.curve_canvas.bind("<ButtonRelease-1>", self._on_canvas_release)
- reset_curve_btn = ttk.Button(curve_frame, text="Reset Curve", command=self.reset_curve)
- reset_curve_btn.grid(row=1, column=0, sticky='ew', pady=(5,0))
- control_frame = ttk.Frame(left_frame, padding=(5, 10))
- control_frame.grid(row=4, column=0, sticky=(tk.W, tk.E))
- control_frame.columnconfigure(0, weight=1); control_frame.columnconfigure(1, weight=1)
- self.start_button = ttk.Button(control_frame, text="Start Conversion", command=self.start_conversion)
- self.start_button.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0,2))
- self.selection_controls.append(self.start_button)
- self.stop_button = ttk.Button(control_frame, text="Stop Conversion", command=self.stop_conversion, state=tk.DISABLED)
- self.stop_button.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(2,0))
- self.status_label = ttk.Label(left_frame, text="Status: Idle. Please select a mouse and application.")
- self.status_label.grid(row=5, column=0, sticky=(tk.W, tk.E), padx=5, pady=(5,0))
- right_frame = ttk.Frame(self.root, padding=(0, 10, 10, 10))
- right_frame.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.E, tk.W))
- right_frame.rowconfigure(1, weight=1)
- right_frame.rowconfigure(2, weight=0)
- profile_frame = ttk.LabelFrame(right_frame, text="Profiles", padding="10")
- profile_frame.grid(row=0, column=0, sticky=(tk.N, tk.E, tk.W))
- profile_frame.columnconfigure(0, weight=1)
- profile_frame.columnconfigure(1, weight=1)
- self.profile_combobox = ttk.Combobox(profile_frame, textvariable=self.selected_profile, state="readonly")
- self.profile_combobox.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 5))
- profile_load_btn = ttk.Button(profile_frame, text="Load Profile", command=self.load_profile)
- profile_load_btn.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0,2), padx=(0, 1))
- profile_refresh_btn = ttk.Button(profile_frame, text="Refresh", command=self.populate_profiles_list)
- profile_refresh_btn.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0,2), padx=(1, 0))
- profile_save_btn = ttk.Button(profile_frame, text="Save New Profile", command=self.save_profile)
- profile_save_btn.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(2,0))
- self.profile_overwrite_btn = ttk.Button(profile_frame, text="Overwrite Active Profile", command=self.overwrite_profile, state=tk.DISABLED)
- self.profile_overwrite_btn.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(2,0))
- ttk.Separator(profile_frame, orient='horizontal').grid(row=4, column=0, columnspan=2, sticky='ew', pady=10)
- ttk.Label(profile_frame, text="Active Profile:").grid(row=5, column=0, columnspan=2, sticky=tk.W, pady=(0, 2))
- self.active_profile_label = ttk.Label(profile_frame, textvariable=self.active_profile_display, font=("Segoe UI", 9, "bold"))
- self.active_profile_label.grid(row=6, column=0, columnspan=2, sticky=tk.W)
- comments_frame = ttk.LabelFrame(right_frame, text="Comments", padding=(10, 5))
- comments_frame.grid(row=1, column=0, sticky="nsew", pady=(5,0))
- comments_frame.rowconfigure(0, weight=1)
- comments_frame.columnconfigure(0, weight=1)
- comments_scrollbar = ttk.Scrollbar(comments_frame, orient=tk.VERTICAL)
- self.comments_text = tk.Text(comments_frame, height=4, yscrollcommand=comments_scrollbar.set, wrap=tk.WORD)
- comments_scrollbar.config(command=self.comments_text.yview)
- comments_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
- self.comments_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- self.comments_text.bind("<KeyRelease>", self.on_setting_change)
- visualizer_frame = ttk.LabelFrame(right_frame, text="Output Visualizer", padding=(10, 5))
- visualizer_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0))
- visualizer_frame.columnconfigure(0, weight=0)
- visualizer_frame.columnconfigure(1, weight=1)
- self.visualizer_canvas = tk.Canvas(visualizer_frame, width=120, height=120, bg='#FFFFFF', highlightthickness=1, highlightbackground="grey")
- self.visualizer_canvas.grid(row=0, column=0, rowspan=3, sticky='nsew', padx=(0, 10))
- ttk.Label(visualizer_frame, textvariable=self.controller_name_var, font=("Segoe UI", 9, "bold")).grid(row=0, column=1, sticky='sw')
- ttk.Label(visualizer_frame, textvariable=self.mouse_coords_var).grid(row=1, column=1, sticky='w')
- ttk.Label(visualizer_frame, textvariable=self.joystick_coords_var).grid(row=2, column=1, sticky='nw')
- self.root.after(100, self._draw_default_visualizer_bg)
- def _draw_default_visualizer_bg(self):
- if not hasattr(self, 'visualizer_canvas') or self.visualizer_canvas.winfo_width() <= 1:
- self.root.after(100, self._draw_default_visualizer_bg)
- return
- self.visualizer_canvas.delete("all")
- w, h = self.visualizer_canvas.winfo_width(), self.visualizer_canvas.winfo_height()
- center_x, center_y = w / 2, h / 2
- radius = min(center_x, center_y) - 5
- self.visualizer_canvas.create_oval(center_x - radius, center_y - radius, center_x + radius, center_y + radius, outline='grey')
- self.visualizer_canvas.create_line(center_x, center_y - radius, center_x, center_y + radius, fill='lightgrey')
- self.visualizer_canvas.create_line(center_x - radius, center_y, center_x + radius, center_y, fill='lightgrey')
- def _update_visualizer(self):
- if self.is_running:
- self._draw_default_visualizer_bg()
- w, h = self.visualizer_canvas.winfo_width(), self.visualizer_canvas.winfo_height()
- center_x, center_y = w / 2, h / 2
- radius = min(center_x, center_y) - 5
- final_x = -self.joystick_y
- final_y = -self.joystick_x
- if self.reverse_output.get():
- final_x *= -1
- final_y *= -1
- indicator_x = center_x + final_x * radius
- indicator_y = center_y - final_y * radius
- r = 5
- self.visualizer_canvas.create_oval(indicator_x - r, indicator_y - r, indicator_x + r, indicator_y + r, fill="#d9534f", outline="black")
- self.mouse_coords_var.set(f"Mouse Δ: X={self.mouse_dx}, Y={self.mouse_dy}")
- self.joystick_coords_var.set(f"Joystick: X={final_x:.3f}, Y={final_y:.3f}")
- self.root.after(30, self._update_visualizer)
- else:
- self._draw_default_visualizer_bg()
- self.mouse_dx, self.mouse_dy = 0, 0
- self.mouse_coords_var.set("Mouse Δ: X=0, Y=0")
- self.joystick_coords_var.set("Joystick: X=0.000, Y=0.000")
- def on_setting_change(self, *args):
- self.settings_changed = True
- self._update_profile_status_display()
- def _update_profile_status_display(self):
- if self.loaded_profile_name:
- display_name = os.path.basename(self.loaded_profile_name).replace('.txt', '')
- self.profile_overwrite_btn.config(state=tk.NORMAL)
- if self.settings_changed:
- self.active_profile_display.set(f"{display_name} (modified)")
- self.active_profile_label.config(foreground="#f0ad4e")
- else:
- self.active_profile_display.set(display_name)
- self.active_profile_label.config(foreground="#0275d8")
- else:
- self.active_profile_display.set("N/A")
- self.active_profile_label.config(foreground="#d9534f")
- if hasattr(self, 'profile_overwrite_btn'):
- self.profile_overwrite_btn.config(state=tk.DISABLED)
- def _update_sensitivity_label(self, value):
- slider_val_norm = float(value) / 100.0
- # Exponential scaling: 10 * x^3.3219. Midpoint (50) gives 1.0x, Max (100) gives 10.0x
- multiplier = 10.0 * (slider_val_norm ** 3.3219)
- self.sensitivity_display.set(f"{multiplier:.2f}x")
- def _highlight_listbox_selection(self, listbox, index_to_highlight):
- default_bg_color = listbox.cget('bg')
- highlight_color = '#cce5ff'
- for i in range(listbox.size()):
- listbox.itemconfig(i, bg=default_bg_color)
- if index_to_highlight is not None and index_to_highlight < listbox.size():
- listbox.itemconfig(index_to_highlight, bg=highlight_color)
- def reset_defaults(self):
- self.sensitivity.set(DEFAULT_SENSITIVITY)
- self.reset_key.set(DEFAULT_RESET_KEY)
- self.reverse_output.set(False)
- self._update_sensitivity_label(DEFAULT_SENSITIVITY)
- self.comments_text.delete("1.0", tk.END)
- self.reset_curve()
- def reset_curve(self):
- self.curve_points = [p[:] for p in DEFAULT_CURVE_POINTS]
- self._draw_curve()
- self.on_setting_change()
- def _coords_to_canvas(self, x, y):
- w, h = self.curve_canvas.winfo_width(), self.curve_canvas.winfo_height()
- plot_w, plot_h = w - 2 * self.curve_margin, h - 2 * self.curve_margin
- return self.curve_margin + x * plot_w, self.curve_margin + (1 - y) * plot_h
- def _canvas_to_coords(self, cx, cy):
- w, h = self.curve_canvas.winfo_width(), self.curve_canvas.winfo_height()
- plot_w, plot_h = w - 2 * self.curve_margin, h - 2 * self.curve_margin
- x = (cx - self.curve_margin) / plot_w
- y = 1 - ((cy - self.curve_margin) / plot_h)
- return x, y
- def _draw_curve(self):
- if self.curve_canvas.winfo_width() <= 1: return
- self.curve_canvas.delete("all")
- self.point_handles.clear()
- w, h = self.curve_canvas.winfo_width(), self.curve_canvas.winfo_height()
- self.curve_canvas.create_rectangle(self.curve_margin, self.curve_margin, w - self.curve_margin, h - self.curve_margin, outline="grey")
- p1_canvas = self._coords_to_canvas(0.0, 0.0)
- p2_canvas = self._coords_to_canvas(1.0, 1.0)
- self.curve_canvas.create_line(p1_canvas, p2_canvas, fill="#cccccc", width=1, dash=(2, 4))
- canvas_points = [self._coords_to_canvas(p[0], p[1]) for p in self.curve_points]
- self.curve_canvas.create_line(canvas_points, fill="#0275d8", width=2)
- for i, (px, py) in enumerate(canvas_points):
- r = 4
- color = "#0275d8" if 0 < i < len(canvas_points) - 1 else "grey"
- handle = self.curve_canvas.create_oval(px - r, py - r, px + r, py + r, fill=color, outline="black")
- self.point_handles.append(handle)
- def _on_canvas_press(self, event):
- for i, handle_id in enumerate(self.point_handles):
- if 0 < i < len(self.point_handles) - 1:
- coords = self.curve_canvas.coords(handle_id)
- if coords[0] <= event.x <= coords[2] and coords[1] <= event.y <= coords[3]:
- self.dragged_point_index = i
- break
- def _on_canvas_drag(self, event):
- if self.dragged_point_index is None: return
- x, y = self._canvas_to_coords(event.x, event.y)
- prev_x = self.curve_points[self.dragged_point_index - 1][0]
- next_x = self.curve_points[self.dragged_point_index + 1][0]
- self.curve_points[self.dragged_point_index][0] = max(prev_x + 0.01, min(next_x - 0.01, x))
- self.curve_points[self.dragged_point_index][1] = max(0.0, min(1.0, y))
- self._draw_curve()
- self.on_setting_change()
- def _on_canvas_release(self, event):
- self.dragged_point_index = None
- def populate_mice_list(self):
- self.mouse_listbox.delete(0, tk.END)
- self.mice_data = list_raw_mice()
- for mouse in self.mice_data: self.mouse_listbox.insert(tk.END, f" {mouse['display_name']}")
- def populate_windows_list(self):
- self.app_listbox.delete(0, tk.END)
- self.windows_data = [{'title': 'None (Do not hide mouse)', 'hwnd': None}] + list_windows()
- for window in self.windows_data: self.app_listbox.insert(tk.END, f" {window['title'][:80]}")
- def select_mouse(self):
- selection = self.mouse_listbox.curselection()
- if not selection: messagebox.showwarning("Selection Error", "Please select a mouse."); return
- self._highlight_listbox_selection(self.mouse_listbox, selection[0])
- self.selected_mouse = self.mice_data[selection[0]]
- self.status_label.config(text=f"Mouse: {self.selected_mouse['display_name']}")
- self.on_setting_change()
- def select_app(self):
- selection = self.app_listbox.curselection()
- if not selection: messagebox.showwarning("Selection Error", "Please select an application."); return
- self._highlight_listbox_selection(self.app_listbox, selection[0])
- self.selected_window = self.windows_data[selection[0]]
- self.status_label.config(text=f"App: {self.selected_window['title'][:50]}...")
- self.on_setting_change()
- def populate_profiles_list(self):
- try:
- current_selection = self.selected_profile.get()
- profiles = sorted([f for f in os.listdir('.') if f.endswith('.txt')])
- self.profile_combobox['values'] = profiles
- if current_selection in profiles:
- self.selected_profile.set(current_selection)
- elif not self.selected_profile.get() and profiles:
- self.selected_profile.set(profiles[0])
- except Exception as e:
- messagebox.showerror("Profile Error", f"Could not read profile files: {e}")
- def _write_profile_to_file(self, filename):
- try:
- with open(filename, 'w', encoding='utf-8') as f:
- f.write(f"reset_key: {self.reset_key.get()}\n")
- f.write(f"sensitivity: {self.sensitivity.get()}\n")
- f.write(f"reverse_output: {self.reverse_output.get()}\n")
- f.write(f"curve_points: {self.curve_points}\n")
- if self.selected_mouse:
- f.write(f"mouse_name: {self.selected_mouse['raw_name']}\n")
- if self.selected_window:
- f.write(f"app_title: {self.selected_window['title']}\n")
- comments = self.comments_text.get("1.0", tk.END).strip()
- if comments:
- f.write("comments_start:\n")
- f.write(comments + "\n")
- f.write("comments_end:\n")
- return True
- except Exception as e:
- messagebox.showerror("Save Error", f"Failed to save profile: {e}")
- return False
- def save_profile(self):
- if not self.selected_mouse or not self.selected_window:
- messagebox.showwarning("Save Error", "Please select a mouse and an application before saving.")
- return
- profile_name = simpledialog.askstring("Save Profile", "Enter profile name:")
- if not profile_name or not profile_name.strip():
- return
- safe_name = re.sub(r'[\\/*?:"<>|]', "", profile_name.strip())
- if not safe_name:
- messagebox.showerror("Save Error", "The provided profile name contains only invalid characters.")
- return
- filename = f"{safe_name}.txt"
- if os.path.exists(filename) and not messagebox.askyesno("Overwrite", f"Profile '{filename}' already exists. Overwrite?"):
- return
- if self._write_profile_to_file(filename):
- messagebox.showinfo("Success", f"Profile '{filename}' saved.")
- self.populate_profiles_list()
- self.selected_profile.set(filename)
- self.loaded_profile_name = filename
- self.settings_changed = False
- self._update_profile_status_display()
- def overwrite_profile(self):
- if not self.loaded_profile_name:
- messagebox.showerror("Overwrite Error", "No profile is currently active to overwrite.")
- return
- if not self.selected_mouse or not self.selected_window:
- messagebox.showwarning("Save Error", "A mouse and an application must be selected.")
- return
- if messagebox.askyesno("Confirm Overwrite", f"Are you sure you want to overwrite '{self.loaded_profile_name}' with the current settings?"):
- if self._write_profile_to_file(self.loaded_profile_name):
- messagebox.showinfo("Success", f"Profile '{self.loaded_profile_name}' has been updated.")
- self.settings_changed = False
- self._update_profile_status_display()
- def load_profile(self):
- filename = self.selected_profile.get()
- if not filename:
- messagebox.showwarning("Load Error", "No profile selected.")
- return
- try:
- settings = {}
- comments_lines = []
- is_reading_comments = False
- with open(filename, 'r', encoding='utf-8') as f:
- for line in f:
- stripped_line = line.strip()
- if stripped_line == "comments_start:":
- is_reading_comments = True
- continue
- if stripped_line == "comments_end:":
- is_reading_comments = False
- continue
- if is_reading_comments:
- comments_lines.append(line)
- elif ':' in line:
- key, value = line.split(':', 1)
- settings[key.strip()] = value.strip()
- self.reset_key.set(settings.get('reset_key', DEFAULT_RESET_KEY))
- new_sens = float(settings.get('sensitivity', settings.get('input_range', DEFAULT_SENSITIVITY)))
- self.sensitivity.set(new_sens)
- self._update_sensitivity_label(new_sens)
- reverse_val_str = settings.get('reverse_output', 'False')
- self.reverse_output.set(reverse_val_str.lower() == 'true')
- try:
- curve_str = settings.get('curve_points')
- if curve_str:
- self.curve_points = ast.literal_eval(curve_str)
- else:
- self.curve_points = [p[:] for p in DEFAULT_CURVE_POINTS]
- except (ValueError, SyntaxError):
- self.curve_points = [p[:] for p in DEFAULT_CURVE_POINTS]
- self._draw_curve()
- self.comments_text.delete("1.0", tk.END)
- if comments_lines:
- self.comments_text.insert("1.0", "".join(comments_lines).strip())
- mouse_name = settings.get('mouse_name')
- found_mouse = False
- if mouse_name:
- self.populate_mice_list()
- for i, mouse in enumerate(self.mice_data):
- if mouse['raw_name'] == mouse_name:
- self.mouse_listbox.selection_clear(0, tk.END)
- self.mouse_listbox.selection_set(i)
- self.selected_mouse = self.mice_data[i]
- self._highlight_listbox_selection(self.mouse_listbox, i)
- found_mouse = True
- break
- if not found_mouse:
- messagebox.showwarning("Load Warning", f"Could not find saved mouse:\n{mouse_name}")
- self.selected_mouse = None
- self._highlight_listbox_selection(self.mouse_listbox, None)
- app_title = settings.get('app_title')
- found_app = False
- if app_title:
- self.populate_windows_list()
- for i, window in enumerate(self.windows_data):
- if app_title in window['title']:
- self.app_listbox.selection_clear(0, tk.END)
- self.app_listbox.selection_set(i)
- self.selected_window = self.windows_data[i]
- self._highlight_listbox_selection(self.app_listbox, i)
- found_app = True
- break
- if not found_app:
- messagebox.showwarning("Load Warning", f"Could not find an open application with title containing:\n{app_title}")
- self.selected_window = None
- self._highlight_listbox_selection(self.app_listbox, None)
- self.status_label.config(text=f"Profile '{filename}' loaded. Ready to start.")
- self.loaded_profile_name = filename
- self.settings_changed = False
- self._update_profile_status_display()
- except FileNotFoundError:
- messagebox.showerror("Load Error", f"Profile file not found: {filename}"); self.populate_profiles_list()
- except Exception as e:
- messagebox.showerror("Load Error", f"Failed to load profile '{filename}':\n{e}")
- def start_conversion(self):
- if not self.selected_mouse or not self.selected_window:
- messagebox.showerror("Error", "A mouse and application must be selected."); return
- self.is_running = True
- self.controller_name_var.set("Emulating: XBox 360 Controller")
- self.stop_button.config(state=tk.NORMAL)
- for widget in self.selection_controls: widget.config(state=tk.DISABLED)
- self.status_label.config(text=f"Running... Use TAB and SPACE to select and activate the Stop Conversion button.")
- user32.GetWindowThreadProcessId(self.selected_window['hwnd'], ctypes.byref(self.target_pid))
- self.hook_id = user32.SetWindowsHookExW(14, self.low_level_mouse_proc_ptr, 0, 0)
- if not self.hook_id:
- messagebox.showerror("Error", "Failed to set mouse hook.")
- self._finalize_stop()
- return
- self.conversion_thread = threading.Thread(target=self._conversion_loop, daemon=True)
- self.conversion_thread.start()
- self.root.after(100, self._update_visualizer)
- def stop_conversion(self):
- self.status_label.config(text="Status: Stopping...")
- self.is_running = False
- self.stop_button.config(state=tk.DISABLED)
- self.root.after(100, self._check_thread_stopped)
- def _check_thread_stopped(self):
- if self.conversion_thread and self.conversion_thread.is_alive():
- self.root.after(100, self._check_thread_stopped)
- else:
- self._finalize_stop()
- def _finalize_stop(self):
- if self.hook_id:
- user32.UnhookWindowsHookEx(self.hook_id)
- self.hook_id = None
- self.status_label.config(text="Status: Stopped.")
- self.controller_name_var.set("Emulating: None")
- for widget in self.selection_controls: widget.config(state=tk.NORMAL)
- def on_closing(self):
- if self.is_running: self.is_running = False
- self.root.destroy()
- def _apply_curve(self, value):
- sensitivity_val = self.sensitivity.get()
- if sensitivity_val <= 0: return 0.0
- # Normalize slider value from 0-100 to 0.0-1.0
- slider_val_norm = sensitivity_val / 100.0
- # Apply an exponential scale (4 * x^2). This keeps the midpoint (50) at 1.0x
- # while making the high end (100) much more sensitive at 4.0x.
- sensitivity_multiplier = 10.0 * (slider_val_norm ** 3.3219)
- if sensitivity_multiplier <= 0: return 0.0
- # An effective input range is calculated. Higher sensitivity means a smaller range,
- # so less mouse movement is needed for max joystick output.
- effective_range = BASE_INPUT_RANGE / sensitivity_multiplier
- sign = 1.0 if value >= 0 else -1.0
- norm_input_x = min(1.0, abs(value) / effective_range)
- p1, p2 = self.curve_points[0], self.curve_points[1]
- for i in range(1, len(self.curve_points)):
- if norm_input_x <= self.curve_points[i][0]:
- p1 = self.curve_points[i-1]
- p2 = self.curve_points[i]
- break
- x_range = p2[0] - p1[0]
- if x_range == 0:
- norm_output_y = p1[1]
- else:
- y_range = p2[1] - p1[1]
- t = (norm_input_x - p1[0]) / x_range
- norm_output_y = p1[1] + t * y_range
- return norm_output_y * sign
- def _conversion_loop(self):
- hwnd, class_atom, gamepad = None, None, None
- hInstance = kernel32.GetModuleHandleW(None)
- class_name = f"RawInputWindowClass-{time.time()}"
- try:
- gamepad = vg.VX360Gamepad()
- wnd_class = WNDCLASSW(lpfnWndProc=self.wnd_proc_ptr, lpszClassName=class_name, hInstance=hInstance)
- class_atom = user32.RegisterClassW(ctypes.byref(wnd_class))
- if not class_atom: raise RuntimeError("Failed to register window class.")
- hwnd = user32.CreateWindowExW(0, class_name, None, 0, 0, 0, 0, 0, -3, 0, hInstance, None)
- if not hwnd: raise RuntimeError("Failed to create message-only window.")
- RIDEV_INPUTSINK, RIDEV_NOLEGACY = 0x00000100, 0x00000030
- device = RAWINPUTDEVICE(1, 2, RIDEV_INPUTSINK | RIDEV_NOLEGACY, hwnd)
- if not user32.RegisterRawInputDevices(ctypes.byref(device), 1, ctypes.sizeof(RAWINPUTDEVICE)):
- raise RuntimeError("Failed to register for raw input.")
- msg = MSG(); p_msg = ctypes.byref(msg)
- while self.is_running:
- while user32.PeekMessageW(p_msg, None, 0, 0, 1):
- user32.TranslateMessage(p_msg); user32.DispatchMessageW(p_msg)
- if time.time() - self.last_mouse_event_time > DECAY_TIMEOUT: self.joystick_x, self.joystick_y = 0.0, 0.0
- key_str = self.reset_key.get()
- if key_str:
- vk_code = ord(key_str[0].upper())
- if user32.GetAsyncKeyState(vk_code) & 0x8000:
- self.joystick_x, self.joystick_y = 0.0, 0.0
- final_x = -self.joystick_y
- final_y = -self.joystick_x
- if self.reverse_output.get():
- final_x *= -1
- final_y *= -1
- gamepad.left_joystick_float(x_value_float=final_x, y_value_float=final_y)
- gamepad.update(); time.sleep(0.001)
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("Runtime Error", f"An error occurred in worker thread: {e}"))
- self.root.after(0, self.stop_conversion)
- finally:
- if hwnd: user32.DestroyWindow(hwnd)
- if class_atom: user32.UnregisterClassW(class_name, hInstance)
- def _wnd_proc(self, hwnd, msg, wparam, lparam):
- if msg == 0x00FF: # WM_INPUT
- size = wintypes.UINT(0)
- user32.GetRawInputData(lparam, 0x10000003, None, ctypes.byref(size), ctypes.sizeof(RAWINPUTHEADER))
- if size.value > 0:
- buf = ctypes.create_string_buffer(size.value)
- user32.GetRawInputData(lparam, 0x10000003, buf, ctypes.byref(size), ctypes.sizeof(RAWINPUTHEADER))
- raw = ctypes.cast(buf, ctypes.POINTER(RAWINPUT)).contents
- if self.selected_mouse and raw.header.hDevice == self.selected_mouse['handle']:
- self.last_mouse_event_time = time.time()
- dx, dy = raw.mouse.lLastX, raw.mouse.lLastY
- self.mouse_dx, self.mouse_dy = dx, dy
- self.joystick_x = max(-1.0, min(1.0, self._apply_curve(dx)))
- self.joystick_y = max(-1.0, min(1.0, self._apply_curve(-dy)))
- return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
- def _low_level_mouse_proc(self, nCode, wParam, lParam):
- if nCode >= 0 and self.is_running and self.target_pid.value != 0:
- foreground_hwnd = user32.GetForegroundWindow()
- if foreground_hwnd:
- foreground_pid = wintypes.DWORD(0)
- user32.GetWindowThreadProcessId(foreground_hwnd, ctypes.byref(foreground_pid))
- if foreground_pid.value == self.target_pid.value:
- return 1
- return user32.CallNextHookEx(self.hook_id, nCode, wParam, lParam)
- if __name__ == '__main__':
- if check_and_install_vgamepad():
- root = tk.Tk()
- app = Mouse2JoystickApp(root)
- root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment