Skip to content

Commit

Permalink
Add tranparency slider and toggle to allow pymhf gui to always be on top
Browse files Browse the repository at this point in the history
  • Loading branch information
monkeyman192 committed Dec 27, 2024
1 parent ba6bfee commit bab8f17
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 27 deletions.
93 changes: 72 additions & 21 deletions pymhf/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
from enum import Enum
from typing import Optional, TypedDict, Union

# import win32gui
# import win32con
import dearpygui.dearpygui as dpg
import win32gui

import pymhf.core._internal as _internal
import pymhf.core.caching as cache
from pymhf.core.mod_loader import Mod, ModManager
from pymhf.gui.protocols import ButtonProtocol, VariableProtocol, VariableType
from pymhf.utils.winapi import set_window_transparency

SETTINGS_NAME = "_pymhf_gui_settings"
DETAILS_NAME = "_pymhf_gui_details"
WINDOW_TITLE = "pyMHF"

# TODO:
# - add keyboard shortcut to show or hide the GUI
Expand All @@ -32,15 +35,19 @@ class Widgets(TypedDict):
variables: dict[str, list[tuple[Union[int, str], WidgetType]]]


def toggle_on_top(item: int, value: bool):
dpg.set_viewport_always_top(value)


class GUI:
def __init__(self, mod_manager: ModManager, config: dict):
self.config = config
self.scale = config.get("gui", {}).get("scale", 1)
dpg.create_context()
dpg.create_viewport(
title="pyMHF",
width=int(400 * self.scale),
height=int(400 * self.scale),
title=WINDOW_TITLE,
width=int(600 * self.scale),
height=int(800 * self.scale),
decorated=True,
)
dpg.setup_dearpygui()
Expand All @@ -60,8 +67,14 @@ def __init__(self, mod_manager: ModManager, config: dict):
self._window_dimensions = [0, 0]
self._window_position = [0, 0]

# Some info related settings
self._hide_pyd_modules = True

self.add_window()

def alpha_callback(self, sender, app_data):
set_window_transparency(self.hwnd, app_data)

def show_window(self):
# TODO: This needs to be called twice to run properly...
# It might still be better to try and use the `win32gui` calls to get the system to handle this better
Expand Down Expand Up @@ -98,20 +111,43 @@ def add_settings_tab(self):
tab_alias = dpg.get_alias_id(tab)
self.tabs[tab_alias] = SETTINGS_NAME

# Toggle for debug mode
with dpg.group(horizontal=True, parent=SETTINGS_NAME):
dpg.add_text("Enable debug mode")
dpg.add_checkbox(
source="is_debug",
callback=self.toggle_debug_mode,
)
# Toggle for whether to show the gui at all.
with dpg.group(horizontal=True, parent=SETTINGS_NAME):
dpg.add_text("Show GUI")
dpg.add_checkbox(
source="show_gui",
callback=self.toggle_show_gui,
)
with dpg.table(header_row=False, parent=SETTINGS_NAME, policy=dpg.mvTable_SizingStretchProp):
dpg.add_table_column()
dpg.add_table_column()

# Toggle for debug mode
with dpg.table_row():
dpg.add_text("Enable debug mode")
dpg.add_checkbox(
source="is_debug",
callback=self.toggle_debug_mode,
)

# Toggle for whether to show the gui at all.
with dpg.table_row():
dpg.add_text("Show GUI")
dpg.add_checkbox(
source="show_gui",
callback=self.toggle_show_gui,
)

# Add a slider for the visibility.
with dpg.table_row():
dpg.add_text("Transparency")
dpg.add_slider_float(
default_value=1,
max_value=1,
min_value=0,
callback=self.alpha_callback,
)

# Add a checkbox to toggle whether the window should stay always on top.
with dpg.table_row():
dpg.add_text("Always on top")
dpg.add_checkbox(callback=toggle_on_top)

def _toggle_show_pyd(self, item: int, value: bool):
self._hide_pyd_modules = value

def add_details_tab(self):
tab = dpg.add_tab(label="Details", tag=DETAILS_NAME, parent="tabbar")
Expand All @@ -125,6 +161,20 @@ def add_details_tab(self):
for func_name in functions.keys():
dpg.add_tree_node(label=func_name, parent=dll_branch, leaf=True, bullet=True)

