diff --git a/gui/v2/actions/menu_coordinator.py b/gui/v2/actions/menu_coordinator.py index 53d629c..b18c73f 100644 --- a/gui/v2/actions/menu_coordinator.py +++ b/gui/v2/actions/menu_coordinator.py @@ -25,7 +25,7 @@ from gui.v2.infrastructure.setup_observers import ( # generic from typing import Callable, Optional - +@type_checker def get_profile(profile_id: int): profile = ProfileController.get(int(profile_id)) if not profile: @@ -61,7 +61,7 @@ def start_and_navigate_to_ticketing(): return start_result - +@type_checker def random_ticket_dispatch(profile_id: int, passed_ui_caller: Callable) -> Result: """ 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: 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: """Evaluate if a random ticket will be used.""" @@ -156,7 +156,7 @@ def switch_to_tor_connection() -> Result: ConfigurationController.set_connection_enum(ConnectionChoice.TOR) return Result(valid=True) - +@type_checker def NOT_need_tor_warnings(profile_id: int) -> Result: """ Purpose: @@ -226,7 +226,7 @@ def literally_connect( else: return Result(valid=True) except Exception as error: - return translate_error(error=error, profile_id=profile_id) + return translate_error(error=error, profile_id=profile.id) diff --git a/gui/v2/infrastructure/connection_manager.py b/gui/v2/infrastructure/connection_manager.py index 179df5d..db0c5e2 100755 --- a/gui/v2/infrastructure/connection_manager.py +++ b/gui/v2/infrastructure/connection_manager.py @@ -16,7 +16,8 @@ class ConnectionManager: return self.profile_button_objects.get(profile_id, None) 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): try: diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index ab5b315..72a3602 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -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.Result import Result, ResultError +from core.observers.Notifications import Notifications, EventType import functools import os @@ -34,6 +37,10 @@ from gui.v2.infrastructure.setup_observers import ( 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): @@ -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.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', []))) @@ -105,41 +114,91 @@ class MenuPage(Page): button_definitions = { "create_profile": (self.create_prof, (410, 440, 110, 60), True, 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), + "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") - icon = QtGui.QIcon(os.path.join(self.btn_path, f"{icon_name}.png")) - boton.setIcon(icon) - boton.setIconSize(boton.size()) + # 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 - button_attributes = { - "disconnect_button": "disconnect", - "boton_launch": "launch", - "boton_create": "create_profile", - "boton_edit": "edit_profile", - "boton_just": "just", - "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()}) + # 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") @@ -165,7 +224,8 @@ class MenuPage(Page): 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()} - self.update_status.update_image(appearance_value=menu_data["background"]) + + for button in getattr(self, 'buttons', []): button.parent().deleteLater() if hasattr(self, 'button_group'): @@ -200,12 +260,8 @@ class MenuPage(Page): self.update_status.current_profile_id = None self.current_profile_location = None self.reverse_id = None + 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'): return self.button_group.setExclusive(False) @@ -234,14 +290,13 @@ class MenuPage(Page): 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) - icon = QIcon(self.button_states.get( - profile_id, self.not_connected_profile)) - button.setIcon(icon) + button.setIcon(QIcon(self.not_connected_profile)) button.setIconSize(QtCore.QSize(175, 60)) button.show() @@ -393,8 +448,7 @@ class MenuPage(Page): 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"] @@ -402,16 +456,13 @@ class MenuPage(Page): self.update_status.current_profile_id = self.reverse_id self.current_profile_location = details["location"] - target_button = self.profile_button_map.get(self.reverse_id) - if target_button is not None: - target_button.setChecked(True) - 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) - 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) + + # Update buttons: + self.update_action_button_visuals(self.reverse_id) + + # verification icons: self._set_verification_icons_enabled() - self.update_button_visibility(details["is_connected"], details["is_session"]) + self.update_status_message(details) for label in self.additional_labels: label.deleteLater() @@ -511,15 +562,16 @@ class MenuPage(Page): self.page_stack.indexOf(verification_page)) def _set_task_busy(self, busy): - 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)) + 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): @@ -537,18 +589,6 @@ class MenuPage(Page): # CONNECTION WORKED # -------------------- 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 # -------------------- @@ -571,10 +611,8 @@ class MenuPage(Page): """Enable the profile id tied to reverse_id""" print("Starting to connect..") - # Step 1. Disable all buttons to prevent repeat pushing. - self.boton_just.setVisible(False) - self.boton_launch.setEnabled(False) - self.boton_just_session.setEnabled(False) + # # 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) @@ -584,13 +622,6 @@ class MenuPage(Page): self.update_status.update_status(immediate_error.message) 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): self._set_task_busy(True) 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.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): if not state["profile_loaded"]: 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): 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)