hydraveil-gui/gui/main_ui.py

915 lines
33 KiB
Python
Executable file

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())