# TODO: Add this toggle back and make it work.
# with dpg.group(horizontal=True, parent=DETAILS_NAME):
# dpg.add_text("Hide *.pyd files")
# dpg.add_checkbox(callback=self._toggle_show_pyd, default_value=True)

# Add in a section to show the actual list of loaded modules
module_tree = dpg.add_tree_node(label="Loaded modules", parent=DETAILS_NAME)
if self._hide_pyd_modules:
names = (name for name in cache.module_map.keys() if not name.endswith(".pyd"))
else:
names = (name for name in cache.module_map.keys())
for func_name in names:
dpg.add_tree_node(label=func_name, parent=module_tree, leaf=True, bullet=True)

def reload_tab(self, cls: Mod):
"""Reload the tab for the specific mod."""
name = cls.__class__.__name__
Expand Down Expand Up @@ -235,8 +285,8 @@ def change_tab(self, sender: str, app_data: int):
def add_window(self):
with dpg.window(
label="pyMHF",
width=int(200 * self.scale),
height=int(200 * self.scale),
width=int(600 * self.scale),
height=int(800 * self.scale),
tag="pyMHF",
on_close=self.exit,
):
Expand Down Expand Up @@ -340,7 +390,8 @@ def remove_tab(self, cls: Mod):
def run(self):
try:
dpg.show_viewport()
dpg.set_primary_window("pyMHF", True)
dpg.set_primary_window(WINDOW_TITLE, True)
self.hwnd = win32gui.FindWindow(None, WINDOW_TITLE)
while dpg.is_dearpygui_running():
# For each tracking variable, update the value.
for vars in self.tracking_variables.get(self._current_tab, []):
Expand Down
48 changes: 42 additions & 6 deletions pymhf/utils/winapi.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
# This file includes various functions which use any of the various windows dlls to determine things.

import ctypes
import ctypes.wintypes
from ctypes import wintypes

import pymem

GetModuleFileNameExA = ctypes.windll.psapi.GetModuleFileNameExA
GetModuleFileNameExA.restype = ctypes.wintypes.DWORD
GetModuleFileNameExA.restype = wintypes.DWORD
GetModuleFileNameExA.argtypes = [
ctypes.wintypes.HANDLE,
ctypes.wintypes.HMODULE,
ctypes.wintypes.LPSTR,
ctypes.wintypes.DWORD,
wintypes.HANDLE,
wintypes.HMODULE,
wintypes.LPSTR,
wintypes.DWORD,
]

GetWindowLongA = ctypes.windll.user32.GetWindowLongA
GetWindowLongA.restype = wintypes.LONG
GetWindowLongA.argtypes = [
wintypes.HWND,
ctypes.c_int,
]

SetWindowLongA = ctypes.windll.user32.SetWindowLongA
SetWindowLongA.restype = wintypes.LONG
SetWindowLongA.argtypes = [
wintypes.HWND,
ctypes.c_int,
wintypes.LONG,
]

SetLayeredWindowAttributes = ctypes.windll.user32.SetLayeredWindowAttributes
SetLayeredWindowAttributes.restype = wintypes.BOOL
SetLayeredWindowAttributes.argtypes = [
wintypes.HWND,
wintypes.COLORREF,
wintypes.BYTE,
wintypes.DWORD,
]

MAX_EXE_NAME_SIZE = 1024
WS_EX_LAYERED = 0x00080000 # layered window
GWL_EXSTYLE = -20 # "extended window style"

LWA_COLORKEY = 0x00000001
LWA_ALPHA = 0x00000002


def get_exe_path_from_pid(proc: pymem.Pymem) -> str:
"""Get the name of the exe which was run to create the pymem process."""
name_buffer = ctypes.create_string_buffer(b"", MAX_EXE_NAME_SIZE)
GetModuleFileNameExA(proc.process_handle, None, name_buffer, MAX_EXE_NAME_SIZE)
return name_buffer.value.decode()


def set_window_transparency(hwnd: int, alpha: float):
SetWindowLongA(hwnd, GWL_EXSTYLE, GetWindowLongA(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED)
rgb = wintypes.RGB(10, 10, 10)
SetLayeredWindowAttributes(hwnd, rgb, int(255 * alpha), LWA_ALPHA)

0 comments on commit bab8f17

Please sign in to comment.