import wx import wx.adv import wx.html import wx.html2 import subprocess import os import threading import queue import json import webbrowser import requests import sys def run_subprocess(cmd, output_queue): try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) for line in process.stdout: output_queue.put(line) process.wait() if process.returncode == 0: output_queue.put("Process completed successfully!") else: output_queue.put(f"Process failed with return code {process.returncode}") except Exception as e: output_queue.put(f"An error occurred: {str(e)}") def update_output(output_text, output_queue): try: while True: line = output_queue.get_nowait() if wx.Window.FindWindowById(output_text.GetId()): wx.CallAfter(output_text.AppendText, line) else: return # Exit if the text control no longer exists except queue.Empty: pass except RuntimeError: return # Exit if a RuntimeError occurs (e.g., window closed) wx.CallLater(100, update_output, output_text, output_queue) def open_store_folder(folder_path): if os.path.exists(folder_path): os.startfile(folder_path) else: wx.MessageBox(f"The folder {folder_path} does not exist.", "Error", wx.OK | wx.ICON_ERROR) class DarkThemedTextCtrl(wx.TextCtrl): def __init__(self, parent, id=wx.ID_ANY, value="", style=0): super().__init__(parent, id, value, style=style | wx.NO_BORDER) self.SetBackgroundColour(wx.Colour(0, 0, 0)) self.SetForegroundColour(wx.WHITE) class CollapsiblePanel(wx.Panel): def __init__(self, parent, title, *args, **kwargs): wx.Panel.__init__(self, parent, *args, **kwargs) self.SetBackgroundColour(parent.GetBackgroundColour()) self.toggle_button = wx.Button(self, label=title, style=wx.NO_BORDER) self.toggle_button.SetBackgroundColour(self.GetBackgroundColour()) self.toggle_button.Bind(wx.EVT_BUTTON, self.on_toggle) self.content_panel = wx.Panel(self) self.content_panel.SetBackgroundColour(self.GetBackgroundColour()) self.main_sizer = wx.BoxSizer(wx.VERTICAL) self.main_sizer.Add(self.toggle_button, 0, wx.EXPAND | wx.ALL, 5) self.main_sizer.Add(self.content_panel, 0, wx.EXPAND | wx.ALL, 5) self.SetSizer(self.main_sizer) self.collapsed = True self.toggle_button.SetLabel(f"▶ {title}") self.content_panel.Hide() def on_toggle(self, event): self.collapsed = not self.collapsed self.toggle_button.SetLabel(f"{'▶' if self.collapsed else '▼'} {self.toggle_button.GetLabel()[2:]}") self.content_panel.Show(not self.collapsed) self.Layout() self.GetParent().Layout() def get_content_panel(self): return self.content_panel class CustomToolTip(wx.PopupWindow): def __init__(self, parent, text): wx.PopupWindow.__init__(self, parent) # Main panel for tooltip panel = wx.Panel(self) self.st = wx.StaticText(panel, 1, text, pos=(10, 10)) font = wx.Font(8, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Poppins") self.st.SetFont(font) size = self.st.GetBestSize() self.SetSize((size.width + 20, size.height + 20)) # Adjust the panel size panel.SetSize(self.GetSize()) panel.SetBackgroundColour(wx.Colour(255, 255, 255)) # Bind paint event to draw border panel.Bind(wx.EVT_PAINT, self.on_paint) def on_paint(self, event): # Get the device context for the panel (not self) panel = event.GetEventObject() # Get the panel triggering the paint event dc = wx.PaintDC(panel) # Use panel as the target of the PaintDC dc.SetPen(wx.Pen(wx.Colour(210, 210, 210), 1)) # Border color dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255))) # Fill with white size = panel.GetSize() dc.DrawRectangle(0, 0, size.width, size.height) # Draw border around panel class MainFrame(wx.Frame): def __init__(self): super().__init__(parent=None, title="Music Source Separation Training & Inference GUI") self.SetSize(994, 670) self.SetBackgroundColour(wx.Colour(247, 248, 250)) # #F7F8FA icon = wx.Icon("gui/favicon.ico", wx.BITMAP_TYPE_ICO) self.SetIcon(icon) self.saved_combinations = {} # Center the window on the screen self.Center() # Set Poppins font for the entire application font_path = "gui/Poppins Regular 400.ttf" bold_font_path = "gui/Poppins Bold 700.ttf" wx.Font.AddPrivateFont(font_path) wx.Font.AddPrivateFont(bold_font_path) self.font = wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Poppins") self.bold_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, "Poppins") self.SetFont(self.font) panel = wx.Panel(self) main_sizer = wx.BoxSizer(wx.VERTICAL) # Add image (with error handling) try: img = wx.Image("gui/mvsep.png", wx.BITMAP_TYPE_PNG) img_bitmap = wx.Bitmap(img) img_ctrl = wx.StaticBitmap(panel, -1, img_bitmap) main_sizer.Add(img_ctrl, 0, wx.ALIGN_CENTER | wx.TOP, 20) except: print("Failed to load image: gui/mvsep.png") # Add title text title_text = wx.StaticText(panel, label="Music Source Separation Training && Inference GUI") title_text.SetFont(self.bold_font) title_text.SetForegroundColour(wx.BLACK) main_sizer.Add(title_text, 0, wx.ALIGN_CENTER | wx.TOP, 10) # Add subtitle text subtitle_text = wx.StaticText(panel, label="Code by ZFTurbo / GUI by Bas Curtiz") subtitle_text.SetForegroundColour(wx.BLACK) main_sizer.Add(subtitle_text, 0, wx.ALIGN_CENTER | wx.TOP, 5) # Add GitHub link github_link = wx.adv.HyperlinkCtrl(panel, -1, "GitHub Repository", "https://github.com/ZFTurbo/Music-Source-Separation-Training") github_link.SetNormalColour(wx.Colour(1, 118, 179)) # #0176B3 github_link.SetHoverColour(wx.Colour(86, 91, 123)) # #565B7B main_sizer.Add(github_link, 0, wx.ALIGN_CENTER | wx.TOP, 10) # Add Download models button on a new line with 10px bottom margin download_models_btn = self.create_styled_button(panel, "Download Models", self.on_download_models) main_sizer.Add(download_models_btn, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10) # Training Configuration self.training_panel = CollapsiblePanel(panel, "Training Configuration") self.training_panel.toggle_button.SetFont(self.bold_font) self.create_training_controls(self.training_panel.get_content_panel()) main_sizer.Add(self.training_panel, 0, wx.EXPAND | wx.ALL, 10) # Inference Configuration self.inference_panel = CollapsiblePanel(panel, "Inference Configuration") self.inference_panel.toggle_button.SetFont(self.bold_font) self.create_inference_controls(self.inference_panel.get_content_panel()) main_sizer.Add(self.inference_panel, 0, wx.EXPAND | wx.ALL, 10) panel.SetSizer(main_sizer) self.load_settings() def create_styled_button(self, parent, label, handler): btn = wx.Button(parent, label=label, style=wx.BORDER_NONE) btn.SetBackgroundColour(wx.Colour(1, 118, 179)) # #0176B3 btn.SetForegroundColour(wx.WHITE) btn.SetFont(self.bold_font) def on_enter(event): btn.SetBackgroundColour(wx.Colour(86, 91, 123)) event.Skip() def on_leave(event): btn.SetBackgroundColour(wx.Colour(1, 118, 179)) event.Skip() def on_click(event): btn.SetBackgroundColour(wx.Colour(86, 91, 123)) handler(event) wx.CallLater(100, lambda: btn.SetBackgroundColour(wx.Colour(1, 118, 179))) btn.Bind(wx.EVT_ENTER_WINDOW, on_enter) btn.Bind(wx.EVT_LEAVE_WINDOW, on_leave) btn.Bind(wx.EVT_BUTTON, on_click) return btn def create_training_controls(self, panel): sizer = wx.BoxSizer(wx.VERTICAL) # Model Type model_type_sizer = wx.BoxSizer(wx.HORIZONTAL) model_type_sizer.Add(wx.StaticText(panel, label="Model Type:"), 0, wx.ALIGN_CENTER_VERTICAL) self.model_type = wx.Choice(panel, choices=["apollo", "bandit", "bandit_v2", "bs_roformer", "htdemucs", "mdx23c", "mel_band_roformer", "scnet", "scnet_unofficial", "segm_models", "swin_upernet", "torchseg"]) self.model_type.SetFont(self.font) model_type_sizer.Add(self.model_type, 0, wx.LEFT, 5) sizer.Add(model_type_sizer, 0, wx.EXPAND | wx.ALL, 5) # Config File self.config_entry = self.add_browse_control(panel, sizer, "Config File:", is_folder=False, is_config=True) # Start Checkpoint self.checkpoint_entry = self.add_browse_control(panel, sizer, "Checkpoint:", is_folder=False, is_checkpoint=True) # Results Path self.result_path_entry = self.add_browse_control(panel, sizer, "Results Path:", is_folder=True) # Data Paths self.data_entry = self.add_browse_control(panel, sizer, "Data Paths (separated by ';'):", is_folder=True) # Validation Paths self.valid_entry = self.add_browse_control(panel, sizer, "Validation Paths (separated by ';'):", is_folder=True) # Number of Workers and Device IDs workers_device_sizer = wx.BoxSizer(wx.HORIZONTAL) workers_sizer = wx.BoxSizer(wx.HORIZONTAL) workers_sizer.Add(wx.StaticText(panel, label="Number of Workers:"), 0, wx.ALIGN_CENTER_VERTICAL) self.workers_entry = wx.TextCtrl(panel, value="4") self.workers_entry.SetFont(self.font) workers_sizer.Add(self.workers_entry, 0, wx.LEFT, 5) workers_device_sizer.Add(workers_sizer, 0, wx.EXPAND) device_sizer = wx.BoxSizer(wx.HORIZONTAL) device_sizer.Add(wx.StaticText(panel, label="Device IDs (comma-separated):"), 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) self.device_entry = wx.TextCtrl(panel, value="0") self.device_entry.SetFont(self.font) device_sizer.Add(self.device_entry, 0, wx.LEFT, 5) workers_device_sizer.Add(device_sizer, 0, wx.EXPAND) sizer.Add(workers_device_sizer, 0, wx.EXPAND | wx.ALL, 5) # Run Training Button self.run_button = self.create_styled_button(panel, "Run Training", self.run_training) sizer.Add(self.run_button, 0, wx.ALIGN_CENTER | wx.ALL, 10) panel.SetSizer(sizer) def create_inference_controls(self, panel): sizer = wx.BoxSizer(wx.VERTICAL) # Model Type and Saved Combinations infer_model_type_sizer = wx.BoxSizer(wx.HORIZONTAL) infer_model_type_sizer.Add(wx.StaticText(panel, label="Model Type:"), 0, wx.ALIGN_CENTER_VERTICAL) self.infer_model_type = wx.Choice(panel, choices=["apollo", "bandit", "bandit_v2", "bs_roformer", "htdemucs", "mdx23c", "mel_band_roformer", "scnet", "scnet_unofficial", "segm_models", "swin_upernet", "torchseg"]) self.infer_model_type.SetFont(self.font) infer_model_type_sizer.Add(self.infer_model_type, 0, wx.LEFT, 5) # Add "Preset:" label infer_model_type_sizer.Add(wx.StaticText(panel, label="Preset:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 20) # Add dropdown for saved combinations self.saved_combinations_dropdown = wx.Choice(panel, choices=[]) self.saved_combinations_dropdown.SetFont(self.font) self.saved_combinations_dropdown.Bind(wx.EVT_CHOICE, self.on_combination_selected) # Set the width to 200px and an appropriate height self.saved_combinations_dropdown.SetMinSize((358, -1)) # -1 keeps the height unchanged # Add to sizer infer_model_type_sizer.Add(self.saved_combinations_dropdown, 0, wx.LEFT, 5) # Add plus button plus_button = self.create_styled_button(panel, "+", self.on_save_combination) plus_button.SetMinSize((30, 30)) infer_model_type_sizer.Add(plus_button, 0, wx.LEFT, 5) # Add help button with custom tooltip help_button = wx.StaticText(panel, label="?") help_button.SetFont(self.bold_font) help_button.SetForegroundColour(wx.Colour(1, 118, 179)) #0176B3 tooltip_text = ("How to add a preset?\n\n" "1. Click Download Models\n" "2. Right-click a model's Config && Checkpoint\n" "3. Save link as && select a proper destination\n" "4. Copy the Model name\n" "5. Close Download Models\n\n" "6. Browse for the Config file\n" "7. Browse for the Checkpoint\n" "8. Select the Model Type\n" "9. Click the + button\n" "10. Paste the Model name && click OK\n\n" "On next use, just select it from the Preset dropdown.") self.tooltip = CustomToolTip(self, tooltip_text) self.tooltip.Hide() def on_help_enter(event): self.tooltip.Position(help_button.ClientToScreen((0, help_button.GetSize().height)), (0, 0)) self.tooltip.Show() def on_help_leave(event): self.tooltip.Hide() help_button.Bind(wx.EVT_ENTER_WINDOW, on_help_enter) help_button.Bind(wx.EVT_LEAVE_WINDOW, on_help_leave) infer_model_type_sizer.Add(help_button, 0, wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 5) sizer.Add(infer_model_type_sizer, 0, wx.EXPAND | wx.ALL, 5) # Config File self.infer_config_entry = self.add_browse_control(panel, sizer, "Config File:", is_folder=False, is_config=True) # Start Checkpoint self.infer_checkpoint_entry = self.add_browse_control(panel, sizer, "Checkpoint:", is_folder=False, is_checkpoint=True) # Input Folder self.infer_input_entry = self.add_browse_control(panel, sizer, "Input Folder:", is_folder=True) # Store Directory self.infer_store_entry = self.add_browse_control(panel, sizer, "Output Folder:", is_folder=True) # Extract Instrumental Checkbox self.extract_instrumental_checkbox = wx.CheckBox(panel, label="Extract Instrumental") self.extract_instrumental_checkbox.SetFont(self.font) sizer.Add(self.extract_instrumental_checkbox, 0, wx.EXPAND | wx.ALL, 5) # Run Inference Button self.run_infer_button = self.create_styled_button(panel, "Run Inference", self.run_inference) sizer.Add(self.run_infer_button, 0, wx.ALIGN_CENTER | wx.ALL, 10) panel.SetSizer(sizer) def add_browse_control(self, panel, sizer, label, is_folder=False, is_config=False, is_checkpoint=False): browse_sizer = wx.BoxSizer(wx.HORIZONTAL) browse_sizer.Add(wx.StaticText(panel, label=label), 0, wx.ALIGN_CENTER_VERTICAL) entry = wx.TextCtrl(panel) entry.SetFont(self.font) browse_sizer.Add(entry, 1, wx.EXPAND | wx.LEFT, 5) browse_button = self.create_styled_button(panel, "Browse", lambda event, entry=entry, is_folder=is_folder, is_config=is_config, is_checkpoint=is_checkpoint: self.browse(event, entry, is_folder, is_config, is_checkpoint)) browse_sizer.Add(browse_button, 0, wx.LEFT, 5) sizer.Add(browse_sizer, 0, wx.EXPAND | wx.ALL, 5) return entry def browse(self, event, entry, is_folder=False, is_config=False, is_checkpoint=False): if is_folder: dialog = wx.DirDialog(self, "Choose a directory", style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST) else: wildcard = "All files (*.*)|*.*" if is_config: wildcard = "YAML files (*.yaml)|*.yaml" elif is_checkpoint: wildcard = "Checkpoint files (*.bin;*.chpt;*.ckpt;*.th)|*.bin;*.chpt;*.ckpt;*.th" dialog = wx.FileDialog(self, "Choose a file", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, wildcard=wildcard) dialog.SetFont(self.font) if dialog.ShowModal() == wx.ID_OK: entry.SetValue(dialog.GetPath()) dialog.Destroy() def create_output_window(self, title, folder_path): output_frame = wx.Frame(self, title=title, style=wx.DEFAULT_FRAME_STYLE | wx.STAY_ON_TOP) output_frame.SetIcon(self.GetIcon()) output_frame.SetSize(994, 670) output_frame.SetBackgroundColour(wx.Colour(0, 0, 0)) output_frame.SetFont(self.font) # Set the position of the output frame to match the main frame output_frame.SetPosition(self.GetPosition()) output_title = wx.StaticText(output_frame, label=title) output_title.SetFont(self.bold_font) output_title.SetForegroundColour(wx.WHITE) output_text = DarkThemedTextCtrl(output_frame, style=wx.TE_MULTILINE | wx.TE_READONLY) output_text.SetFont(self.font) open_folder_button = self.create_styled_button(output_frame, f"Open Output Folder", lambda event: open_store_folder(folder_path)) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(output_title, 0, wx.ALIGN_CENTER | wx.TOP, 10) sizer.Add(output_text, 1, wx.EXPAND | wx.ALL, 10) sizer.Add(open_folder_button, 0, wx.ALIGN_CENTER | wx.BOTTOM, 10) output_frame.SetSizer(sizer) return output_frame, output_text def run_training(self, event): model_type = self.model_type.GetStringSelection() config_path = self.config_entry.GetValue() start_checkpoint = self.checkpoint_entry.GetValue() results_path = self.result_path_entry.GetValue() data_paths = self.data_entry.GetValue() valid_paths = self.valid_entry.GetValue() num_workers = self.workers_entry.GetValue() device_ids = self.device_entry.GetValue() if not model_type: wx.MessageBox("Please select a model type.", "Input Error", wx.OK | wx.ICON_ERROR) return if not config_path: wx.MessageBox("Please select a config file.", "Input Error", wx.OK | wx.ICON_ERROR) return if not results_path: wx.MessageBox("Please specify a results path.", "Input Error", wx.OK | wx.ICON_ERROR) return if not data_paths: wx.MessageBox("Please specify data paths.", "Input Error", wx.OK | wx.ICON_ERROR) return if not valid_paths: wx.MessageBox("Please specify validation paths.", "Input Error", wx.OK | wx.ICON_ERROR) return cmd = [ sys.executable, "train.py", "--model_type", model_type, "--config_path", config_path, "--results_path", results_path, "--data_path", *data_paths.split(';'), "--valid_path", *valid_paths.split(';'), "--num_workers", num_workers, "--device_ids", device_ids ] if start_checkpoint: cmd += ["--start_check_point", start_checkpoint] output_queue = queue.Queue() threading.Thread(target=run_subprocess, args=(cmd, output_queue), daemon=True).start() output_frame, output_text = self.create_output_window("Training Output", results_path) output_frame.Show() update_output(output_text, output_queue) self.save_settings() def run_inference(self, event): model_type = self.infer_model_type.GetStringSelection() config_path = self.infer_config_entry.GetValue() start_checkpoint = self.infer_checkpoint_entry.GetValue() input_folder = self.infer_input_entry.GetValue() store_dir = self.infer_store_entry.GetValue() extract_instrumental = self.extract_instrumental_checkbox.GetValue() if not model_type: wx.MessageBox("Please select a model type.", "Input Error", wx.OK | wx.ICON_ERROR) return if not config_path: wx.MessageBox("Please select a config file.", "Input Error", wx.OK | wx.ICON_ERROR) return if not input_folder: wx.MessageBox("Please specify an input folder.", "Input Error", wx.OK | wx.ICON_ERROR) return if not store_dir: wx.MessageBox("Please specify an output folder.", "Input Error", wx.OK | wx.ICON_ERROR) return cmd = [ sys.executable, "inference.py", "--model_type", model_type, "--config_path", config_path, "--input_folder", input_folder, "--store_dir", store_dir ] if start_checkpoint: cmd += ["--start_check_point", start_checkpoint] if extract_instrumental: cmd += ["--extract_instrumental"] output_queue = queue.Queue() threading.Thread(target=run_subprocess, args=(cmd, output_queue), daemon=True).start() output_frame, output_text = self.create_output_window("Inference Output", store_dir) output_frame.Show() update_output(output_text, output_queue) self.save_settings() def save_settings(self): settings = { "model_type": self.model_type.GetStringSelection(), "config_path": self.config_entry.GetValue(), "start_checkpoint": self.checkpoint_entry.GetValue(), "results_path": self.result_path_entry.GetValue(), "data_paths": self.data_entry.GetValue(), "valid_paths": self.valid_entry.GetValue(), "num_workers": self.workers_entry.GetValue(), "device_ids": self.device_entry.GetValue(), "infer_model_type": self.infer_model_type.GetStringSelection(), "infer_config_path": self.infer_config_entry.GetValue(), "infer_start_checkpoint": self.infer_checkpoint_entry.GetValue(), "infer_input_folder": self.infer_input_entry.GetValue(), "infer_store_dir": self.infer_store_entry.GetValue(), "extract_instrumental": self.extract_instrumental_checkbox.GetValue(), "saved_combinations": self.saved_combinations } with open("settings.json", "w") as f: json.dump(settings, f, indent=2, ensure_ascii=False) def load_settings(self): try: with open("settings.json", "r") as f: settings = json.load(f) self.model_type.SetStringSelection(settings.get("model_type", "")) self.config_entry.SetValue(settings.get("config_path", "")) self.checkpoint_entry.SetValue(settings.get("start_checkpoint", "")) self.result_path_entry.SetValue(settings.get("results_path", "")) self.data_entry.SetValue(settings.get("data_paths", "")) self.valid_entry.SetValue(settings.get("valid_paths", "")) self.workers_entry.SetValue(settings.get("num_workers", "4")) self.device_entry.SetValue(settings.get("device_ids", "0")) self.infer_model_type.SetStringSelection(settings.get("infer_model_type", "")) self.infer_config_entry.SetValue(settings.get("infer_config_path", "")) self.infer_checkpoint_entry.SetValue(settings.get("infer_start_checkpoint", "")) self.infer_input_entry.SetValue(settings.get("infer_input_folder", "")) self.infer_store_entry.SetValue(settings.get("infer_store_dir", "")) self.extract_instrumental_checkbox.SetValue(settings.get("extract_instrumental", False)) self.saved_combinations = settings.get("saved_combinations", {}) self.update_saved_combinations() except FileNotFoundError: pass # If the settings file doesn't exist, use default values def on_download_models(self, event): DownloadModelsFrame(self).Show() def on_save_combination(self, event): dialog = wx.TextEntryDialog(self, "Enter a name for this preset:", "Save Preset") if dialog.ShowModal() == wx.ID_OK: name = dialog.GetValue() if name: combination = { "model_type": self.infer_model_type.GetStringSelection(), "config_path": self.infer_config_entry.GetValue(), "checkpoint": self.infer_checkpoint_entry.GetValue() } self.saved_combinations[name] = combination self.update_saved_combinations() self.save_settings() dialog.Destroy() def on_combination_selected(self, event): name = self.saved_combinations_dropdown.GetStringSelection() if name: combination = self.saved_combinations.get(name) if combination: self.infer_model_type.SetStringSelection(combination["model_type"]) self.infer_config_entry.SetValue(combination["config_path"]) self.infer_checkpoint_entry.SetValue(combination["checkpoint"]) def update_saved_combinations(self): self.saved_combinations_dropdown.Clear() for name in self.saved_combinations.keys(): self.saved_combinations_dropdown.Append(name) class DownloadModelsFrame(wx.Frame): def __init__(self, parent): super().__init__(parent, title="Download Models", size=(994, 670), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)) self.SetBackgroundColour(wx.Colour(247, 248, 250)) # #F7F8FA self.SetFont(wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, "Poppins")) # Set the position of the Download Models frame to match the main frame self.SetPosition(parent.GetPosition()) # Set the icon for the Download Models frame icon = wx.Icon("gui/favicon.ico", wx.BITMAP_TYPE_ICO) self.SetIcon(icon) panel = wx.Panel(self) sizer = wx.BoxSizer(wx.VERTICAL) # Add WebView self.web_view = wx.html2.WebView.New(panel) self.web_view.LoadURL("https://bascurtiz.x10.mx/models-checkpoint-config-urls.html") self.web_view.Bind(wx.html2.EVT_WEBVIEW_NAVIGATING, self.on_link_click) self.web_view.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.on_page_load) sizer.Add(self.web_view, 1, wx.EXPAND) panel.SetSizer(sizer) def on_link_click(self, event): url = event.GetURL() if not url.startswith("https://bascurtiz.x10.mx"): event.Veto() # Prevent the WebView from navigating webbrowser.open(url) # Open the link in the default browser def on_page_load(self, event): self.inject_custom_css() def inject_custom_css(self): css = """ body { margin: 0; padding: 0; } ::-webkit-scrollbar { width: 12px; } ::-webkit-scrollbar-track { background: #f1f1f1; } ::-webkit-scrollbar-thumb { background: #888; } ::-webkit-scrollbar-thumb:hover { background: #555; } """ js = f"var style = document.createElement('style'); style.textContent = `{css}`; document.head.appendChild(style);" self.web_view.RunScript(js) if __name__ == "__main__": app = wx.App() frame = MainFrame() frame.Show() app.MainLoop()