Redesign desktop lobby as a workspace

This commit is contained in:
Peter Stockings
2026-10-05 16:25:38 +11:00
parent 21a35d81f0
commit 0c5d4ee546
4 changed files with 330 additions and 141 deletions
+4
View File
@@ -10,6 +10,10 @@ address remains fixed. Detached chat opens docked on the next launch.
## Active sessions
The desktop workspace has a sidebar for your saved display name, **Create room**,
and **Join by code**. Creation and code entry open in separate dialogs. Drag a
video onto the lobby to prepare a new room. Enter your name to enable room actions.
The lobby checks for listed rooms at startup and every 15 seconds while open.
Select a session and click **Choose file & join**, or double-click it, to use the
existing file verification flow. Enter your display name first. **Refresh** checks
+295 -141
View File
@@ -2,9 +2,38 @@ import os
import re
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QFileDialog, QFrame, QApplication, QCheckBox, QListWidget, QListWidgetItem
)
from PyQt6.QtCore import Qt, pyqtSignal
QFileDialog, QFrame, QApplication, QCheckBox, QListWidget, QListWidgetItem, QDialog,
QStyledItemDelegate, QStyle
)
from PyQt6.QtCore import Qt, pyqtSignal, QSize
from PyQt6.QtGui import QColor, QFont, QFontMetrics
class SessionDelegate(QStyledItemDelegate):
"""Draw readable session rows with elided filenames and secondary details."""
def paint(self, painter, option, index):
painter.save()
selected = bool(option.state & QStyle.StateFlag.State_Selected)
hovered = bool(option.state & QStyle.StateFlag.State_MouseOver)
painter.fillRect(option.rect, QColor("#29243f" if selected else
"#222839" if hovered else "#191e29"))
rect = option.rect.adjusted(22, 20, -22, -20)
lines = str(index.data()).splitlines()
for i, text in enumerate(lines[:3]):
font = QFont("Segoe UI", 10)
font.setWeight(QFont.Weight.DemiBold if i == 0 else QFont.Weight.Normal)
painter.setFont(font)
painter.setPen(QColor("#edf1fa" if i == 0 else "#9ca9c0"))
line = rect.adjusted(0, i * 24, 0, 0)
line.setHeight(24)
text = QFontMetrics(font).elidedText(text, Qt.TextElideMode.ElideRight, line.width())
painter.drawText(line, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
painter.setPen(QColor("#2c3546"))
painter.drawLine(option.rect.bottomLeft(), option.rect.bottomRight())
if option.state & QStyle.StateFlag.State_HasFocus:
painter.setPen(QColor("#9b8bff"))
painter.drawRect(option.rect.adjusted(1, 1, -1, -1))
painter.restore()
class RoomCodeInput(QLineEdit):
"""QLineEdit that auto-fills from clipboard if it looks like a room code."""
@@ -23,9 +52,9 @@ class RoomCodeInput(QLineEdit):
class LobbyWidget(QWidget):
# Signals to communicate to VlcSyncApp
create_requested = pyqtSignal(str, str, str, object) # username, path, filename, size
join_requested = pyqtSignal(str, str) # username, room_code
file_selected = pyqtSignal(str)
refresh_requested = pyqtSignal()
join_requested = pyqtSignal(str, str) # username, room_code
file_selected = pyqtSignal(str)
refresh_requested = pyqtSignal()
def __init__(self):
super().__init__()
@@ -33,132 +62,239 @@ class LobbyWidget(QWidget):
self.local_file_path = None
self.local_file_name = None
self.local_file_size = 0
self.media_folder = ""
self.local_file_size = 0
self.media_folder = ""
self._setup_ui()
def _setup_ui(self):
self.setObjectName("desktopLobby")
self._connecting = False
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Container for centering
container = QFrame()
container.setObjectName("lobbyCard")
container.setMinimumWidth(420)
container.setMaximumWidth(500)
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(30, 30, 30, 30)
container_layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Brand
title = QLabel("VideoSync")
title.setObjectName("brandTitle")
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
tagline = QLabel("Watch together, anywhere")
tagline.setObjectName("brandTagline")
tagline.setAlignment(Qt.AlignmentFlag.AlignCenter)
container_layout.addWidget(title)
container_layout.addWidget(tagline)
container_layout.addSpacing(20)
header = QFrame()
header.setObjectName("lobbyHeader")
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(28, 18, 28, 18)
brand = QLabel("VideoSync")
brand.setObjectName("lobbyBrand")
header_layout.addWidget(brand)
header_layout.addStretch()
self.connection_label = QLabel("Watch together. Stay in sync.")
self.connection_label.setObjectName("lobbyMuted")
header_layout.addWidget(self.connection_label)
layout.addWidget(header)
# Username
body = QHBoxLayout()
body.setSpacing(0)
sidebar = QFrame()
sidebar.setObjectName("lobbySidebar")
sidebar.setFixedWidth(260)
side = QVBoxLayout(sidebar)
side.setContentsMargins(24, 32, 24, 24)
side.setSpacing(12)
title = QLabel("Watch together.")
title.setObjectName("sidebarTitle")
side.addWidget(title)
subtitle = QLabel("One room. Everyone in sync.")
subtitle.setObjectName("lobbyMuted")
side.addWidget(subtitle)
side.addSpacing(12)
self.create_action_btn = QPushButton("+ Create room")
self.create_action_btn.setObjectName("lobbyPrimary")
self.create_action_btn.clicked.connect(self.open_create_dialog)
side.addWidget(self.create_action_btn)
self.join_action_btn = QPushButton("Join by code")
self.join_action_btn.clicked.connect(self.open_join_dialog)
side.addWidget(self.join_action_btn)
side.addSpacing(28)
name_label = QLabel("Your display name")
side.addWidget(name_label)
self.username_input = QLineEdit()
self.username_input.setPlaceholderText("Enter a display name")
container_layout.addWidget(QLabel("YOUR NAME"))
container_layout.addWidget(self.username_input)
container_layout.addSpacing(20)
# Actions Layout (Create vs Join)
actions_layout = QHBoxLayout()
actions_layout.setSpacing(20)
# Create Room Panel
create_panel = QVBoxLayout()
create_panel.addWidget(QLabel("Create a Room"))
self.create_file_btn = QPushButton("Choose Video File")
self.create_file_btn.setObjectName("secondaryBtn")
self.username_input.setPlaceholderText("Enter your name")
self.username_input.setAccessibleName("Your display name")
side.addWidget(self.username_input)
name_label.setBuddy(self.username_input)
hint = QLabel("Remembered on this computer.")
hint.setObjectName("lobbyMuted")
hint.setWordWrap(True)
side.addWidget(hint)
side.addStretch()
privacy = QLabel("Your video stays on your computer.\nOnly playback and chat are shared.")
privacy.setObjectName("lobbyMuted")
privacy.setWordWrap(True)
side.addWidget(privacy)
body.addWidget(sidebar)
workspace = QVBoxLayout()
workspace.setContentsMargins(32, 32, 32, 24)
workspace.setSpacing(12)
heading = QHBoxLayout()
title_group = QVBoxLayout()
title_group.setSpacing(8)
title = QLabel("Active sessions")
title.setObjectName("workspaceTitle")
title_group.addWidget(title)
description = QLabel("See who’s watching and join in.")
description.setObjectName("lobbyMuted")
title_group.addWidget(description)
heading.addLayout(title_group)
heading.addStretch()
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.setObjectName("lobbyQuiet")
self.refresh_btn.clicked.connect(self.refresh_requested.emit)
heading.addWidget(self.refresh_btn)
workspace.addLayout(heading)
workspace.addSpacing(12)
self.sessions_status = QLabel("Checking for active sessions…")
self.sessions_status.setObjectName("lobbyMuted")
self.sessions_status.setWordWrap(True)
workspace.addWidget(self.sessions_status)
self.sessions_list = QListWidget()
self.sessions_list.setObjectName("lobbySessions")
self.sessions_list.setWordWrap(True)
self.sessions_list.setItemDelegate(SessionDelegate(self.sessions_list))
self.sessions_list.setAccessibleName("Active sessions")
workspace.addWidget(self.sessions_list, 1)
bottom = QHBoxLayout()
self.session_count = QLabel("Select a session to get started.")
self.session_count.setObjectName("lobbyMuted")
bottom.addWidget(self.session_count)
bottom.addStretch()
self.session_join_btn = QPushButton("Choose file && join")
self.session_join_btn.setObjectName("lobbyPrimary")
self.session_join_btn.setEnabled(False)
self.session_join_btn.clicked.connect(self.join_selected_session)
bottom.addWidget(self.session_join_btn)
workspace.addLayout(bottom)
self.sessions_list.itemDoubleClicked.connect(lambda _: self.join_selected_session())
self.sessions_list.itemSelectionChanged.connect(self.check_inputs)
body.addLayout(workspace, 1)
layout.addLayout(body, 1)
self.create_dialog = self._make_dialog("Create a room", "Choose a local video, then invite your friends.")
create = self.create_dialog.layout()
self.create_file_btn = QPushButton("Choose video file")
self.create_file_btn.clicked.connect(self.select_file)
create.addWidget(self.create_file_btn)
self.create_file_info = QLabel("")
self.create_file_info.setObjectName("fileInfo")
self.create_file_info.setObjectName("lobbyFileInfo")
self.create_file_info.setWordWrap(True)
self.create_file_info.hide()
self.create_room_btn = QPushButton("Create Room")
self.create_room_btn.setObjectName("primaryBtn")
self.create_room_btn.setEnabled(False)
create.addWidget(self.create_file_info)
self.listed_checkbox = QCheckBox("List room in active sessions")
self.listed_checkbox.setChecked(True)
create.addWidget(self.listed_checkbox)
hint = QLabel("Unlisted rooms can still be joined using their code.")
hint.setObjectName("lobbyMuted")
hint.setWordWrap(True)
create.addWidget(hint)
self.create_room_btn = QPushButton("Create room")
self.create_room_btn.setObjectName("lobbyPrimary")
self.create_room_btn.clicked.connect(self.create_room)
create_panel.addWidget(self.create_file_btn)
create_panel.addWidget(self.create_file_info)
self.listed_checkbox = QCheckBox("List room in active sessions")
self.listed_checkbox.setChecked(True)
self.listed_checkbox.setToolTip("Unlisted rooms can still be joined using their code.")
create_panel.addWidget(self.listed_checkbox)
create_panel.addWidget(self.create_room_btn)
# Join Room Panel
join_panel = QVBoxLayout()
join_panel.addWidget(QLabel("Join a Room"))
self._dialog_actions(self.create_dialog, self.create_room_btn)
self.join_dialog = self._make_dialog("Join by code", "Enter the room code your friend shared.")
self.room_code_input = RoomCodeInput()
self.room_code_input.setPlaceholderText("e.g. ABC123")
self.join_room_btn = QPushButton("Join Room")
self.join_room_btn.setObjectName("secondaryBtn")
self.join_room_btn.setEnabled(False)
self.room_code_input.setAccessibleName("Room code")
self.room_code_input.setMaxLength(8)
self.room_code_input.returnPressed.connect(self._join_if_valid)
self.join_dialog.layout().addWidget(self.room_code_input)
self.join_room_btn = QPushButton("Join room")
self.join_room_btn.setObjectName("lobbyPrimary")
self.join_room_btn.clicked.connect(self.join_room)
join_panel.addWidget(self.room_code_input)
join_panel.addWidget(self.join_room_btn)
actions_layout.addLayout(create_panel)
# Divider
divider = QLabel("OR")
divider.setAlignment(Qt.AlignmentFlag.AlignCenter)
divider.setObjectName("divider")
actions_layout.addWidget(divider)
actions_layout.addLayout(join_panel)
container_layout.addLayout(actions_layout)
panels = QHBoxLayout()
panels.addWidget(container)
sessions = QVBoxLayout()
heading = QHBoxLayout()
heading.addWidget(QLabel("ALREADY WATCHING"))
self.refresh_btn = QPushButton("Refresh")
self.refresh_btn.clicked.connect(self.refresh_requested.emit)
heading.addWidget(self.refresh_btn)
sessions.addLayout(heading)
self.sessions_status = QLabel("Checking for active sessions…")
self.sessions_status.setWordWrap(True)
sessions.addWidget(self.sessions_status)
self.sessions_list = QListWidget()
self.sessions_list.setWordWrap(True)
self.sessions_list.setMinimumWidth(280)
sessions.addWidget(self.sessions_list)
self.session_join_btn = QPushButton("Choose file & join")
self.session_join_btn.setEnabled(False)
self.session_join_btn.clicked.connect(self.join_selected_session)
self.sessions_list.itemDoubleClicked.connect(lambda _: self.join_selected_session())
self.sessions_list.itemSelectionChanged.connect(self.check_inputs)
sessions.addWidget(self.session_join_btn)
panels.addLayout(sessions)
layout.addLayout(panels)
# Signals to enable/disable buttons
self._dialog_actions(self.join_dialog, self.join_room_btn)
self.username_input.textChanged.connect(self.check_inputs)
self.room_code_input.textChanged.connect(self.check_inputs)
self._apply_lobby_style()
self.check_inputs()
def _make_dialog(self, title, description):
dialog = QDialog(self)
dialog.setWindowTitle(title)
dialog.setObjectName("lobbyDialog")
dialog.setModal(True)
dialog.setMinimumWidth(420)
layout = QVBoxLayout(dialog)
layout.setContentsMargins(28, 28, 28, 28)
layout.setSpacing(18)
label = QLabel(title)
label.setObjectName("dialogTitle")
layout.addWidget(label)
text = QLabel(description)
text.setObjectName("lobbyMuted")
text.setWordWrap(True)
layout.addWidget(text)
return dialog
def _dialog_actions(self, dialog, action):
row = QHBoxLayout()
row.addStretch()
cancel = QPushButton("Cancel")
cancel.clicked.connect(dialog.reject)
row.addWidget(cancel)
row.addWidget(action)
dialog.layout().addLayout(row)
def open_create_dialog(self):
if not self.username_input.text().strip():
self.username_input.setFocus()
return
self.create_dialog.show()
self.create_dialog.raise_()
self.create_file_btn.setFocus()
def open_join_dialog(self):
if not self.username_input.text().strip():
self.username_input.setFocus()
return
self.join_dialog.show()
self.join_dialog.raise_()
self.room_code_input.setFocus()
def _join_if_valid(self):
if self.join_room_btn.isEnabled():
self.join_room()
def _apply_lobby_style(self):
self.setStyleSheet("""
QWidget#desktopLobby, QDialog#lobbyDialog { background: #10131b; }
QLabel { color: #edf1fa; font-family: 'Segoe UI'; font-size: 13px; font-weight: normal; }
QFrame#lobbyHeader { background: #191e29; border-bottom: 1px solid #2c3546; }
QFrame#lobbySidebar { background: #191e29; border-right: 1px solid #2c3546; }
QLabel#lobbyBrand { font-size: 20px; font-weight: 600; }
QLabel#sidebarTitle, QLabel#dialogTitle { font-size: 23px; font-weight: 600; }
QLabel#workspaceTitle { font-size: 28px; font-weight: 600; }
QLabel#lobbyMuted { color: #9ca9c0; font-size: 12px; }
QLabel#lobbyFileInfo { background: #29243f; color: #c6bcff; padding: 14px; border-radius: 7px; }
QLineEdit { background: #10131b; color: #edf1fa; border: 1px solid #364157;
border-radius: 7px; padding: 11px; font: 14px 'Segoe UI'; }
QLineEdit:focus { border-color: #9b8bff; }
QPushButton { background: #191e29; color: #edf1fa; border: 1px solid #364157;
border-radius: 7px; padding: 11px 15px; font: 13px 'Segoe UI'; }
QPushButton:hover { background: #242b3b; border-color: #9b8bff; }
QPushButton#lobbyPrimary { background: #9b8bff; color: #141020; border: none; font-weight: 600; }
QPushButton#lobbyPrimary:hover { background: #b1a4ff; }
QPushButton:disabled, QPushButton#lobbyPrimary:disabled { background: #242b38; color: #738099; border-color: #2c3546; }
QPushButton#lobbyQuiet { background: transparent; }
QCheckBox { color: #edf1fa; spacing: 9px; font: 13px 'Segoe UI'; }
QListWidget#lobbySessions { background: #191e29; color: #edf1fa;
border: 1px solid #2c3546; border-radius: 9px; outline: none; font: 14px 'Segoe UI'; }
QListWidget#lobbySessions::item { padding: 20px; border-bottom: 1px solid #2c3546; }
QListWidget#lobbySessions::item:selected { background: #29243f; color: #edf1fa; }
QListWidget#lobbySessions::item:hover { background: #222839; }
QListWidget#lobbySessions:focus { border-color: #9b8bff; }
""")
def _set_local_file(self, file_path: str):
self.local_file_path = file_path
self.local_file_name = os.path.basename(file_path)
self.local_file_size = os.path.getsize(file_path)
self.media_folder = os.path.dirname(file_path)
self.file_selected.emit(file_path)
self.local_file_size = os.path.getsize(file_path)
self.media_folder = os.path.dirname(file_path)
self.file_selected.emit(file_path)
size_mb = self.local_file_size / (1024 * 1024)
self.create_file_info.setText(f"{self.local_file_name}\n{size_mb:.1f} MB")
@@ -167,7 +303,7 @@ class LobbyWidget(QWidget):
def select_file(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Select Video File", self.media_folder, "Video Files (*.mp4 *.mkv *.avi *.mov *.webm);;All Files (*)"
self, "Select Video File", self.media_folder, "Video Files (*.mp4 *.mkv *.avi *.mov *.webm);;All Files (*)"
)
if file_path:
self._set_local_file(file_path)
@@ -185,6 +321,7 @@ class LobbyWidget(QWidget):
ext = os.path.splitext(file_path)[1].lower()
if ext in ['.mp4', '.mkv', '.avi', '.mov', '.webm']:
self._set_local_file(file_path)
self.open_create_dialog()
break
def check_inputs(self):
@@ -192,36 +329,47 @@ class LobbyWidget(QWidget):
has_file = self.local_file_path is not None
has_code = len(self.room_code_input.text().strip()) >= 4
self.create_room_btn.setEnabled(has_name and has_file)
self.join_room_btn.setEnabled(has_name and has_code)
self.session_join_btn.setEnabled(has_name and self.sessions_list.currentItem() is not None)
def update_sessions(self, rooms):
selected = self.sessions_list.currentItem()
code = selected.data(Qt.ItemDataRole.UserRole) if selected else None
self.sessions_list.clear()
for room in rooms:
seconds = max(0, int(room.get("position", 0)))
state = "Playing" if room.get("playing") else "Paused"
users = room.get("users", [])
item = QListWidgetItem(f"{room['fileInfo']['name']}\n"
f"{state} · {seconds // 60}:{seconds % 60:02d} · {len(users)} connected\n"
f"{', '.join(users)}")
item.setData(Qt.ItemDataRole.UserRole, room['code'])
self.sessions_list.addItem(item)
if room['code'] == code:
self.sessions_list.setCurrentItem(item)
self.sessions_status.setText("Select a session to join." if rooms else
"No active sessions. Create a room to get started.")
self.check_inputs()
def join_selected_session(self):
item = self.sessions_list.currentItem()
if item and self.username_input.text().strip():
self.room_code_input.setText(item.data(Qt.ItemDataRole.UserRole))
self.join_room()
self.create_room_btn.setEnabled(has_name and has_file and not self._connecting)
self.create_action_btn.setEnabled(has_name and not self._connecting)
self.join_action_btn.setEnabled(has_name and not self._connecting)
self.join_room_btn.setEnabled(has_name and has_code and not self._connecting)
self.session_join_btn.setEnabled(has_name and self.sessions_list.currentItem() is not None and not self._connecting)
def update_sessions(self, rooms):
selected = self.sessions_list.currentItem()
code = selected.data(Qt.ItemDataRole.UserRole) if selected else None
self.sessions_list.clear()
for room in rooms:
seconds = max(0, int(room.get("position", 0)))
state = "Playing" if room.get("playing") else "Paused"
users = room.get("users", [])
item = QListWidgetItem(f"{room['fileInfo']['name']}\n"
f"{state} · {seconds // 60}:{seconds % 60:02d} · {len(users)} connected\n"
f"{', '.join(users)}")
item.setSizeHint(QSize(0, 116))
item.setToolTip(item.text())
item.setData(Qt.ItemDataRole.UserRole, room['code'])
self.sessions_list.addItem(item)
if room['code'] == code:
self.sessions_list.setCurrentItem(item)
self.session_count.setText(f"{len(rooms)} room{'s' if len(rooms) != 1 else ''} available")
self.sessions_list.setVisible(bool(rooms))
self.sessions_status.setText("Select a session to join." if rooms else
"No active sessions. Create a room to get started.")
self.check_inputs()
def join_selected_session(self):
item = self.sessions_list.currentItem()
if item and self.username_input.text().strip():
self.room_code_input.setText(item.data(Qt.ItemDataRole.UserRole))
self.join_room()
def create_room(self):
if not self.create_room_btn.isEnabled():
return
self._connecting = True
self.create_dialog.accept()
self.check_inputs()
username = self.username_input.text().strip()
self.create_room_btn.setText("Connecting...")
self.create_room_btn.setEnabled(False)
@@ -229,6 +377,11 @@ class LobbyWidget(QWidget):
self.create_requested.emit(username, self.local_file_path, self.local_file_name, self.local_file_size)
def join_room(self):
if not self.join_room_btn.isEnabled():
return
self._connecting = True
self.join_dialog.accept()
self.check_inputs()
username = self.username_input.text().strip()
room_code = self.room_code_input.text().strip().upper()
self.join_room_btn.setText("Connecting...")
@@ -237,8 +390,9 @@ class LobbyWidget(QWidget):
self.join_requested.emit(username, room_code)
def reset_ui(self):
self.create_room_btn.setText("Create Room")
self.join_room_btn.setText("Join Room")
self._connecting = False
self.create_room_btn.setText("Create room")
self.join_room_btn.setText("Join room")
self.check_inputs()
def clear_file(self):
+2
View File
@@ -109,6 +109,8 @@ class VlcSyncApp(QMainWindow):
self.lobby_widget.update_sessions(rooms)
except (ValueError, KeyError, TypeError, OverflowError):
self.lobby_widget.sessions_list.clear()
self.lobby_widget.sessions_list.hide()
self.lobby_widget.session_count.setText("Join by code is still available.")
self.lobby_widget.check_inputs()
self.lobby_widget.sessions_status.setText("Couldn’t check for sessions. You can retry or join by code.")
finally:
+29
View File
@@ -1,4 +1,5 @@
from lobby_widget import LobbyWidget
from PyQt6.QtCore import Qt
def test_session_selection_uses_existing_join_flow(qtbot):
@@ -21,3 +22,31 @@ def test_session_selection_uses_existing_join_flow(qtbot):
lobby.update_sessions([])
assert not lobby.session_join_btn.isEnabled()
assert "No active sessions" in lobby.sessions_status.text()
def test_workspace_dialogs_and_connection_lock(qtbot, tmp_path):
lobby = LobbyWidget()
qtbot.addWidget(lobby)
lobby.show()
assert not lobby.create_action_btn.isEnabled()
lobby.username_input.setText("Alex")
qtbot.mouseClick(lobby.create_action_btn, Qt.MouseButton.LeftButton)
assert lobby.create_dialog.isVisible()
assert not lobby.create_room_btn.isEnabled()
video = tmp_path / "movie.mkv"
video.write_bytes(b"local video")
lobby._set_local_file(str(video))
lobby.listed_checkbox.setChecked(False)
with qtbot.waitSignal(lobby.create_requested) as signal:
qtbot.mouseClick(lobby.create_room_btn, Qt.MouseButton.LeftButton)
assert signal.args == ["Alex", str(video), "movie.mkv", 11]
assert not lobby.create_dialog.isVisible()
assert not lobby.create_action_btn.isEnabled()
assert not lobby.join_action_btn.isEnabled()
lobby.reset_ui()
qtbot.mouseClick(lobby.join_action_btn, Qt.MouseButton.LeftButton)
lobby.room_code_input.setText("abc123")
with qtbot.waitSignal(lobby.join_requested) as signal:
qtbot.keyClick(lobby.room_code_input, Qt.Key.Key_Return)
assert signal.args == ["Alex", "ABC123"]
assert not lobby.join_dialog.isVisible()