-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
249 lines (194 loc) · 7.4 KB
/
Copy pathmain.py
File metadata and controls
249 lines (194 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import tkinter as tk
from tkinter import ttk
import psutil
import time
import threading
import argparse
import os
import tempfile
import sys
import core.file_loader as file_loader
from core.asset_loader import AssetLoader
import core.theme as theme
from core.input_handler import setup_tab_minimizer
from core.window_handler import WindowResizer, get_window_position
from audio.audio_player import AudioPlayer
from ui.header import Header
from ui.play_area import PlayArea
from ui.footer import Footer
class App:
def __init__(self, args):
self.root = tk.Tk()
self._setup_window()
self._load_data()
self._setup_fonts()
self._setup_layout()
self._setup_style()
self._setup_ui()
self._setup_events()
self._setup_resize()
if args.min:
self.header.minimize()
if self.settings["auto_close_on_game_close"] and args.autoclose:
self._start_nw_detection()
self.root.mainloop()
def _setup_window(self):
self.root.title("LuteGuide")
self.root.attributes("-topmost", True)
self.root.overrideredirect(True)
self.root.minsize(600, 200)
def _load_data(self):
self.settings = file_loader.load_settings()
self.song_statistics = file_loader.load_song_statistics()
self.assets = AssetLoader(self.root)
self.audio_player = AudioPlayer(self.root, self.assets)
self.song_selector = None
def _setup_fonts(self):
file_loader.load_temp_font(self.root, theme.FONT_PATHS["Sava Pro Light"])
file_loader.load_temp_font(self.root, theme.FONT_PATHS["StoneSansStd-Medium"])
def _setup_layout(self):
x, y = get_window_position(self.root, self.settings)
self.root.geometry(
f'{self.settings["window_width"]}x{self.settings["window_height"]}+{x}+{y}'
)
self.root.configure(bg=theme.TRANSPARENT)
self.root.wm_attributes("-transparentcolor", theme.TRANSPARENT)
self.root.attributes("-alpha", 0.9)
def _setup_style(self):
self.style = ttk.Style()
self.style.theme_use("alt")
self.root.update_idletasks()
def _setup_ui(self):
self.listener = setup_tab_minimizer(self)
self.layout = tk.Frame(self.root, bg=theme.TRANSPARENT)
self.layout.pack(fill="both", expand=True)
self.layout.root = self.root
# Header
self.header = Header(
self.layout,
self.assets,
"LuteGuide",
on_close=self.on_closing
)
# Play area
self.play_area = PlayArea(self.layout, self)
self.play_area.place(x=0, relwidth=1)
self.play_area.lower(self.header)
self.play_area.update_font(theme.FONT_FAMILY_PRIMARY, self.settings["font_size"])
# Footer
self.footer = Footer(self)
def _setup_events(self):
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.root.bind("<Enter>", self._show_footer)
self.root.bind("<Leave>", self._hide_footer)
def _setup_resize(self):
def on_window_resize(w, h):
header_h = self.header.visual_height
overlap = header_h // 2
play_h = h - overlap
self.play_area.place(
x=0,
y=overlap,
relwidth=1,
height=play_h
)
WindowResizer(self.root, on_resize=on_window_resize)
self.root.after(
0,
lambda: on_window_resize(
self.root.winfo_width(),
self.root.winfo_height()
)
)
def _show_footer(self, event=None):
self.footer.show()
def _hide_footer(self, event):
if event.widget == self.root and (self.song_selector is None or self.song_selector.visible is False):
self.footer.hide()
def on_closing(self):
self.settings["window_width"] = self.root.winfo_width()
self.settings["window_height"] = self.root.winfo_height()
self.settings["window_x"] = self.root.winfo_x()
self.settings["window_y"] = self.root.winfo_y()
self.settings["click_sound_state"] = self.footer.click_sound_state.get()
self.settings["note_sound_state"] = self.footer.note_sound_state.get()
self.settings["font_size"] = int(round(self.footer.font_slider.value))
file_loader.save_json(self.settings, "settings.json")
file_loader.save_json(self.song_statistics, "song_statistics.json")
self.root.after_idle(lambda: (self.root.quit(), self.root.destroy()))
def _start_nw_detection(self):
t = threading.Thread(target=self._watch_nw_process, daemon=True)
t.start()
return t
def _watch_nw_process(self):
nw_seen = False
grace_start = None
gameclient_pid = None
while True:
processes = {p.info['name']: p for p in psutil.process_iter(['name']) if p.info['name']}
nw_proc = processes.get("Neverwinter.exe")
gc_proc = processes.get("GameClient.exe")
if gc_proc:
gameclient_pid = gc_proc.pid
break
if nw_proc:
nw_seen = True
grace_start = None
else:
if nw_seen:
if grace_start is None:
grace_start = time.time()
elif time.time() - grace_start >= 2:
print("Neverwinter closed and GameClient never started.")
self.on_closing()
return
time.sleep(1)
try:
proc = psutil.Process(gameclient_pid)
proc.wait()
print("GameClient.exe closed")
except (psutil.NoSuchProcess, psutil.ZombieProcess):
print("GameClient already exited")
self.on_closing()
class InstanceLock:
def __init__(self, name="bard_overlay.lock"):
self._lockfile = os.path.join(tempfile.gettempdir(), name)
def acquire(self):
if os.path.exists(self._lockfile):
try:
with open(self._lockfile, "r") as f:
pid = int(f.read().strip())
if psutil.pid_exists(pid):
return False
except Exception:
pass
# stale
try:
os.remove(self._lockfile)
except Exception:
pass
with open(self._lockfile, "w") as f:
f.write(str(os.getpid()))
return True
def release(self):
try:
if os.path.exists(self._lockfile):
with open(self._lockfile, "r") as f:
pid = int(f.read().strip())
if pid == os.getpid():
os.remove(self._lockfile)
except Exception:
pass
if __name__ == "__main__":
lock = InstanceLock()
if not lock.acquire():
print("Another instance is running")
sys.exit(0)
try:
parser = argparse.ArgumentParser(description="LuteGuide")
parser.add_argument("-min", action="store_true", help="Minimized window at startup")
parser.add_argument("-autoclose", action="store_true", help="Closes when it detects Neverwinter closing. Requires 'auto_close_on_shutdown' to be true in settings.json")
args = parser.parse_args()
app = App(args)
finally:
lock.release()