hydraveil-gui/gui/v2/ui/pages/menu_page.py

856 lines
35 KiB
Python
Executable file

from core.infrastructure.global_state_coordinator import connected_session_profile_ids, connected_system_profile_id, is_systemwide_on, is_connected, evaluate_state_on_startup
from gui.v2.infrastructure.take_plumbing_action import take_action
from gui.v2.actions.error_handlers import empty_filler
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import ProfileType
from core.models.Result import Result, ResultError
from core.observers.Notifications import Notifications, EventType
import functools
import os
from itertools import chain
from PyQt6 import QtCore, QtGui
from PyQt6.QtCore import Qt, QSize, pyqtSlot
from PyQt6.QtGui import QIcon, QPainter, QPixmap
from PyQt6.QtWidgets import (
QApplication, QButtonGroup, QFrame, QHBoxLayout, QLabel, QLineEdit,
QMessageBox, QPushButton, QScrollArea, QTextEdit, QVBoxLayout, QWidget,
)
from gui.v2.actions.menu_coordinator import connect_profile
from gui.v2.actions.menu_profiles import find_verification_icon, get_profile_details, load_menu_profiles, menu_icons
from gui.v2.actions.menu_tasks import (
continue_menu_task, create_profile, delete_profile, disable_profile,
download_update, edit_profile, enable_profile
)
from gui.v2.actions.menu_results import MenuResult
from gui.v2.ui.pages.Page import Page
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
from gui.v2.ui.styles.css.menu_css import CREATE_BUTTON_STYLE
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
from gui.v2.infrastructure.setup_observers import (
application_version_observer, connection_observer,
profile_observer
)
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
from typing import Optional
class MenuPage(Page):
def __init__(self, page_stack=None, main_window=None, parent=None):
super().__init__("Menu", page_stack, main_window, parent)
self.main_window = main_window
self.connection_manager = main_window.connection_manager
self.btn_path = main_window.btn_path
self.page_stack = page_stack
self.profile_info = {}
self.additional_labels = []
self.update_status = main_window
self.title.setGeometry(400, 40, 380, 30)
self.title.setText("Load Profile")
self.scroll_area = QScrollArea(self)
self.scroll_area.setGeometry(420, 80, 370, 350)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scroll_area.setStyleSheet(SCROLLBAR_CYAN_QSS)
self.scroll_widget = QWidget()
self.scroll_widget.setStyleSheet("background-color: transparent;")
self.scroll_area.setWidget(self.scroll_widget)
self.not_connected_profile = os.path.join(
self.btn_path, "button_profile.png")
self.connected_profile = os.path.join(
self.btn_path, "button_session_profile.png")
self.system_wide_connected_profile = os.path.join(
self.btn_path, "button_system_profile.png")
self.connected_tor_profile = os.path.join(
self.btn_path, "button_session_tor.png")
self.worker = None
self.reverse_id = None
self._task_busy = False
self.button_states = {}
self.profile_button_map = {}
self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else ""
self.create_interface_elements()
self.observers_subscribe()
def showEvent(self, event):
super().showEvent(event)
self._set_verification_icons_enabled()
self.state_refresh()
def _set_verification_icons_enabled(self):
parents = filter(None, (button.parent() for button in getattr(self, 'buttons', [])))
icons = chain.from_iterable(parent.findChildren(QPushButton) for parent in parents)
verification_icons = filter(
lambda icon: icon.geometry().width() == 20 and icon.geometry().height() == 20,
icons,
)
for icon in verification_icons:
icon.setEnabled(self.connection_manager.is_synced())
def on_update_check_finished(self):
reply = QMessageBox.question(self, 'Update Available',
'An update is available. Would you like to download it?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.worker = download_update(self.handle_task_result)
else:
self.update_status.update_status('Update canceled')
def create_interface_elements(self):
button_definitions = {
"create_profile": (self.create_prof, (410, 440, 110, 60), True, True),
"edit_profile": (self.edit_prof, (540, 440, 110, 60), False, True),
"settings": (self.settings_gui, (670, 440, 110, 60), True, True),
"action_button": (self.handle_action_click, (200, 433, 185, 73), True, True),
}
buttons = {}
for icon_name, (function, position, enabled, visible) in button_definitions.items():
boton = QPushButton(self)
boton.setGeometry(QtCore.QRect(*position))
boton.setObjectName("create_edit_settings_buttons")
# Note: action_button icon is handled dynamically in update_action_button()
if icon_name != "action_button":
icon = QtGui.QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))
boton.setIcon(icon)
boton.setIconSize(boton.size())
boton.clicked.connect(functools.partial(function))
boton.setEnabled(enabled)
boton.setVisible(visible)
buttons[icon_name] = boton
# Map the specific buttons back to the class
self.boton_create = buttons["create_profile"]
self.boton_edit = buttons["edit_profile"]
self.settings_button = buttons["settings"]
self.action_button = buttons["action_button"]
self.action_button.setIconSize(self.action_button.size())
self.refresh_menu_buttons()
def update_action_button_visuals(self, triggered_profile_id: Optional[int] = None):
"""Syncs the button art and enabled state with the current external truth."""
# Which profile is selected:
profile_id = getattr(self.update_status, 'current_profile_id', None)
if profile_id is None:
self.action_button.setVisible(False)
return
# triggered_profile_id is coming via a Notification.
# Don't update the button via from a notification, if the user clicked to a different profile,
if triggered_profile_id and profile_id != triggered_profile_id:
return
self.action_button.setVisible(True)
self.action_button.setEnabled(True) # Re-enable after the Event has fired
connected = is_connected(profile_id)
p_type = ProfileController.what_type_is_profile(profile_id)
# if it's connected, then it can't be edited.
self.boton_edit.setEnabled(not connected)
# Logic Matrix for Art
if p_type == ProfileType.SESSION:
icon_name = "disconnect" if connected else "just_session"
else: # SYSTEM
icon_name = "disconnect_system_wide" if connected else "just"
print(f"Setting icon_name to {icon_name}")
icon = QtGui.QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))
self.action_button.setIcon(icon)
def handle_action_click(self):
"""Route the action button to connect or disconnect, depending on state"""
# 1. Relevant profile ID:
profile_id = getattr(self.update_status, 'current_profile_id', None)
if profile_id is None:
return
# 2. Immediately disable to prevent double-clicks during async transition
self.action_button.setEnabled(False)
# 3. Connection Status
connected = is_connected(profile_id)
# 4. Route:
if connected:
self.disconnect()
else:
self.connect()
def _make_confirmation_popup(self, message, button_text='Proceed'):
popup = ConfirmationPopup(
self, message=message, action_button_text=button_text, cancel_button_text="Cancel")
popup.setWindowModality(Qt.WindowModality.ApplicationModal)
return popup
def delete_profile(self):
self._set_task_busy(True)
self.worker = delete_profile(self.reverse_id, self.handle_task_result)
def refresh_profiles_data(self):
self.refresh_menu_buttons()
def refresh_menu_buttons(self, state_change=None):
menu_data = load_menu_profiles(
getattr(self.update_status, 'gui_config_file', None),
self.update_status.update_status, self.reverse_id, state_change,
)
self.profiles_data = menu_data["display_profiles"]
self.profile_info = self.profiles_data
self.number_of_profiles = len(self.profiles_data)
self.connection_manager.set_connected_profiles(menu_data["enabled_kinds"])
icon_paths = {"session_tor": self.connected_tor_profile,
"session": self.connected_profile, "system": self.system_wide_connected_profile}
self.button_states = {profile_id: icon_paths[kind] for profile_id, kind in menu_data["enabled_kinds"].items()}
for button in getattr(self, 'buttons', []):
button.parent().deleteLater()
if hasattr(self, 'button_group'):
self.button_group.deleteLater()
self.profile_button_map.clear()
self.button_group = QButtonGroup(self)
self.buttons = []
for profile_name, profile_value in self.profiles_data.items():
self.create_profile_widget(profile_name, profile_value)
self.update_scroll_widget_size()
if menu_data["selected_details"] is not None:
self._render_profile_details(menu_data["selected_details"])
return
self.clear_profile_details()
def update_scroll_widget_size(self):
if self.number_of_profiles == 0:
self.scroll_widget.setFixedSize(370, 0)
return
rows = (self.number_of_profiles + 1) // 2
height = rows * 120
self.scroll_widget.setFixedSize(370, height)
def clear_profile_details(self):
for label in self.additional_labels:
label.deleteLater()
self.additional_labels.clear()
self.selected_profiles.clear()
self.selected_wireguard.clear()
self.selected_residential.clear()
self.update_status.current_profile_id = None
self.current_profile_location = None
self.reverse_id = None
self.boton_edit.setEnabled(False)
if not hasattr(self, 'button_group'):
return
self.button_group.setExclusive(False)
for button in self.buttons:
button.setChecked(False)
self.button_group.setExclusive(True)
def create_profile_widget(self, key, value):
profile_id = value['id']
parent_label = self.create_parent_label(profile_id)
button = self.create_profile_button(parent_label, profile_id, key)
self.profile_button_map[profile_id] = button
self.create_profile_name_label(parent_label, value["name"])
self.create_profile_icons(parent_label, value)
def create_parent_label(self, profile_id):
parent_label = QLabel(self.scroll_widget)
profile_list = list(self.profiles_data.keys())
index = profile_list.index(f'Profile_{profile_id}')
row, column = divmod(index, 2)
parent_label.setGeometry(column * 179, row * 120, 175, 100)
parent_label.setVisible(True)
return parent_label
def create_profile_button(self, parent_label, profile_id, key):
button = QPushButton(parent_label)
button.setGeometry(0, 0, 175, 60)
button.setStyleSheet(CREATE_BUTTON_STYLE)
button.setIcon(QIcon(self.not_connected_profile))
button.setIconSize(QtCore.QSize(175, 60))
button.show()
button.setCheckable(True)
self.button_group.addButton(button)
self.buttons.append(button)
button.clicked.connect(
lambda checked, pid=profile_id: self.print_profile_details(f"Profile_{pid}"))
return button
def create_profile_name_label(self, parent_label, name):
child_label = QLabel(parent_label)
child_label.setGeometry(0, 65, 175, 30)
child_label.setText(str(name or "Unnamed Profile"))
child_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
child_label.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents)
child_label.show()
def create_profile_icons(self, parent_label, value):
for index, icon in enumerate(menu_icons(self.btn_path, value)):
child_label = QLabel(parent_label)
child_label.setObjectName("label_profiles")
child_label.setGeometry(3 + index * 60, 5, 50, 50)
child_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
child_label.setStyleSheet("background-color: rgba(128, 128, 128, 0.5); border-radius: 25px;")
pixmap = self._load_profile_icon_pixmap(icon["path"], icon["kind"])
child_label.setPixmap(self._add_browser_warning(pixmap, icon["warning"]))
child_label.show()
def _load_profile_icon_pixmap(self, image_path, label_name):
pixmap = QPixmap(image_path)
fallback_names = {
'location': 'default_location_mini.png',
'browser': 'default_browser_mini.png',
}
fallback_name = fallback_names.get(label_name)
if not pixmap.isNull() or fallback_name is None:
return pixmap
pixmap = QPixmap(os.path.join(self.btn_path, fallback_name))
if not pixmap.isNull():
return pixmap
placeholder = QPixmap(50, 50)
placeholder.fill(QtGui.QColor(200, 200, 200))
return placeholder
def _add_browser_warning(self, pixmap, needs_warning):
if not needs_warning:
return pixmap
painter = QPainter(pixmap)
painter.drawPixmap(pixmap.width() - 22, pixmap.height() - 21,
QPixmap(os.path.join(self.btn_path, 'warning.png')))
painter.end()
return pixmap
def create_verification_widget(self, values):
verification_widget = QWidget(self)
verification_widget.setGeometry(0, 90, 400, 300)
verification_widget.setStyleSheet("background: transparent;")
verification_layout = QVBoxLayout(verification_widget)
verification_layout.setContentsMargins(20, 20, 20, 20)
verification_layout.setSpacing(15)
for label, full_value, value in values:
container = QWidget()
container.setStyleSheet("background: transparent;")
container_layout = QHBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(10)
label_widget = QLabel(label + ":")
label_widget.setStyleSheet(
f"color: white; font-size: 12px; {self.font_style}")
label_widget.setMinimumWidth(120)
container_layout.addWidget(label_widget)
value_widget = QLineEdit()
value_widget.setReadOnly(True)
value_widget.setText(value)
value_widget.setCursorPosition(0)
value_widget.setStyleSheet(f"""
QLineEdit {{
color: white;
background: transparent;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
padding: 5px 0px;
font-size: 12px;
{self.font_style}
}}
""")
container_layout.addWidget(value_widget, 1)
checkmark_widget = QLabel("✓")
checkmark_widget.setStyleSheet(
f"color: #4CAF50; font-size: 20px; font-weight: bold; {self.font_style}")
checkmark_widget.setFixedSize(20, 20)
checkmark_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
checkmark_widget.setVisible(bool(value and value != "N/A"))
container_layout.addWidget(checkmark_widget)
copy_button = QPushButton()
copy_button.setIcon(
QIcon(os.path.join(self.btn_path, "paste_button.png")))
copy_button.setIconSize(QSize(24, 24))
copy_button.setFixedSize(30, 30)
copy_button.setStyleSheet(f"""
QPushButton {{
background: transparent;
border: none;
}}
QPushButton:hover {{
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}}
""")
copy_button.clicked.connect(
lambda checked=False, val=full_value: self.copy_verification_value(val))
container_layout.addWidget(copy_button)
verification_layout.addWidget(container)
verification_widget.show()
return verification_widget
def copy_verification_value(self, value):
if value and value != "N/A":
clipboard = QApplication.clipboard()
clipboard.setText(value)
self.update_status.update_status(
"Verification value copied to clipboard")
def print_profile_details(self, profile_name):
summary = self.profile_info.get(profile_name)
if summary is None:
self.clear_profile_details()
return
details = get_profile_details(summary["id"], summary,
self.connection_manager.is_profile_connected(summary["id"]),
self.update_status.update_status)
self._render_profile_details(details)
def _render_profile_details(self, details):
# print(f"details entering _render_profile_details is {details}")
self.reverse_id = details["id"]
# THIS IS UPDATING THE PROFILE ID
self.update_status.current_profile_id = self.reverse_id
self.current_profile_location = details["location"]
# Update buttons:
self.update_action_button_visuals(self.reverse_id)
# verification icons:
self._set_verification_icons_enabled()
self.update_status_message(details)
for label in self.additional_labels:
label.deleteLater()
self.additional_labels.clear()
if details["background_asset"]:
self._detail_image(details["background_asset"], (0, 60, 410, 354), lower=True)
renderers = {"verification": self._render_verification,
"operator_details": self._render_operator_details,
"protocol_image": self._render_protocol_image}
renderers[details["display_mode"]](details)
if details["show_verified_icon"]:
self._detail_image("verified_profile.png", (20, 50, 100, 80))
self.add_selected_profile(details["selection"])
def _detail_image(self, asset, geometry, lower=False, pixmap=None):
label = QLabel(self)
label.setGeometry(*geometry)
label.setPixmap(pixmap if pixmap is not None else QPixmap(os.path.join(self.btn_path, asset)))
label.setScaledContents(True)
label.show()
if lower:
label.lower()
self.additional_labels.append(label)
return label
def _render_verification(self, details):
self.additional_labels.append(self.create_verification_widget(details["verification_values"]))
def _render_protocol_image(self, details):
pixmap = QPixmap(os.path.join(self.btn_path, details["primary_asset"]))
if details["location_overlay"]:
pixmap = self.build_encrypted_proxy_detail_pixmap(pixmap, details["location"], details["overlay_y"])
self._detail_image(details["primary_asset"], (0, 80 if details["raised_primary"] else 90, 400, 300), pixmap=pixmap)
def _render_operator_details(self, details):
color = "black" if details["is_session"] else "white"
self._detail_image(f"icon_{details['location']}.png", (20, 125, 100, 100))
text_blocks = [
((130, 110, 260, 250), f"Location: {details['location_text']}\n\nOperator: {details['operator_name']}",
QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere),
((20, 260, 370, 80), f"Nostr: {details['nostr_public_key']}", QtGui.QTextOption.WrapMode.WrapAnywhere),
]
for geometry, text, wrapping in text_blocks:
widget = QTextEdit(self)
widget.setGeometry(*geometry)
widget.setReadOnly(True)
widget.setFrameStyle(QFrame.Shape.NoFrame)
widget.setStyleSheet(f"background: transparent; border: none; color: {color}; font-size: 17px;")
widget.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
widget.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
widget.setWordWrapMode(wrapping)
widget.setText(text)
widget.show()
self.additional_labels.append(widget)
def build_encrypted_proxy_detail_pixmap(self, pixmap, location, overlay_y):
if pixmap.isNull():
return pixmap
location_pixmap = QPixmap(os.path.join(
self.btn_path, f"icon_mini_{location}.png"))
if location_pixmap.isNull():
location_pixmap = QPixmap(os.path.join(
self.btn_path, "default_location_mini.png"))
if location_pixmap.isNull():
return pixmap
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
location_pixmap = location_pixmap.scaled(
icon_size,
icon_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
x = int(pixmap.width() * 0.58)
y = int(pixmap.height() * overlay_y)
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
painter = QPainter(pixmap)
painter.drawPixmap(x, y, location_pixmap)
painter.end()
return pixmap
def show_profile_verification(self, profile_id):
location_icon_name = find_verification_icon(
profile_id,
self.btn_path,
self.connection_manager.get_location_info,
self.update_status.update_status,
)
if location_icon_name:
verification_page = LocationVerificationPage(
self.page_stack, self.custom_window, location_icon_name, self)
self.page_stack.addWidget(verification_page)
self.page_stack.setCurrentIndex(
self.page_stack.indexOf(verification_page))
def _set_task_busy(self, busy):
pass
# self._task_busy = busy
# for button in (self.boton_just, self.boton_just_session, self.disconnect_button,
# self.disconnect_system_wide_button, self.boton_create, self.boton_edit):
# button.setEnabled(not busy)
# self.scroll_area.setEnabled(not busy)
# self.boton_just.setEnabled(not busy and self.reverse_id is not None)
# self.boton_just_session.setEnabled(not busy and self.reverse_id is not None)
# self.boton_edit.setEnabled(not busy and self.reverse_id is not None
# and not self.connection_manager.is_profile_connected(self.reverse_id))
def post_connect(self, result: Result):
"""Recieve the callback after a connection attempt."""
# --------------------
# BOTTOM LEFT STATUS BAR
# --------------------
if result.message:
if len(result.message) > 20:
self.update_status.enable_marquee
self.update_status.update_status(result.message)
# --------------------
# CONNECTION WORKED
# --------------------
if result.valid:
return
# --------------------
# FAILURE EVALUATION
# --------------------
if result.error_type == ResultError.NAVIGATE_TO:
print(f"We are auto-navigating to page {result.navigate_to}")
self.custom_window.navigator.navigate(result.navigate_to)
elif result.error_type == ResultError.SUBSCRIPTION:
print("We are navigating to the subscription page")
self.update_status.update_status("Select Subscription Choice")
self.custom_window.navigator.navigate("id")
else:
error_type = result.error_type.value
print(f"We do not currently have a pre-made solution for the unknown situation of {error_type}")
self.update_status.update_status(error_type)
def connect(self):
"""Enable the profile id tied to reverse_id"""
print("Starting to connect..")
# # Step 1. Disable edit button.
self.boton_edit.setEnabled(False)
# Step 2. Connect with post_connect setup to recieve the result.
immediate_error = connect_profile(profile_id=self.reverse_id, ui_caller=self.post_connect)
# Step 3. Evaluate immediate errors (if it never even gets to the callback)
if not immediate_error.valid:
self.update_status.update_status(immediate_error.message)
return
def disconnect(self):
self._set_task_busy(True)
self.update_status.update_status("Disconnecting...")
self.worker = disable_profile(self.reverse_id, self.connection_manager.get_connected_profiles(),
self.handle_task_result)
def create_prof(self):
self._set_task_busy(True)
self.worker = create_profile(getattr(self.update_status, "gui_config_file", None),
self.connection_manager.is_synced(), self.handle_task_result)
def update_status_message(self, state):
if not state["profile_loaded"]:
self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.")
return
status_handlers = {
True: self.update_status.enable_marquee,
False: self.update_status.update_status,
}
status_handlers[state["use_marquee"]](state["message"])
def edit_prof(self):
self._set_task_busy(True)
self.worker = edit_profile(getattr(self.update_status, "gui_config_file", None),
self.connection_manager.is_synced(), self.handle_task_result)
def settings_gui(self):
self.custom_window.navigator.navigate("settings")
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
self.custom_window.navigator.navigate("menu")
self.update_status.update_status('Critical: invalid billing code from ticket.')
self.popup = TicketDataLossPopup(
self,
ticket_number=ticket_number,
billing_code=billing_code,
)
self.popup.show()
def DisplayInstallScreen(self, package_name):
self.custom_window.navigator.navigate("install_system_package")
install_page = self.custom_window.navigator.get_cached("install_system_package")
if install_page is not None:
install_page.configure(package_name=package_name, distro='debian')
@pyqtSlot(object)
def handle_task_result(self, result: MenuResult):
print(f"result in handle_task_result is {result}")
if "sync_data" in result:
self.update_status.update_values(*result["sync_data"])
if result.get("clear_selection") and result.get("profile_id") == self.reverse_id:
self.clear_profile_details()
if result.get("refresh"):
self.refresh_menu_buttons(result)
self._set_task_busy(result.get("busy", False))
if result.get("message"):
handlers = {True: self.update_status.enable_marquee, False: self.update_status.update_status}
handlers[bool(result.get("is_enabled"))](result["message"])
handlers = {"confirmation": self._show_confirmation, "verification": self._show_verification,
"navigate": self._navigate_result, "install": self._show_install_result,
"ticket_data_loss": self._show_ticket_loss_result}
handler = handlers.get(result["status"])
if handler is not None:
handler(result)
def _show_confirmation(self, result):
popup = self._make_confirmation_popup(result["message"], result["button_text"])
popup.finished.connect(functools.partial(self._confirmation_response, result["request"]))
self.popup = popup
popup.show()
def _confirmation_response(self, request, accepted):
self._set_task_busy(accepted)
self.worker = continue_menu_task(request, "continue" if accepted else "abort", self.handle_task_result)
def _show_verification(self, result):
popup = EndpointVerificationPopup(self, message=result["message"])
popup.finished.connect(functools.partial(self._verification_response, result["request"]))
self.popup = popup
popup.show()
def _verification_response(self, request, accepted, choice):
self._set_task_busy(accepted)
self.worker = continue_menu_task(request, choice if accepted else "abort", self.handle_task_result)
def _navigate_result(self, result):
if "profile_id" in result:
self.update_status.current_profile_id = result["profile_id"]
destination = result["destination"]
page = self.custom_window.navigator.get_cached(destination)
if destination == "ticket_or_billing_choice" and page is not None:
page.populate()
self.custom_window.navigator.navigate(destination)
def _show_install_result(self, result):
self.DisplayInstallScreen(result["package_name"])
def _show_ticket_loss_result(self, result):
self.show_ticket_data_loss_popup(result["ticket_number"], result["billing_code"])
def _update_profile_icon(self, profile_id: int, image_path: str, is_active: bool = False):
"""Helper to update profile icons and the 'checked' state for the border/highlight."""
button = self.profile_button_map.get(profile_id)
if button:
button.setIcon(QIcon(image_path))
# This ensures the UI 'checked' state matches the connection state
button.setChecked(is_active)
def profile_is_currently_selected(self, profile_id: int) -> bool:
"""Checks if the profile that's currently selected by the user, matches profile_id"""
return self.update_status.current_profile_id == profile_id
@mainthread
def turn_on_session_visuals(self, profile_id: int):
"""Flip off the Visual buttons for a given profile."""
print(f"Turning ON Session {profile_id}!")
# Update the specific profile button
self._update_profile_icon(profile_id, self.connected_profile, is_active=True)
# Update disconnect/action button:
self.update_action_button_visuals(profile_id)
@mainthread
def turn_off_session_visuals(self, profile_id: int):
"""
Flip off the Visual buttons for a given profile.
And re-enable the edit button if its currently selected.
"""
print(f"Stopping Session {profile_id}!")
# If you want the border to stay when off, set is_active=True
# If you want the border to disappear when off, set is_active=False
self._update_profile_icon(profile_id, self.not_connected_profile, is_active=True)
# Update disconnect/action button:
self.update_action_button_visuals(profile_id)
# Re-enable edit button:
if self.profile_is_currently_selected(profile_id):
self.boton_edit.setEnabled(True)
@mainthread
def turn_on_system_visuals(self, profile_id: int):
print(f"Turning ON Systemwide on {profile_id}!")
# Update the specific profile button
self._update_profile_icon(profile_id, self.system_wide_connected_profile, is_active=True)
# Update the general background status
self.update_status.update_image(appearance_value="background_connected")
# Update disconnect/action button:
self.update_action_button_visuals(profile_id)
@mainthread
def turn_off_system_visuals(self, profile_id: int):
"""
Flip off the Visual buttons for a given System-wide.
Change the entire app's background for connected.
And re-enable the edit button if its currently selected.
"""
print(f"Turning OFF Systemwide on {profile_id}!")
# Update the specific profile button
self._update_profile_icon(profile_id, self.not_connected_profile, is_active=True)
# Update the general background status
self.update_status.update_image(appearance_value="original")
# Update connect/action button:
self.update_action_button_visuals(profile_id)
# Re-enable edit button:
if self.profile_is_currently_selected(profile_id):
self.boton_edit.setEnabled(True)
def observers_subscribe(self):
Notifications.subscribe(EventType.SESSION_PROFILE_ENABLE, self.turn_on_session_visuals)
Notifications.subscribe(EventType.SESSION_PROFILE_DISABLE, self.turn_off_session_visuals)
Notifications.subscribe(EventType.SYSTEM_ENABLE, self.turn_on_system_visuals)
Notifications.subscribe(EventType.SYSTEM_DISABLE, self.turn_off_system_visuals)
take_action(
action=evaluate_state_on_startup,
ui_caller=empty_filler # do nothing on success.
)
def state_refresh(self):
"""Update the UI with the current Global State Repo"""
# 1. Collect current state
sys_on = is_systemwide_on()
sys_id = connected_system_profile_id()
sess_ids = connected_session_profile_ids() or []
# 2. Set the global background
if sys_on:
self.update_status.update_image(appearance_value="background_connected")
else:
self.update_status.update_image(appearance_value="original")
# 3. Sync every button
for pid in self.profile_button_map.keys():
# We check for the "On" states first.
# If none of these match, it falls through to the default 'off' state.
if sys_on and pid == sys_id:
# Priority 1: System-wide active
self.turn_on_system_visuals(pid)
elif pid in sess_ids:
# Priority 2: Session active
self.turn_on_session_visuals(pid)
else:
# DEFAULT STARTING PLACE:
# If it's not system-wide and not a session, it must be off.
self.turn_off_session_visuals(pid)