Repository navigation
Expand file tree
/
Copy pathdesktop_ui.py
More file actions
334 lines (305 loc) · 23.3 KB
/
Copy pathdesktop_ui.py
File metadata and controls
334 lines (305 loc) · 23.3 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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# SPDX-License-Identifier: GPL-3.0-only
"""Native desktop presentation; transfer and conversion logic stays in app.py."""
import os
import tkinter as tk
from tkinter import ttk, messagebox
from bridge import ROOT, ASSETS
from storage_ui import StoragePicker
from game_list import GameList
BG='#f3f5f7'
SURFACE='#ffffff'
INK='#172936'
MUTED='#647582'
NAV='#142b39'
ACCENT='#147d70'
class ScrollPage(ttk.Frame):
"""Keyboard/wheel/arrow accessible page for small displays and high DPI."""
def __init__(self, parent):
super().__init__(parent)
self.canvas=tk.Canvas(self,bg=BG,highlightthickness=0)
rail=ttk.Frame(self);rail.pack(side='right',fill='y')
ttk.Button(rail,text='▴',width=2,command=lambda:self.canvas.yview_scroll(-3,'units')).pack()
scroll=ttk.Scrollbar(rail,orient='vertical',command=self.canvas.yview)
scroll.pack(fill='y',expand=True)
ttk.Button(rail,text='▾',width=2,command=lambda:self.canvas.yview_scroll(3,'units')).pack()
self.canvas.configure(yscrollcommand=scroll.set)
self.canvas.pack(fill='both',expand=True)
self.body=ttk.Frame(self.canvas,padding=(24,12,14,20))
self.window=self.canvas.create_window(0,0,window=self.body,anchor='nw')
self.body.bind('<Configure>',lambda _:self.canvas.configure(scrollregion=self.canvas.bbox('all')))
self.canvas.bind('<Configure>',lambda e:self.canvas.itemconfigure(self.window,width=e.width))
def wheel(self,event):
# Each page handles only its own descendants, never another window.
widget=event.widget
while widget is not None and widget is not self:
widget=getattr(widget,'master',None)
if widget is self and not isinstance(event.widget,(tk.Listbox,tk.Text,ttk.Treeview,ttk.Combobox)):
self.canvas.yview_scroll(-int(event.delta/120),'units')
class DesktopUI:
def build_ui(self):
self.title('Quest2Frame');self.geometry('1280x720');self.minsize(960,600)
self.configure(bg=BG)
self.action_buttons=[]
self._locked=[]
self.status=tk.StringVar(value='Connect your Frame to get started. Your Quest stays connected by USB.')
self.device_status=tk.StringVar(value='FRAME / Not verified')
self.library_count=tk.StringVar(value='Scan your Quest to discover installed games')
self.selection_count=tk.StringVar(value='No games selected')
self.progress_value=tk.DoubleVar(value=0)
self.progress_text=tk.StringVar(value='Ready when you are')
self.vars['refresh_rate']=tk.StringVar(value='Default')
self.vars['headset']=tk.StringVar(value='Default')
self.rect_fix=tk.BooleanVar(value=False)
self.theme()
side=tk.Frame(self,bg=NAV,width=204);side.pack(side='left',fill='y');side.pack_propagate(False)
logo_source=tk.PhotoImage(master=self,file=str(ASSETS/'assets/quest2frame-logo.png'))
self.logo_image=logo_source.subsample(max(1,(logo_source.width()+79)//80))
self.window_icon=logo_source.subsample(max(1,(logo_source.width()+255)//256))
self.iconphoto(True,self.window_icon)
tk.Label(side,image=self.logo_image,bg=NAV,bd=0).pack(anchor='w',padx=23,pady=(17,1))
tk.Label(side,text='Quest2Frame',bg=NAV,fg='white',font=('Segoe UI',17,'bold')).pack(anchor='w',padx=23,pady=(0,25))
self.nav=[]
for index,label in enumerate(('01 Connect','02 Choose games','03 Review & convert','04 Install on Frame')):
button=tk.Button(side,text=label,anchor='w',relief='flat',bd=0,padx=18,pady=14,
font=('Segoe UI',10,'bold'),bg=NAV,fg='#bdccd5',activebackground='#264553',activeforeground='white',
command=lambda i=index:self.tabs.select(i))
button.pack(fill='x',padx=10,pady=3);self.nav.append(button)
tk.Frame(side,height=1,bg='#35505e').pack(fill='x',padx=23,pady=18)
for label,command in [('Transfer queue',self.show_batch),('Saved queue…',self.open_batch),('Manage Frame games',self.manage_installed),('Backups & builds',lambda:os.startfile(ROOT/'games'))]:
button=tk.Button(side,text=label,command=command,anchor='w',relief='flat',bd=0,padx=18,pady=10,
bg=NAV,fg='#bdccd5',activebackground='#264553',activeforeground='white',font=('Segoe UI',10))
button.pack(fill='x',padx=10);self.action_buttons.append(button)
tk.Button(side,text='About & licenses',command=self.about,bg=NAV,fg='#94afbe',relief='flat',bd=0,font=('Segoe UI',9)).pack(side='bottom',anchor='w',padx=22,pady=20)
main=ttk.Frame(self);main.pack(side='left',fill='both',expand=True)
top=ttk.Frame(main,padding=(26,18,26,10));top.pack(fill='x')
ttk.Label(top,text='WORKSPACE',style='Eyebrow.TLabel').pack(side='left')
ttk.Label(top,textvariable=self.device_status,style='Muted.TLabel').pack(side='right')
footer=ttk.Frame(main,style='Card.TFrame',padding=(24,12));footer.pack(side='bottom',fill='x')
line=ttk.Frame(footer,style='Card.TFrame');line.pack(fill='x')
ttk.Label(line,textvariable=self.progress_text,style='Card.TLabel',wraplength=690).pack(side='left',fill='x',expand=True)
self.button(line,'Activity log',self.show_log).pack(side='right',padx=(12,0))
self.progress=ttk.Progressbar(footer,variable=self.progress_value,maximum=100,mode='determinate')
self.progress.pack(fill='x',pady=(9,7))
ttk.Label(footer,textvariable=self.status,style='CardMuted.TLabel',wraplength=880).pack(anchor='w')
footer.bind('<Configure>',lambda e:self.wrap_labels(footer,e.width-48))
self.tabs=ttk.Notebook(main,style='Pages.TNotebook');self.tabs.pack(fill='both',expand=True)
self.pages=[];self.scroll_pages=[]
for label in ('Connect','Choose games','Review & convert','Install on Frame'):
page=ScrollPage(self.tabs);self.tabs.add(page,text=label);self.scroll_pages.append(page);self.pages.append(page.body)
self.setup_page();self.quest_page();self.convert_page();self.install_page()
self.tabs.bind('<<NotebookTabChanged>>',self.navigation_changed)
self.bind_all('<MouseWheel>',self.page_wheel,add='+')
self.navigation_changed()
# A hidden native log window keeps diagnostics available without
# competing with the workflow for vertical space.
self.log_window=tk.Toplevel(self);self.log_window.title('Quest2Frame · Activity log')
self.log_window.geometry('950x450');self.log_window.withdraw()
self.log_window.protocol('WM_DELETE_WINDOW',self.log_window.withdraw)
self.logbox=tk.Text(self.log_window,bg=NAV,fg='#dbe8ef',insertbackground='white',font=('Consolas',10),wrap='word',state='disabled',relief='flat',padx=15,pady=15)
log_scroll=ttk.Scrollbar(self.log_window,command=self.logbox.yview);log_scroll.pack(side='right',fill='y')
self.logbox.configure(yscrollcommand=log_scroll.set);self.logbox.pack(fill='both',expand=True)
def theme(self):
self.option_add('*TCombobox*Listbox.background',SURFACE)
self.option_add('*TCombobox*Listbox.foreground',INK)
style=ttk.Style(self);style.theme_use('clam')
style.configure('.',background=BG,foreground=INK,font=('Segoe UI',10))
style.configure('TFrame',background=BG)
style.configure('Card.TFrame',background=SURFACE)
style.configure('TLabel',background=BG)
style.configure('Card.TLabel',background=SURFACE)
style.configure('Muted.TLabel',foreground=MUTED)
style.configure('CardMuted.TLabel',background=SURFACE,foreground=MUTED,font=('Segoe UI',9))
style.configure('Eyebrow.TLabel',foreground=ACCENT,font=('Segoe UI',9,'bold'))
style.configure('Title.TLabel',font=('Segoe UI',25,'bold'))
style.configure('CardTitle.TLabel',background=SURFACE,font=('Segoe UI',13,'bold'))
style.configure('TButton',padding=(13,9),background='#e7edf0',borderwidth=0,focuscolor=ACCENT)
style.map('TButton',background=[('active','#dce5e9')],foreground=[('disabled','#9aa8af')])
style.configure('Primary.TButton',background=ACCENT,foreground='white',font=('Segoe UI',10,'bold'))
style.map('Primary.TButton',background=[('disabled','#c2d2cf'),('active','#09675d')],foreground=[('disabled','#637e78'),('active','white')])
style.configure('TEntry',fieldbackground=SURFACE,padding=8,bordercolor='#d9e1e5',lightcolor='#d9e1e5',darkcolor='#d9e1e5')
style.configure('TCombobox',padding=7,fieldbackground=SURFACE,background='#e7edf0',arrowsize=14)
style.map('TCombobox',fieldbackground=[('readonly',SURFACE)],foreground=[('readonly',INK)])
style.configure('TCheckbutton',background=SURFACE,padding=(0,5))
style.map('TCheckbutton',background=[('active',SURFACE)])
style.configure('TRadiobutton',background=SURFACE,padding=(0,7))
style.configure('TLabelframe',background=BG,bordercolor='#d9e1e5')
style.configure('Treeview',background=SURFACE,fieldbackground=SURFACE,rowheight=34,borderwidth=0)
style.configure('Treeview.Heading',background='#e7edf0',padding=9,font=('Segoe UI',10,'bold'))
style.configure('Games.Treeview',rowheight=62,font=('Segoe UI',12),indent=10)
style.map('Treeview',background=[('selected','#cfece5')],foreground=[('selected',INK)])
style.configure('Horizontal.TProgressbar',background=ACCENT,troughcolor='#e7edf0',borderwidth=0,thickness=6)
style.layout('Pages.TNotebook.Tab',[])
style.configure('Pages.TNotebook',background=BG,borderwidth=0,tabmargins=0)
def button(self,parent,text,command,primary=False):
button=ttk.Button(parent,text=text,command=command,style='Primary.TButton' if primary else 'TButton')
if command!=self.show_log:self.action_buttons.append(button)
return button
def heading(self,page,title,description):
ttk.Label(page,text=title,style='Title.TLabel',wraplength=850).pack(anchor='w')
ttk.Label(page,text=description,style='Muted.TLabel',wraplength=800).pack(anchor='w',pady=(5,12))
page.bind('<Configure>',lambda e:self.wrap_labels(page,e.width-40),add='+')
def wrap_labels(self,parent,width):
for widget in parent.winfo_children():
if isinstance(widget,ttk.Label) and int(widget.cget('wraplength') or 0):
widget.configure(wraplength=max(150,width))
def responsive_cards(self,columns,left,right):
def layout(event):
stacked=event.width<790
columns.columnconfigure(0,weight=1,uniform='' if stacked else 'cards')
columns.columnconfigure(1,weight=0 if stacked else 1,uniform='' if stacked else 'cards')
left.grid_configure(row=0,column=0,padx=(0,0 if stacked else 8),pady=(0,12 if stacked else 0))
right.grid_configure(row=1 if stacked else 0,column=0 if stacked else 1,padx=(0 if stacked else 8,0))
columns.bind('<Configure>',layout)
for card in (left,right):
card.bind('<Configure>',lambda e,c=card:self.wrap_labels(c,e.width-28))
def card(self,parent,title):
card=ttk.Frame(parent,style='Card.TFrame',padding=14)
ttk.Label(card,text=title,style='CardTitle.TLabel').pack(anchor='w',pady=(0,9))
return card
def field(self,parent,name,label,show=None):
ttk.Label(parent,text=label,style='CardMuted.TLabel').pack(anchor='w',pady=(6,4))
entry=ttk.Entry(parent,textvariable=self.vars[name],show=show or '')
entry.pack(fill='x');return entry
def setup_page(self):
page=self.pages[0]
self.heading(page,'Two headsets. One library.','Connect Frame over your network, then plug Quest into your PC to choose your games.')
columns=ttk.Frame(page);columns.pack(fill='x');columns.columnconfigure((0,1),weight=1,uniform='cards')
frame=self.card(columns,'01 / Steam Frame');frame.grid(row=0,column=0,sticky='nsew',padx=(0,8))
self.field(frame,'host','Hostname or IP address')
row=ttk.Frame(frame,style='Card.TFrame');row.pack(fill='x',pady=(3,0))
left=ttk.Frame(row,style='Card.TFrame');left.pack(side='left',fill='x',expand=True,padx=(0,10))
right=ttk.Frame(row,style='Card.TFrame',width=85);right.pack(side='right');right.pack_propagate(False);right.configure(height=65)
self.field(left,'username','SSH username');self.field(right,'port','Port')
self.field(frame,'password','SSH password · never saved',show='•')
self.button(frame,'Verify Frame connection',self.test_ssh,True).pack(fill='x',pady=(16,8))
ttk.Label(frame,text='First connection requires Frame USB to verify its identity. After that, use Wi-Fi.',style='CardMuted.TLabel',wraplength=350).pack(anchor='w')
quest=self.card(columns,'02 / Meta Quest');quest.grid(row=0,column=1,sticky='nsew',padx=(8,0))
for title,body in [('Enable developer mode','Enable USB debugging on Quest and development access on Frame.'),('Connect Quest by USB','Unlock your Quest and accept the USB debugging prompt.'),('Keep Frame on your network','Quest stays plugged into your PC. Frame can install over Wi-Fi using SSH.')]:
ttk.Label(quest,text=title,style='Card.TLabel',font=('Segoe UI',11,'bold')).pack(anchor='w',pady=(8,4))
ttk.Label(quest,text=body,style='CardMuted.TLabel',wraplength=340).pack(anchor='w',pady=(0,9))
self.button(quest,'Browse Quest games →',self.browse_quest,True).pack(fill='x',pady=(13,0))
self.responsive_cards(columns,frame,quest)
self.button(page,'Already converted? Open a saved build…',self.open_build).pack(anchor='w',pady=(14,0))
def browse_quest(self):
self.tabs.select(1)
if not self.package_list:self.scan()
def quest_page(self):
page=self.pages[1]
self.heading(page,'Choose what comes with you.','Select one game to customize, or several to build a transfer queue.')
tools=ttk.Frame(page);tools.pack(fill='x',pady=(0,10))
ttk.Label(tools,text='Search',style='Muted.TLabel').pack(side='left',padx=(0,10))
ttk.Entry(tools,textvariable=self.vars['filter']).pack(side='left',fill='x',expand=True,padx=(0,10))
self.button(tools,'Scan Quest',self.scan).pack(side='right')
self.vars['filter'].trace_add('write',lambda *_:self.filter_packages())
ttk.Label(page,textvariable=self.library_count,style='Muted.TLabel').pack(anchor='w',pady=(0,8))
well=ttk.Frame(page,style='Card.TFrame');well.pack(fill='both',expand=True)
self.listbox=GameList(well)
scroll=ttk.Scrollbar(well,command=self.listbox.yview);scroll.pack(side='right',fill='y')
self.listbox.configure(yscrollcommand=scroll.set);self.listbox.pack(fill='both',expand=True,ipady=8)
self.listbox.bind('<<TreeviewSelect>>',self.library_selection_changed)
self.listbox.bind('<Double-Button-1>',lambda _:self.select_game())
self.listbox.bind('<Button-3>',self.game_details_menu)
self.listbox.bind('<Shift-F10>',self.game_details_menu)
ttk.Label(page,text='Ctrl / Shift-click for multiple games. Right-click for package details. Searching clears selection.',style='Muted.TLabel').pack(anchor='w',pady=(9,10))
actions=ttk.Frame(page);actions.pack(fill='x')
self.button(actions,'Select all',self.select_all).pack(side='left')
ttk.Label(actions,textvariable=self.selection_count,style='Muted.TLabel').pack(side='left',padx=15)
self.button(actions,'Review selection →',self.select_game,True).pack(side='right')
def select_all(self):
self.listbox.selection_set(0,'end');self.library_selection_changed()
def game_details_menu(self,event):
if getattr(event,'num',None)==3:
row=self.listbox.identify_row(event.y)
if not row:return
self.listbox.selection_set(self.listbox.index(row))
selected=self.listbox.curselection()
if not selected:return
package=self.visible_packages[selected[0]]
menu=tk.Menu(self,tearoff=False)
def copy():self.clipboard_clear();self.clipboard_append(package)
menu.add_command(label='Copy package ID',command=copy)
menu.add_command(label='App details…',command=lambda:messagebox.showinfo('App details',self.package_names.get(package,package)+'\n\n'+package,parent=self))
try:menu.tk_popup(event.x_root,event.y_root)
finally:menu.grab_release();menu.destroy()
def library_selection_changed(self,event=None):
count=len(self.listbox.curselection())
self.selection_count.set(f'{count} game'+('s' if count!=1 else '')+' selected')
def convert_page(self):
page=self.pages[2]
self.heading(page,'Make it feel at home.','Review your game and performance preferences before creating a Frame-ready copy.')
self.selected=tk.StringVar(value='No game selected · choose a Quest game or open a saved build.')
ttk.Label(page,textvariable=self.selected,style='Muted.TLabel',wraplength=830).pack(anchor='w',pady=(0,6))
columns=ttk.Frame(page);columns.pack(fill='x');columns.columnconfigure((0,1),weight=1,uniform='cards')
game=self.card(columns,'Library details');game.grid(row=0,column=0,sticky='nsew',padx=(0,8))
self.field(game,'title','Game title')
self.field(game,'steam_id','Steam App ID · optional artwork')
self.button(game,'Find artwork',self.search_artwork).pack(anchor='w',pady=9)
self.art_matches=ttk.Combobox(game,state='readonly',width=22);self.art_matches.pack(fill='x')
self.art_matches.set('Choose an artwork match')
self.art_matches.bind('<<ComboboxSelected>>',self.choose_artwork)
ttk.Checkbutton(game,text='Search Steam automatically',variable=self.auto_art).pack(anchor='w',pady=(8,0))
ttk.Label(game,text='Search sends the game title to Steam. Confirm the matching edition; artwork is optional.',style='CardMuted.TLabel',wraplength=345).pack(anchor='w',pady=(5,0))
performance=self.card(columns,'Performance & compatibility');performance.grid(row=0,column=1,sticky='nsew',padx=(8,0))
for name,title,values in [('scale','Resolution scale (%)',('50','67','75','85','100','125','150','175','200')),('refresh_rate','Headset refresh rate (Hz)',('Default','72','80','90','120','144')),('headset','Reported headset (experimental; rebuild required)',('Default','Quest 2','Quest 3'))]:
ttk.Label(performance,text=title,style='CardMuted.TLabel').pack(anchor='w',pady=(6,4))
ttk.Combobox(performance,textvariable=self.vars[name],values=values,state='readonly',width=15).pack(fill='x')
ttk.Label(performance,text='100% uses the runtime recommendation. Refresh rate is a request, not an FPS cap.',style='CardMuted.TLabel',wraplength=350).pack(anchor='w',pady=5)
ttk.Checkbutton(performance,text='Physical-controller compatibility',variable=self.controllers).pack(anchor='w')
ttk.Checkbutton(performance,text='Disable unsupported Quest foveation',variable=self.foveation).pack(anchor='w')
ttk.Checkbutton(performance,text='Fix black screen (swapchain rectangles; rebuild)',variable=self.rect_fix).pack(anchor='w')
self.responsive_cards(columns,game,performance)
actions=ttk.Frame(page);actions.pack(fill='x',pady=(16,0))
self.button(actions,'Open saved build…',self.open_build).pack(side='left')
self.button(actions,'Rebuild APK only',self.rebuild_apk).pack(side='left',padx=8)
self.button(actions,'Go to installation →',lambda:self.tabs.select(3)).pack(side='right',padx=(10,0))
self.button(actions,'Back up & convert',self.convert,True).pack(side='right')
def install_page(self):
page=self.pages[3]
self.heading(page,'Next stop: your Frame.','Install your converted game, then launch it directly from the Steam library.')
self.ready=tk.StringVar(value='No build loaded. Convert a Quest game or open a saved build first.')
ttk.Label(page,textvariable=self.ready,style='Muted.TLabel',wraplength=830).pack(anchor='w',pady=(0,14))
card=self.card(page,'Transfer & destination');card.pack(fill='x')
modes=ttk.Frame(card,style='Card.TFrame');modes.pack(fill='x')
ttk.Radiobutton(modes,text='Wi-Fi · SSH / SFTP',variable=self.wireless_install,value=True).pack(side='left',padx=(0,30))
ttk.Radiobutton(modes,text='USB · ADB + SSH',variable=self.wireless_install,value=False).pack(side='left')
ttk.Label(card,text='Both modes use your Frame connection details. Wi-Fi needs the same network; USB needs Frame connected to your PC.',style='CardMuted.TLabel',wraplength=800).pack(anchor='w',pady=(5,12))
self.storage_picker=StoragePicker(card,self);self.storage_picker.pack(fill='x',pady=(0,10))
ttk.Checkbutton(card,text='Add to Steam with artwork · briefly restarts Steam',variable=self.add_steam).pack(anchor='w')
ttk.Label(card,text='Keep Frame awake during Wi-Fi transfers. Quest2Frame does not change the device’s sleep or power settings.',style='CardMuted.TLabel',wraplength=790).pack(anchor='w',pady=(10,0))
actions=ttk.Frame(page);actions.pack(fill='x',pady=14)
self.button(actions,'Open saved build…',self.open_build).pack(side='left')
self.button(actions,'Install on Frame',self.install,True).pack(side='right')
maintenance=ttk.Frame(page);maintenance.pack(fill='x')
self.button(maintenance,'Apply settings only',self.update_settings).pack(side='left',padx=(0,8))
self.button(maintenance,'Repair Steam shortcut',self.repair_shortcut).pack(side='left')
self.button(maintenance,'Install APK only',self.install_apk_only).pack(side='right')
ttk.Label(page,text='Existing saves are preserved. Updates stay on the original storage device. Compatibility varies by game.',style='Muted.TLabel',wraplength=820).pack(anchor='w',pady=(13,0))
def navigation_changed(self,event=None):
index=self.tabs.index(self.tabs.select())
for i,button in enumerate(self.nav):button.configure(bg='#294b59' if i==index else NAV,fg='white' if i==index else '#bdccd5')
def page_wheel(self,event):
widget=event.widget
while widget is not None:
if isinstance(widget,ScrollPage):widget.wheel(event);break
widget=getattr(widget,'master',None)
def lock_actions(self,locked):
if locked:
self._locked=[]
def walk(widget):
for child in widget.winfo_children():
if isinstance(child,(ttk.Entry,ttk.Combobox,ttk.Checkbutton,ttk.Radiobutton,tk.Listbox)):
self._locked.append((child,str(child.cget('state'))));child.configure(state='disabled')
walk(child)
walk(self.tabs)
for button in self.action_buttons:
if button.winfo_exists():button.configure(state='disabled')
else:
for widget,state in self._locked:
if widget.winfo_exists():widget.configure(state=state)
self._locked=[]
for button in self.action_buttons:
if button.winfo_exists():button.configure(state='normal')
def show_log(self):
self.log_window.deiconify();self.log_window.lift()
def about(self):
messagebox.showinfo('Quest2Frame · About & licenses','Quest2Frame — native desktop edition\n\nOriginal source: GPL-3.0-only, without warranty. Dependencies, game content and artwork retain their own licenses.\n\n'+str(ROOT/'LICENSE')+'\n'+str(ROOT/'THIRD_PARTY_NOTICES.md'),parent=self)