Skip to content

Commit f734c57

Browse files
committed
Multistep win installer
1 parent da688f8 commit f734c57

1 file changed

Lines changed: 164 additions & 8 deletions

File tree

scripts/build_installers.py

Lines changed: 164 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,13 @@ def create_windows_exe(cli_binary: Path, version: str, python_bin: str = "python
150150
installer_entry.write_text(
151151
"""from __future__ import annotations
152152
153+
import os
153154
import shutil
154155
import sys
156+
import threading
155157
from pathlib import Path
158+
import tkinter as tk
159+
from tkinter import messagebox, ttk
156160
157161
158162
def resolve_payload_root() -> Path:
@@ -161,14 +165,17 @@ def resolve_payload_root() -> Path:
161165
return Path(__file__).resolve().parent / 'payload'
162166
163167
164-
def main() -> int:
165-
payload = resolve_payload_root()
166-
168+
def default_resolve_root() -> Path:
167169
appdata = Path.home() / 'AppData' / 'Roaming'
168-
if 'APPDATA' in __import__('os').environ:
169-
appdata = Path(__import__('os').environ['APPDATA'])
170+
if 'APPDATA' in os.environ:
171+
appdata = Path(os.environ['APPDATA'])
172+
return appdata / 'Blackmagic Design' / 'DaVinci Resolve' / 'Support' / 'Fusion'
173+
174+
175+
def install_openbeat(destination: Path) -> None:
176+
payload = resolve_payload_root()
177+
resolve_root = destination.expanduser()
170178
171-
resolve_root = appdata / 'Blackmagic Design' / 'DaVinci Resolve' / 'Support' / 'Fusion'
172179
utility_target = resolve_root / 'Scripts' / 'Utility' / 'OpenBeat'
173180
module_target = resolve_root / 'Modules' / 'OpenBeat'
174181
@@ -189,8 +196,156 @@ def main() -> int:
189196
config_text = f'return {{\\n python_bin = "{exe_path}",\\n}}\\n'
190197
(module_target / 'OpenBeatConfig.local.lua').write_text(config_text, encoding='utf-8')
191198
192-
print(f'OpenBeat installed to: {resolve_root}')
193-
print('Restart Resolve if it is open.')
199+
200+
class InstallerWizard(tk.Tk):
201+
def __init__(self) -> None:
202+
super().__init__()
203+
self.title('OpenBeat Setup')
204+
self.geometry('620x420')
205+
self.minsize(560, 380)
206+
self.resizable(False, False)
207+
self.protocol('WM_DELETE_WINDOW', self.cancel)
208+
209+
self.step = 0
210+
self.install_error: str | None = None
211+
212+
self.columnconfigure(0, weight=1)
213+
self.rowconfigure(0, weight=1)
214+
self.configure(background='#f6f7f9')
215+
216+
self.content = ttk.Frame(self, padding=(28, 24, 28, 18))
217+
self.content.grid(row=0, column=0, sticky='nsew')
218+
self.content.columnconfigure(0, weight=1)
219+
self.content.rowconfigure(5, weight=1)
220+
221+
self.nav = ttk.Frame(self, padding=(18, 12))
222+
self.nav.grid(row=1, column=0, sticky='ew')
223+
self.nav.columnconfigure(0, weight=1)
224+
225+
self.back_button = ttk.Button(self.nav, text='< Back', command=self.go_back)
226+
self.next_button = ttk.Button(self.nav, text='Next >', command=self.go_next)
227+
self.cancel_button = ttk.Button(self.nav, text='Cancel', command=self.cancel)
228+
self.back_button.grid(row=0, column=1, padx=(0, 8))
229+
self.next_button.grid(row=0, column=2, padx=(0, 8))
230+
self.cancel_button.grid(row=0, column=3)
231+
232+
self.frames = [
233+
self.welcome_step,
234+
self.ready_step,
235+
self.installing_step,
236+
self.finish_step,
237+
]
238+
self.show_step(0)
239+
240+
def clear_content(self) -> None:
241+
for child in self.content.winfo_children():
242+
child.destroy()
243+
244+
def add_heading(self, title: str, body: str) -> None:
245+
ttk.Label(self.content, text=title, font=('Segoe UI', 18, 'bold')).grid(
246+
row=0, column=0, sticky='w', pady=(0, 12)
247+
)
248+
ttk.Label(self.content, text=body, wraplength=520, justify='left').grid(
249+
row=1, column=0, sticky='nw'
250+
)
251+
252+
def show_step(self, step: int) -> None:
253+
self.step = step
254+
self.clear_content()
255+
self.frames[step]()
256+
257+
self.back_button.configure(state='normal' if step == 1 else 'disabled')
258+
self.cancel_button.configure(state='normal' if step not in (2, 3) else 'disabled')
259+
260+
if step == 1:
261+
self.next_button.configure(text='Install', state='normal')
262+
elif step == 2:
263+
self.next_button.configure(text='Next >', state='disabled')
264+
elif step == 3:
265+
self.next_button.configure(text='Finish', state='normal')
266+
else:
267+
self.next_button.configure(text='Next >', state='normal')
268+
269+
def welcome_step(self) -> None:
270+
self.add_heading(
271+
'Welcome to OpenBeat Setup',
272+
'This wizard will install the OpenBeat scripts and bundled runtime for DaVinci Resolve. Close Resolve before continuing if it is currently open.',
273+
)
274+
275+
def ready_step(self) -> None:
276+
self.add_heading(
277+
'Ready to Install',
278+
'Click Install to copy OpenBeat into the standard DaVinci Resolve Fusion support folder and configure the bundled runtime.',
279+
)
280+
summary = ttk.LabelFrame(self.content, text='Install summary', padding=14)
281+
summary.grid(row=2, column=0, sticky='ew', pady=(24, 0))
282+
summary.columnconfigure(1, weight=1)
283+
ttk.Label(summary, text='Destination').grid(row=0, column=0, sticky='nw', padx=(0, 12))
284+
ttk.Label(summary, text=str(default_resolve_root()), wraplength=400).grid(row=0, column=1, sticky='w')
285+
286+
def installing_step(self) -> None:
287+
self.add_heading('Installing OpenBeat', 'Please wait while setup copies files into Resolve.')
288+
self.progress = ttk.Progressbar(self.content, mode='indeterminate')
289+
self.progress.grid(row=2, column=0, sticky='ew', pady=(24, 8))
290+
self.status = ttk.Label(self.content, text='Preparing install...')
291+
self.status.grid(row=3, column=0, sticky='w')
292+
self.progress.start(12)
293+
294+
def finish_step(self) -> None:
295+
if self.install_error:
296+
self.add_heading(
297+
'Installation Did Not Complete',
298+
f'OpenBeat could not be installed.\\n\\n{self.install_error}',
299+
)
300+
return
301+
302+
self.add_heading(
303+
'OpenBeat Setup Complete',
304+
'OpenBeat has been installed successfully. Restart DaVinci Resolve if it is open, then use Workspace > Scripts > OpenBeat.',
305+
)
306+
307+
def go_back(self) -> None:
308+
if self.step > 0:
309+
self.show_step(self.step - 1)
310+
311+
def go_next(self) -> None:
312+
if self.step == 3:
313+
self.destroy()
314+
return
315+
if self.step == 1:
316+
self.start_install()
317+
return
318+
self.show_step(self.step + 1)
319+
320+
def cancel(self) -> None:
321+
if self.step == 2:
322+
messagebox.showinfo('OpenBeat Setup', 'Setup is currently installing OpenBeat.')
323+
return
324+
if messagebox.askyesno('Cancel Setup', 'Are you sure you want to cancel OpenBeat Setup?'):
325+
self.destroy()
326+
327+
def start_install(self) -> None:
328+
self.install_error = None
329+
destination = default_resolve_root()
330+
self.show_step(2)
331+
332+
def worker() -> None:
333+
try:
334+
install_openbeat(destination)
335+
except Exception as exc:
336+
self.install_error = str(exc)
337+
self.after(0, self.install_finished)
338+
339+
threading.Thread(target=worker, daemon=True).start()
340+
341+
def install_finished(self) -> None:
342+
if hasattr(self, 'progress'):
343+
self.progress.stop()
344+
self.show_step(3)
345+
346+
347+
def main() -> int:
348+
InstallerWizard().mainloop()
194349
return 0
195350
196351
@@ -207,6 +362,7 @@ def main() -> int:
207362
"--clean",
208363
"--noconfirm",
209364
"--onefile",
365+
"--windowed",
210366
"--name",
211367
"openbeat-installer",
212368
"--add-data",

0 commit comments

Comments
 (0)