Main Menu connect/disconnect combined into a single action button, with it's state tied to a global repo. New notification system in core, pushes button changes to the main menu. Also the individual profiles get from this same global state repo, to toggle on and off by notification.

This commit is contained in:
SimplifiedPrivacy 2026-10-18 21:15:02 -04:00
parent 8b651aff0f
commit 9a13ae1939
3 changed files with 228 additions and 86 deletions

View file

@ -25,7 +25,7 @@ from gui.v2.infrastructure.setup_observers import (
# generic # generic
from typing import Callable, Optional from typing import Callable, Optional
@type_checker
def get_profile(profile_id: int): def get_profile(profile_id: int):
profile = ProfileController.get(int(profile_id)) profile = ProfileController.get(int(profile_id))
if not profile: if not profile:
@ -61,7 +61,7 @@ def start_and_navigate_to_ticketing():
return start_result return start_result
@type_checker
def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Result: def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Result:
""" """
User explicitly wants a random ticket. User explicitly wants a random ticket.
@ -82,7 +82,7 @@ def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Resul
# calls GUI repo function: # calls GUI repo function:
return use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=passed_ui_caller) return use_a_ticket(profile_id=profile_id, which_ticket=which_ticket, ui_caller=passed_ui_caller)
@type_checker
def dispatch_ticketing(profile_id: int, passed_ui_caller: Callable) -> Result: def dispatch_ticketing(profile_id: int, passed_ui_caller: Callable) -> Result:
"""Evaluate if a random ticket will be used.""" """Evaluate if a random ticket will be used."""
@ -156,7 +156,7 @@ def switch_to_tor_connection() -> Result:
ConfigurationController.set_connection_enum(ConnectionChoice.TOR) ConfigurationController.set_connection_enum(ConnectionChoice.TOR)
return Result(valid=True) return Result(valid=True)
@type_checker
def NOT_need_tor_warnings(profile_id: int) -> Result: def NOT_need_tor_warnings(profile_id: int) -> Result:
""" """
Purpose: Purpose:
@ -226,7 +226,7 @@ def literally_connect(
else: else:
return Result(valid=True) return Result(valid=True)
except Exception as error: except Exception as error:
return translate_error(error=error, profile_id=profile_id) return translate_error(error=error, profile_id=profile.id)

View file

@ -16,7 +16,8 @@ class ConnectionManager:
return self.profile_button_objects.get(profile_id, None) return self.profile_button_objects.get(profile_id, None)
def set_profile_button_objects(self, profile_id, profile_button_objects): def set_profile_button_objects(self, profile_id, profile_button_objects):
self.profile_button_objects[profile_id] = profile_button_objects pass
# self.profile_button_objects[profile_id] = profile_button_objects
def get_available_resolutions(self, profile_id): def get_available_resolutions(self, profile_id):
try: try:

View file

@ -1,5 +1,8 @@
from core.infrastructure.global_state_coordinator import connected_session_profile_ids, connected_system_profile_id, is_systemwide_on, is_connected
from core.controllers.ProfileController import ProfileController
from core.models.BaseProfile import ProfileType from core.models.BaseProfile import ProfileType
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.observers.Notifications import Notifications, EventType
import functools import functools
import os import os
@ -34,6 +37,10 @@ from gui.v2.infrastructure.setup_observers import (
profile_observer profile_observer
) )
from gui.v2.infrastructure.ThreadSafetyTool import ThreadSafetyTool
mainthread = ThreadSafetyTool()
from typing import Optional
class MenuPage(Page): class MenuPage(Page):
def __init__(self, page_stack=None, main_window=None, parent=None): def __init__(self, page_stack=None, main_window=None, parent=None):
@ -77,10 +84,12 @@ class MenuPage(Page):
self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else "" self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else ""
self.create_interface_elements() self.create_interface_elements()
self.observers_subscribe()
def showEvent(self, event): def showEvent(self, event):
super().showEvent(event) super().showEvent(event)
self._set_verification_icons_enabled() self._set_verification_icons_enabled()
self.state_refresh()
def _set_verification_icons_enabled(self): def _set_verification_icons_enabled(self):
parents = filter(None, (button.parent() for button in getattr(self, 'buttons', []))) parents = filter(None, (button.parent() for button in getattr(self, 'buttons', [])))
@ -105,41 +114,91 @@ class MenuPage(Page):
button_definitions = { button_definitions = {
"create_profile": (self.create_prof, (410, 440, 110, 60), True, True), "create_profile": (self.create_prof, (410, 440, 110, 60), True, True),
"edit_profile": (self.edit_prof, (540, 440, 110, 60), False, True), "edit_profile": (self.edit_prof, (540, 440, 110, 60), False, True),
"launch": (self.connect, (0, 440, 185, 63), False, False),
"disconnect": (self.disconnect, (200, 433, 185, 73), True, False),
"disconnect_system_wide": (self.disconnect, (200, 433, 185, 73), True, False),
"just": (self.connect, (200, 433, 185, 73), False, False),
"just_session": (self.connect, (200, 433, 185, 73), False, True),
"settings": (self.settings_gui, (670, 440, 110, 60), True, True), "settings": (self.settings_gui, (670, 440, 110, 60), True, True),
"action_button": (self.handle_action_click, (200, 433, 185, 73), True, True),
} }
buttons = {} buttons = {}
for icon_name, (function, position, enabled, visible) in button_definitions.items(): for icon_name, (function, position, enabled, visible) in button_definitions.items():
boton = QPushButton(self) boton = QPushButton(self)
boton.setGeometry(QtCore.QRect(*position)) boton.setGeometry(QtCore.QRect(*position))
boton.setObjectName("create_edit_settings_buttons") 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")) icon = QtGui.QIcon(os.path.join(self.btn_path, f"{icon_name}.png"))
boton.setIcon(icon) boton.setIcon(icon)
boton.setIconSize(boton.size()) boton.setIconSize(boton.size())
boton.clicked.connect(functools.partial(function)) boton.clicked.connect(functools.partial(function))
boton.setEnabled(enabled) boton.setEnabled(enabled)
boton.setVisible(visible) boton.setVisible(visible)
buttons[icon_name] = boton buttons[icon_name] = boton
button_attributes = { # Map the specific buttons back to the class
"disconnect_button": "disconnect", self.boton_create = buttons["create_profile"]
"boton_launch": "launch", self.boton_edit = buttons["edit_profile"]
"boton_create": "create_profile", self.settings_button = buttons["settings"]
"boton_edit": "edit_profile", self.action_button = buttons["action_button"]
"boton_just": "just", self.action_button.setIconSize(self.action_button.size())
"boton_just_session": "just_session",
"connect_button": "just_session",
"disconnect_system_wide_button": "disconnect_system_wide",
"settings_button": "settings",
}
self.__dict__.update({attribute: buttons[name] for attribute, name in button_attributes.items()})
self.refresh_menu_buttons() 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'): def _make_confirmation_popup(self, message, button_text='Proceed'):
popup = ConfirmationPopup( popup = ConfirmationPopup(
self, message=message, action_button_text=button_text, cancel_button_text="Cancel") self, message=message, action_button_text=button_text, cancel_button_text="Cancel")
@ -165,7 +224,8 @@ class MenuPage(Page):
icon_paths = {"session_tor": self.connected_tor_profile, icon_paths = {"session_tor": self.connected_tor_profile,
"session": self.connected_profile, "system": self.system_wide_connected_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()} self.button_states = {profile_id: icon_paths[kind] for profile_id, kind in menu_data["enabled_kinds"].items()}
self.update_status.update_image(appearance_value=menu_data["background"])
for button in getattr(self, 'buttons', []): for button in getattr(self, 'buttons', []):
button.parent().deleteLater() button.parent().deleteLater()
if hasattr(self, 'button_group'): if hasattr(self, 'button_group'):
@ -200,12 +260,8 @@ class MenuPage(Page):
self.update_status.current_profile_id = None self.update_status.current_profile_id = None
self.current_profile_location = None self.current_profile_location = None
self.reverse_id = None self.reverse_id = None
self.boton_edit.setEnabled(False) self.boton_edit.setEnabled(False)
self.boton_launch.setEnabled(False)
self.boton_just.setVisible(False)
self.boton_just_session.setVisible(False)
self.disconnect_button.setVisible(False)
self.disconnect_system_wide_button.setVisible(False)
if not hasattr(self, 'button_group'): if not hasattr(self, 'button_group'):
return return
self.button_group.setExclusive(False) self.button_group.setExclusive(False)
@ -234,14 +290,13 @@ class MenuPage(Page):
parent_label.setVisible(True) parent_label.setVisible(True)
return parent_label return parent_label
def create_profile_button(self, parent_label, profile_id, key): def create_profile_button(self, parent_label, profile_id, key):
button = QPushButton(parent_label) button = QPushButton(parent_label)
button.setGeometry(0, 0, 175, 60) button.setGeometry(0, 0, 175, 60)
button.setStyleSheet(CREATE_BUTTON_STYLE) button.setStyleSheet(CREATE_BUTTON_STYLE)
icon = QIcon(self.button_states.get( button.setIcon(QIcon(self.not_connected_profile))
profile_id, self.not_connected_profile))
button.setIcon(icon)
button.setIconSize(QtCore.QSize(175, 60)) button.setIconSize(QtCore.QSize(175, 60))
button.show() button.show()
@ -393,8 +448,7 @@ class MenuPage(Page):
def _render_profile_details(self, details): def _render_profile_details(self, details):
print(f"details entering _render_profile_details is {details}") # print(f"details entering _render_profile_details is {details}")
self.reverse_id = details["id"] self.reverse_id = details["id"]
@ -402,16 +456,13 @@ class MenuPage(Page):
self.update_status.current_profile_id = self.reverse_id self.update_status.current_profile_id = self.reverse_id
self.current_profile_location = details["location"] self.current_profile_location = details["location"]
target_button = self.profile_button_map.get(self.reverse_id)
if target_button is not None: # Update buttons:
target_button.setChecked(True) self.update_action_button_visuals(self.reverse_id)
self.connection_manager.set_profile_button_objects(self.reverse_id, target_button)
self.boton_edit.setEnabled(not details["is_connected"] and not self._task_busy) # verification icons:
self.boton_launch.setEnabled(not self._task_busy)
self.boton_just.setEnabled(not self._task_busy)
self.boton_just_session.setEnabled(not self._task_busy)
self._set_verification_icons_enabled() self._set_verification_icons_enabled()
self.update_button_visibility(details["is_connected"], details["is_session"])
self.update_status_message(details) self.update_status_message(details)
for label in self.additional_labels: for label in self.additional_labels:
label.deleteLater() label.deleteLater()
@ -511,15 +562,16 @@ class MenuPage(Page):
self.page_stack.indexOf(verification_page)) self.page_stack.indexOf(verification_page))
def _set_task_busy(self, busy): def _set_task_busy(self, busy):
self._task_busy = busy pass
for button in (self.boton_just, self.boton_just_session, self.disconnect_button, # self._task_busy = busy
self.disconnect_system_wide_button, self.boton_create, self.boton_edit): # for button in (self.boton_just, self.boton_just_session, self.disconnect_button,
button.setEnabled(not busy) # self.disconnect_system_wide_button, self.boton_create, self.boton_edit):
self.scroll_area.setEnabled(not busy) # button.setEnabled(not busy)
self.boton_just.setEnabled(not busy and self.reverse_id is not None) # self.scroll_area.setEnabled(not busy)
self.boton_just_session.setEnabled(not busy and self.reverse_id is not None) # self.boton_just.setEnabled(not busy and self.reverse_id is not None)
self.boton_edit.setEnabled(not busy and self.reverse_id is not None # self.boton_just_session.setEnabled(not busy and self.reverse_id is not None)
and not self.connection_manager.is_profile_connected(self.reverse_id)) # 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): def post_connect(self, result: Result):
@ -537,18 +589,6 @@ class MenuPage(Page):
# CONNECTION WORKED # CONNECTION WORKED
# -------------------- # --------------------
if result.valid: if result.valid:
# BACKGROUND CONNECTION IMAGE:
self.update_status.update_image(appearance_value="background_connected")
# BUTTON:
profile_type = result.data
if profile_type == ProfileType.SYSTEM:
self.disconnect_system_wide_button.setVisible(True)
elif profile_type == ProfileType.SESSION:
self.disconnect_button.setVisible(True)
else:
print("This is an error where the GUI recieved an invalid profile type, but we are enabling the systemwide by default.")
self.disconnect_system_wide_button.setVisible(True)
return return
# -------------------- # --------------------
@ -571,10 +611,8 @@ class MenuPage(Page):
"""Enable the profile id tied to reverse_id""" """Enable the profile id tied to reverse_id"""
print("Starting to connect..") print("Starting to connect..")
# Step 1. Disable all buttons to prevent repeat pushing. # # Step 1. Disable edit button.
self.boton_just.setVisible(False) self.boton_edit.setEnabled(False)
self.boton_launch.setEnabled(False)
self.boton_just_session.setEnabled(False)
# Step 2. Connect with post_connect setup to recieve the result. # 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) immediate_error = connect_profile(profile_id=self.reverse_id, ui_caller=self.post_connect)
@ -584,13 +622,6 @@ class MenuPage(Page):
self.update_status.update_status(immediate_error.message) self.update_status.update_status(immediate_error.message)
return return
# Temporary until moved to menu coordinator:
# print("connecting...")
# self._set_task_busy(True)
# self.update_status.update_status("Attempting to connect...")
# self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result)
def disconnect(self): def disconnect(self):
self._set_task_busy(True) self._set_task_busy(True)
self.update_status.update_status("Disconnecting...") self.update_status.update_status("Disconnecting...")
@ -602,14 +633,6 @@ class MenuPage(Page):
self.worker = create_profile(getattr(self.update_status, "gui_config_file", None), self.worker = create_profile(getattr(self.update_status, "gui_config_file", None),
self.connection_manager.is_synced(), self.handle_task_result) self.connection_manager.is_synced(), self.handle_task_result)
def update_button_visibility(self, is_connected, is_session_profile):
self.boton_just_session.setVisible(
not is_connected and is_session_profile)
self.boton_just.setVisible(not is_connected and not is_session_profile)
self.disconnect_button.setVisible(is_connected and is_session_profile)
self.disconnect_system_wide_button.setVisible(
is_connected and not is_session_profile)
def update_status_message(self, state): def update_status_message(self, state):
if not state["profile_loaded"]: if not state["profile_loaded"]:
self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.") self.update_status.update_status("Profile data could not be loaded. Sync or restart and try again.")
@ -707,3 +730,121 @@ class MenuPage(Page):
def _show_ticket_loss_result(self, result): def _show_ticket_loss_result(self, result):
self.show_ticket_data_loss_popup(result["ticket_number"], result["billing_code"]) 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)
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)