import os import sys _WORKSPACE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _WORKSPACE_ROOT not in sys.path: sys.path.insert(0, _WORKSPACE_ROOT) import logging import traceback from PyQt6.QtWidgets import ( QApplication, QMainWindow, QStackedWidget, QLabel, QPushButton, QDialog, QVBoxLayout, QHBoxLayout, QMessageBox ) from PyQt6.QtGui import QPixmap, QFont, QFontDatabase from PyQt6 import QtGui from PyQt6.QtCore import Qt, QTimer, QEvent from core.Constants import Constants from core.infrastructure.service_registry import ServiceLocator from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ProfileController import ProfileController from core.Errors import UnknownConnectionTypeError from core.errors.logger import logger as core_logger core_logger.propagate = False from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool mainthread = ThreadSafetyTool() from gui.v2.infrastructure import orm from gui.v2.infrastructure.navigator import Navigator from gui.v2.infrastructure.setup_observers import setup_observers from gui.v2.infrastructure.connection_manager import ConnectionManager from gui.v2.infrastructure.screen_size import calculate_max_screensize from gui.v2.workers.worker_thread import WorkerThread from gui.v2.ui.builders.bottom_section import create_bottom_section from gui.v2.ui.builders.top_section import create_top_section from gui.v2.ui.builders.system_tray import init_system_tray from gui.v2.actions.config import ( setup_gui_directory, load_gui_config, save_gui_config, default_gui_config, ) from gui.v2.actions.logging_setup import ( is_logging_enabled, setup_gui_logging, teardown_gui_logging, ) from gui.v2.actions.flags import ( has_shown_systemwide_prompt, mark_systemwide_prompt_shown, has_shown_fast_mode_prompt, mark_fast_mode_prompt_shown, set_fast_mode_enabled, check_first_launch, ) from gui.v2.actions.profile_data import ( read_profile_data, write_profile_data, clear_profile_data, ) from gui.v2.actions.database_health import GuiStorageDatabaseError from gui.v2.actions.profile_status import is_profile_enabled_for_gui from gui.v2.actions.ticket_failure import ( save_ticket_verification_failure, get_ticket_verification_failure, clear_ticket_verification_failure, ) from gui.v2.actions.should_be_synchronized import should_be_synchronized class CustomWindow(QMainWindow): def __init__(self, force_sync=False): super().__init__() sys.excepthook = self._handle_exception self.setWindowFlags(Qt.WindowType.Window) gui_dir = os.path.dirname(os.path.abspath(__file__)) font_path = os.path.join(gui_dir, 'resources', 'fonts') retro_gaming_path = os.path.join(font_path, 'retro-gaming.ttf') font_id = QFontDatabase.addApplicationFont(retro_gaming_path) font_family = QFontDatabase.applicationFontFamilies(font_id)[0] app_font = QFont(font_family) app_font.setPointSize(10) QApplication.setFont(app_font) self.open_sans_family = None open_sans_path = os.path.join(font_path, 'open-sans.ttf') if os.path.exists(open_sans_path): open_sans_id = QFontDatabase.addApplicationFont(open_sans_path) if open_sans_id != -1: self.open_sans_family = QFontDatabase.applicationFontFamilies(open_sans_id)[0] self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground) self.setWindowTitle('HydraVeil') screen = QApplication.primaryScreen() ratio = screen.devicePixelRatio() self.host_screen_width = int(screen.size().width() * ratio) self.host_screen_height = int(screen.size().height() * ratio) self._data = {"Profile_1": {}} self.gui_config_home = os.path.join(Constants.HV_CONFIG_HOME, 'gui') self.gui_config_file = os.path.join(self.gui_config_home, 'config.json') self.gui_cache_home = os.path.join(Constants.HV_CACHE_HOME, 'gui') self.gui_log_file = os.path.join(self.gui_cache_home, 'gui.log') self._gui_log_handlers = [] setup_gui_directory( self.gui_cache_home, self.gui_config_home, self.gui_config_file) self.btn_path = os.getenv('BTN_PATH', os.path.join( gui_dir, 'resources', 'images')) self.css_path = os.getenv('CSS_PATH', os.path.join( gui_dir, 'resources', 'styles')) self.force_sync = force_sync self.is_downloading = False self.current_profile_id = None self.connection_manager = ConnectionManager() current_connection = None self.is_tor_mode = current_connection == 'tor' self.is_animating = False self.animation_step = 0 self.page_history = [] self.log_path = None self._close_confirmation_pending = False self._close_disconnect_in_progress = False self._closing_after_disconnect = False self.setFixedSize(800, 570) top = create_top_section(self, self.css_path) self.__dict__.update(top) self.marquee_text = "" self.marquee_position = 0 self.marquee_enabled = False self.scroll_speed = 1 self.page_stack = QStackedWidget(self) self.page_stack.setGeometry(0, 0, 800, 570) bottom = create_bottom_section( self, self.btn_path, Constants.HV_CLIENT_VERSION_NUMBER, should_be_synchronized(), ) self.__dict__.update(bottom) self.status_label = bottom['status_label'] self.tor_text = bottom['tor_text'] self.toggle_button = bottom['toggle_button'] self.tor_icon = bottom['tor_icon'] self.sync_button = bottom['sync_button'] self.page_stack.raise_() self.observers = setup_observers(self.update_status) self.update_image() self.set_scroll_speed(7) self.toggle_button.clicked.connect(self.toggle_mode) self.sync_button.clicked.connect(self.sync) QApplication.instance().installEventFilter(self) self.create_toggle(self.is_tor_mode) self.animation_timer = QTimer() self.animation_timer.timeout.connect(self.animate_toggle) self.check_logging() self.navigator = Navigator(self) ServiceLocator.register(name="navigator", service=self.navigator) self.page_stack.currentChanged.connect(self.page_changed) self.show() self.init_ui() self.tray = None self.tray = init_system_tray(self, self.btn_path) current_connection = self.get_current_connection() self.is_tor_mode = current_connection == 'tor' self.set_toggle_state(self.is_tor_mode) core_logger.info( f"HydraVeil application opened (client version {Constants.HV_CLIENT_VERSION_NUMBER}, " f"connection={current_connection}, tor_mode={self.is_tor_mode})" ) def init_ui(self): if self.check_first_launch(): self.navigator.navigate("welcome") else: self.navigator.navigate("menu") self.navigator.start_preload() if self.force_sync: QTimer.singleShot(0, self._prompt_force_sync) def _prompt_force_sync(self): """ Popup to encourage remote database sync Context: Database loader in __main__ passes in the 'forced_sync' variable, If true, this popup encourages sync to avoid NoneType errors. """ connection = ConfigurationController.get_connection() box = QMessageBox(self) box.setWindowTitle("Data Update Needed") box.setText( "Please download the new data to avoid errors. You have profiles that need the data to function.\n\n" f"Connection type: {connection}") # Apply styling # import it inside the function to reduce load on non-display flows. from gui.v2.ui.styles.css.main_ui_css import forced_sync_popup_style stylesheet = forced_sync_popup_style box.setStyleSheet(stylesheet) # Add styled buttons sync_button = box.addButton("Ok Fetch", QMessageBox.ButtonRole.AcceptRole) sync_button.setCursor(Qt.CursorShape.PointingHandCursor) cancel_button = box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) cancel_button.setCursor(Qt.CursorShape.PointingHandCursor) box.setMinimumWidth(450) # button size box.exec() if box.clickedButton() == sync_button: self.sync() def perform_update_check(self): from core.controllers.ClientController import ClientController update_available = ClientController.can_be_updated() if update_available: self.navigator.update_if_cached( "menu", lambda p: p.on_update_check_finished()) def update_values(self, available_locations, available_browsers, status, is_tor, locations, all_browsers): if not status: self.update_status('Sync failed. Please try again later.') return from PyQt6.QtWidgets import QPushButton from gui.v2.actions.sync import generate_grid_positions self.connection_manager.set_synced(True) self.connection_manager.store_locations(locations) self.connection_manager.store_browsers(available_browsers) location_positions = generate_grid_positions(len(available_locations)) browser_positions = generate_grid_positions(len(available_browsers)) available_locations_list = [ (QPushButton, loc, location_positions[i]) for i, loc in enumerate(available_locations) ] available_browsers_list = [ (QPushButton, brw, browser_positions[i]) for i, brw in enumerate(available_browsers) ] self.navigator.update_if_cached( "browser", lambda p: p.create_interface_elements(available_browsers_list)) self.navigator.update_if_cached( "location", lambda p: p.create_interface_elements(available_locations_list)) self.navigator.update_if_cached( "hidetor", lambda p: p.create_interface_elements(available_locations_list)) self.navigator.update_if_cached( "protocol", lambda p: p.enable_protocol_buttons()) self.navigator.update_if_cached("menu", self._enable_menu_verification_icons) def _enable_menu_verification_icons(self, menu_page): if hasattr(menu_page, 'refresh_profiles_data'): menu_page.refresh_profiles_data() if hasattr(menu_page, 'buttons'): for button in menu_page.buttons: parent = button.parent() if parent: verification_icons = parent.findChildren(QPushButton) for icon in verification_icons: if icon.geometry().width() == 20 and icon.geometry().height() == 20: icon.setEnabled(True) def sync(self): core_logger.info("User clicked sync button") if ConfigurationController.get_connection() == 'tor': self.worker_thread = WorkerThread('SYNC_TOR') else: self.worker_thread = WorkerThread('SYNC') self.sync_button.setIcon(QtGui.QIcon( os.path.join(self.btn_path, 'icon_sync.png'))) self.worker_thread.finished.connect( lambda: self.perform_update_check()) self.worker_thread.sync_output.connect(self.update_values) self.worker_thread.start() def _handle_exception(self, identifier, message, trace): if issubclass(identifier, UnknownConnectionTypeError): self.setup_popup() os.execv(sys.executable, [sys.executable] + sys.argv) elif issubclass(identifier, GuiStorageDatabaseError): core_logger.error( f"Storage database error:\n" f"Type: {identifier.__name__}\n" f"Value: {str(message)}\n" f"Traceback:\n{''.join(traceback.format_tb(trace))}" ) QMessageBox.critical( self, "Database Error", "The local storage database could not be read. Restart HydraVeil and use the database recovery options.", ) else: config = self._load_gui_config() if config and config["logging"]["gui_logging_enabled"] == True: logging.error( f"Uncaught exception:\n" f"Type: {identifier.__name__}\n" f"Value: {str(message)}\n" f"Traceback:\n{''.join(traceback.format_tb(trace))}" ) sys.__excepthook__(identifier, message, trace) def get_current_connection(self): return ConfigurationController.get_connection() def setup_popup(self): connection_dialog = QDialog(self) connection_dialog.setWindowTitle("Connection Type") connection_dialog.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint) connection_dialog.setModal(True) connection_dialog.setFixedSize(400, 200) layout = QVBoxLayout(connection_dialog) label = QLabel( "Please set your preference for connecting to Simplified Privacy's billing server.") label.setWordWrap(True) label.setAlignment(Qt.AlignmentFlag.AlignCenter) label.setStyleSheet( "font-size: 12px; margin-bottom: 20px; color: black;") button_layout = QHBoxLayout() regular_btn = QPushButton("Regular") tor_btn = QPushButton("Tor") button_style = """ QPushButton { background-color: #2b2b2b; color: white; border: none; padding: 10px 20px; border-radius: 5px; font-size: 12px; } QPushButton:hover { background-color: #3b3b3b; } QPushButton:pressed { background-color: #1b1b1b; } """ regular_btn.setStyleSheet(button_style) tor_btn.setStyleSheet(button_style) button_layout.addWidget(regular_btn) button_layout.addWidget(tor_btn) layout.addWidget(label) layout.addLayout(button_layout) def set_regular_connection(): ConfigurationController.set_connection('system') self.is_tor_mode = False self.set_toggle_state(False) core_logger.info("User selected 'Regular' connection type from popup") connection_dialog.accept() def set_tor_connection(): ConfigurationController.set_connection('tor') self.is_tor_mode = True self.set_toggle_state(True) core_logger.info("User selected 'Tor' connection type from popup") connection_dialog.accept() regular_btn.clicked.connect(set_regular_connection) tor_btn.clicked.connect(set_tor_connection) connection_dialog.exec() return ConfigurationController.get_connection() def _setup_gui_directory(self): setup_gui_directory( self.gui_cache_home, self.gui_config_home, self.gui_config_file) def _load_gui_config(self): return load_gui_config(self.gui_config_file) def _save_gui_config(self, config): save_gui_config(self.gui_config_file, config) def _default_gui_config(self): return default_gui_config() def save_ticket_verification_failure(self, result): return save_ticket_verification_failure(self.gui_config_file, result) def get_ticket_verification_failure(self): return get_ticket_verification_failure(self.gui_config_file) def clear_ticket_verification_failure(self): clear_ticket_verification_failure(self.gui_config_file) def check_logging(self): config = self._load_gui_config() if is_logging_enabled(config): self._setup_gui_logging() def has_shown_systemwide_prompt(self): return has_shown_systemwide_prompt(self.gui_config_file) def mark_systemwide_prompt_shown(self): mark_systemwide_prompt_shown(self.gui_config_file) def stop_gui_logging(self): teardown_gui_logging(self.gui_log_file, self._gui_log_handlers) self._gui_log_handlers = [] config = self._load_gui_config() if config: config["logging"]["gui_logging_enabled"] = False self._save_gui_config(config) def _setup_gui_logging(self): self._gui_log_handlers = setup_gui_logging( self.gui_cache_home, self.gui_log_file, self._gui_log_handlers) def set_tor_icon(self): if self.is_tor_mode: icon_path = os.path.join(self.btn_path, "toricon_mini.png") else: icon_path = os.path.join(self.btn_path, "toricon_mini_false.png") pixmap = QPixmap(icon_path) self.tor_icon.setPixmap(pixmap) self.tor_icon.setScaledContents(True) def create_toggle(self, is_tor_mode): handle_x = 30 if is_tor_mode else 3 colors = self._get_toggle_colors(is_tor_mode) if not hasattr(self, 'animation_timer'): self.animation_timer = QTimer() self.animation_timer.timeout.connect(self.animate_toggle) self.toggle_bg = QLabel(self.toggle_button) self.toggle_bg.setGeometry(0, 0, 50, 22) self.toggle_handle = QLabel(self.toggle_button) self.toggle_handle.setGeometry(handle_x, 3, 16, 16) self.toggle_handle.setStyleSheet(""" QLabel { background-color: white; border-radius: 8px; } """) self.on_label = QLabel("ON", self.toggle_button) self.on_label.setGeometry(8, 4, 15, 14) self.off_label = QLabel("OFF", self.toggle_button) self.off_label.setGeometry(25, 4, 40, 14) for widget in (self.toggle_bg, self.toggle_handle, self.on_label, self.off_label): widget.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) self._update_toggle_colors(colors) if is_tor_mode: self.set_tor_icon() self.raise_bottom_controls() def _get_toggle_colors(self, is_tor_mode): return { 'bg': "#00a8ff" if is_tor_mode else "#2c3e50", 'tor': "white" if is_tor_mode else "transparent", 'sys': "transparent" if is_tor_mode else "white" } def _update_toggle_colors(self, colors): self.toggle_bg.setStyleSheet(f""" QLabel {{ background-color: {colors['bg']}; border-radius: 11px; }} """) self.on_label.setStyleSheet(f""" QLabel {{ color: {colors['tor']}; font-size: 9px; font-weight: bold; }} """) self.off_label.setStyleSheet(f""" QLabel {{ color: {colors['sys']}; font-size: 9px; font-weight: bold; }} """) def set_toggle_state(self, is_tor_mode): self.is_tor_mode = is_tor_mode self.toggle_button.setChecked(is_tor_mode) handle_x = 30 if is_tor_mode else 3 self.toggle_handle.setGeometry(handle_x, 3, 16, 16) self._update_toggle_colors(self._get_toggle_colors(is_tor_mode)) self.set_tor_icon() def toggle_mode(self, *_args): if self.is_animating and not self.animation_timer.isActive(): self.is_animating = False if not self.is_animating: target_is_tor = not self.is_tor_mode new_connection = 'tor' if target_is_tor else 'system' core_logger.info(f"Tor toggle clicked; switching connection mode to '{new_connection}'") self.update_status(f"Tor toggle clicked: switching to {new_connection}") try: ConfigurationController.set_connection(new_connection) except Exception as error: core_logger.exception( f"Tor toggle failed while switching connection mode to '{new_connection}'") self.update_status(f"Tor toggle failed: {error}") self.set_toggle_state(self.is_tor_mode) return self.set_toggle_state(target_is_tor) self.is_animating = True self.animation_step = 0 self.animation_timer.start(16) return core_logger.info("Tor toggle clicked while animation was still running") self.update_status("Tor toggle clicked: still switching") def raise_bottom_controls(self): for widget in (self.status_label, self.tor_text, self.toggle_button, self.tor_icon, self.sync_button): widget.raise_() def eventFilter(self, source, event): if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton: if self._is_toggle_event(event): self.toggle_mode() return True return super().eventFilter(source, event) def _is_toggle_event(self, event): if not hasattr(self, 'toggle_button') or not self.toggle_button.isVisible(): return False if hasattr(event, 'globalPosition'): global_pos = event.globalPosition().toPoint() else: global_pos = event.globalPos() for widget in (self.tor_text, self.toggle_button, self.tor_icon): if widget.isVisible() and widget.rect().contains(widget.mapFromGlobal(global_pos)): return True return False def animate_toggle(self): TOTAL_STEPS = 5 if self.animation_step <= TOTAL_STEPS: progress = self.animation_step / TOTAL_STEPS if self.is_tor_mode: new_x = 20 + (10 * progress) start_color = "#2c3e50" end_color = "#00a8ff" else: new_x = 31 - (28 * progress) start_color = "#00a8ff" end_color = "#2c3e50" self.toggle_handle.setGeometry(int(new_x), 3, 16, 16) bg_color = self.interpolate_color(start_color, end_color, progress) self.toggle_bg.setStyleSheet(f""" QLabel {{ background-color: {bg_color}; border-radius: 11px; }} """) self.off_label.setStyleSheet(f""" QLabel {{ color: rgba(255, 255, 255, {1 - progress if self.is_tor_mode else progress}); font-size: 9px; font-weight: bold; }} """) self.on_label.setStyleSheet(f""" QLabel {{ color: rgba(255, 255, 255, {progress if self.is_tor_mode else 1 - progress}); font-size: 9px; font-weight: bold; }} """) self.animation_step += 1 else: self.animation_timer.stop() self.is_animating = False self.set_tor_icon() def interpolate_color(self, start_color, end_color, progress): def hex_to_rgb(hex_color): hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) start_rgb = hex_to_rgb(start_color) end_rgb = hex_to_rgb(end_color) current_rgb = tuple( int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * progress) for i in range(3) ) return f"rgb{current_rgb}" def closeEvent(self, event=None): core_logger.info("HydraVeil application closing") if self._closing_after_disconnect: orm.stop() if event is not None: event.accept() return if self._close_confirmation_pending or self._close_disconnect_in_progress: if event is not None: event.ignore() return connected_profiles = self._get_enabled_profile_ids() if connected_profiles: if event is not None: event.ignore() self._show_close_disconnect_confirmation(connected_profiles) return orm.stop() if event is not None: event.accept() def _get_enabled_profile_ids(self): try: profiles = ProfileController.get_all() except Exception: return self.connection_manager.get_connected_profiles() connected_profiles = [] self.connection_manager._connected_profiles.clear() for profile_id, profile in profiles.items(): try: if is_profile_enabled_for_gui(profile): connected_profiles.append(profile_id) self.connection_manager.add_connected_profile(profile_id) except Exception: pass return connected_profiles def _show_close_disconnect_confirmation(self, connected_profiles): from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup self._close_confirmation_pending = True message = self._create_close_disconnect_message(connected_profiles) self.popup = ConfirmationPopup( self, message=message, action_button_text="Exit", cancel_button_text="Cancel") self.popup.setWindowModality(Qt.WindowModality.ApplicationModal) self.popup.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.popup.finished.connect( lambda result: self._handle_close_disconnect_result(result, connected_profiles)) self.popup.destroyed.connect( lambda *_: self._handle_close_disconnect_popup_closed()) self.popup.show() def _create_close_disconnect_message(self, connected_profiles): profile_numbers = ', '.join(str(profile_id) for profile_id in connected_profiles) is_are = "is" if len(connected_profiles) == 1 else "are" return f'Profile{"" if len(connected_profiles) == 1 else "s"} {profile_numbers} {is_are} still connected.\nAll connected profiles will be disconnected on exit.\nDo you want to proceed?' def _handle_close_disconnect_result(self, result, connected_profiles): self._close_confirmation_pending = False if not result: return self._close_disconnect_in_progress = True self.update_status('Disabling profiles...') self.worker_thread = WorkerThread( 'DISABLE_ALL_PROFILES', profile_data=connected_profiles) self.worker_thread.text_output.connect(self.update_status) self.worker_thread.finished.connect(self._finish_close_disconnect) self.worker_thread.start() def _handle_close_disconnect_popup_closed(self): if self._close_confirmation_pending: self._close_confirmation_pending = False self.popup = None def _finish_close_disconnect(self, _ok=True): self._close_disconnect_in_progress = False connected_profiles = self._get_enabled_profile_ids() if connected_profiles: self.update_status('Could not disconnect all profiles. Exit canceled.') return self.update_status('All profiles disabled. Bye!') self._closing_after_disconnect = True QTimer.singleShot(0, self.close) def update_image(self, appearance_value="original"): image_path = os.path.join(self.btn_path, f"{appearance_value}.png") pixmap = QPixmap(image_path) self.label.setPixmap(pixmap) self.label.setScaledContents(True) def read_data(self): return read_profile_data(self._data) def write_data(self, data): write_profile_data(self._data, data) def clear_data(self): self._data = clear_profile_data() def _set_status_font_size(self, font_size): self.status_label.setStyleSheet( f"color: rgb(0, 255, 255); font-size: {font_size}px;") @mainthread # wrapper of ThreadSafetyTool def update_status(self, text, clear=False): """ Purpose: Update the bottom left status bar Features: Forced on the main Thread by ThreadSafetyTool Depends on: v2.infrastructure.ThreadSafetyTool Called by: v2.infrastructure.setup_observers (outside this class) """ if text is None: self._set_status_font_size(16) self.status_label.setText('Status:') self.disable_marquee() return if clear: self._set_status_font_size(16) self.status_label.setText('') return full_text = 'Status: ' + text metrics = self.status_label.fontMetrics() available_width = self.status_label.width() - 10 if metrics.horizontalAdvance(full_text) > available_width: self.enable_marquee(text) else: self.status_label.setText(full_text) self.disable_marquee() def check_first_launch(self): return check_first_launch(self.gui_config_file) def has_shown_fast_mode_prompt(self): return has_shown_fast_mode_prompt(self.gui_config_file) def mark_fast_mode_prompt_shown(self): mark_fast_mode_prompt_shown(self.gui_config_file) def set_fast_mode_enabled(self, enabled): set_fast_mode_enabled(self.gui_config_file, enabled) def navigate_after_profile_created(self): if not self.has_shown_fast_mode_prompt(): self.navigator.navigate("fast_mode_prompt") else: self.navigator.update_if_cached( "menu", lambda p: p.refresh_menu_buttons() if hasattr(p, 'refresh_menu_buttons') else None) self.navigator.navigate("menu") def enable_marquee(self, text): self.marquee_text = text + " " self.marquee_position = 0 self.marquee_enabled = True self.marquee_timer.start(500) def disable_marquee(self): self.marquee_enabled = False self.marquee_timer.stop() def update_marquee(self): if not self.marquee_enabled: return metrics = self.status_label.fontMetrics() text_width = metrics.horizontalAdvance(self.marquee_text) self.marquee_position += self.scroll_speed if self.marquee_position >= text_width: self.marquee_position = 0 looped_text = self.marquee_text * 2 chars_that_fit = metrics.horizontalAdvance( looped_text[:self.text_width]) visible_text = looped_text[self.marquee_position: self.marquee_position + chars_that_fit] while metrics.horizontalAdvance(visible_text) < self.text_width: visible_text += self.marquee_text while metrics.horizontalAdvance(visible_text) > self.text_width: visible_text = visible_text[:-1] display_text = 'Status: ' + ' ' * \ (self.text_start_x - metrics.horizontalAdvance('Status: ')) display_text += visible_text self.status_label.setText(display_text) def set_scroll_speed(self, speed): self.scroll_speed = speed def page_changed(self, index): if should_be_synchronized(): sync_icon = QtGui.QIcon(os.path.join( self.btn_path, 'icon_sync_urgent.png')) else: sync_icon = QtGui.QIcon(os.path.join( self.btn_path, 'icon_sync.png')) self.sync_button.setIcon(sync_icon) current_page = self.page_stack.widget(index) if self.page_history: previous_page = self.page_history[-1] else: previous_page = None current_name = getattr(current_page, 'name', None) previous_name = getattr(previous_page, 'name', None) if current_name == 'Menu': self.update_status(None) if previous_name in ('Resume', 'Editor', 'Settings', 'FastRegistration', 'FastModePrompt'): if hasattr(current_page, 'eliminacion'): current_page.eliminacion() elif current_name == 'Resume': self.update_status('Choose a name for your profile') if hasattr(current_page, 'eliminacion'): current_page.eliminacion() elif current_name == 'Id': pass elif current_name == 'Location': self.update_status(None, clear=True) elif current_name == 'InstallPackage': self.update_status('Please check package availability.') else: self.update_status(None) if current_name == 'Menu': self.sync_button.setVisible(True) self.tor_text.setVisible(True) self.toggle_button.setGeometry(670, 541, 50, 22) self.tor_icon.setGeometry(730, 537, 25, 25) else: self.sync_button.setVisible(False) self.tor_text.setVisible(False) self.toggle_button.setGeometry(540, 541, 50, 22) self.tor_icon.setGeometry(600, 537, 25, 25) self.raise_bottom_controls() if current_page != previous_page: self.page_history.append(current_page) def start_ui(force_sync): app = QApplication(sys.argv) calculate_max_screensize(app) window = CustomWindow(force_sync=force_sync) sys.exit(app.exec())