Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ui/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Dialog components for the Plugin Configurator application."""

from ui.dialogs.preset_management_dialog import PresetManagementDialog
from ui.dialogs.success_dialog import SuccessDialog

__all__ = ["PresetManagementDialog"]
__all__ = ["PresetManagementDialog", "SuccessDialog"]
274 changes: 274 additions & 0 deletions src/ui/dialogs/success_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
"""Success dialog shown after a project is generated successfully."""

from __future__ import annotations

import platform
import shutil
import subprocess
from pathlib import Path

from PySide6.QtCore import Qt, QTimer, QUrl, Slot
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)

# Emoji frames for the celebration animation
_CELEBRATION_FRAMES = ["🎉", "🎊", "✨", "🌟", "⭐", "✨", "🎊", "🎉"]

# IDE definitions: (display_name, executable, args_before_path)
_IDE_DEFINITIONS: list[tuple[str, str, list[str]]] = [
("VSCode", "code", ["."]),
("CLion", "clion", ["."]),
("Xcode", "xcode-select", []), # macOS only - we handle separately
]


Comment on lines +27 to +34

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_IDE_DEFINITIONS is declared but never used. Either remove it to avoid confusing future readers, or refactor _detect_ides to build from this table so the definitions stay in one place.

Suggested change
# IDE definitions: (display_name, executable, args_before_path)
_IDE_DEFINITIONS: list[tuple[str, str, list[str]]] = [
("VSCode", "code", ["."]),
("CLion", "clion", ["."]),
("Xcode", "xcode-select", []), # macOS only - we handle separately
]

Copilot uses AI. Check for mistakes.
def _detect_ides(project_path: str) -> list[tuple[str, callable]]:
"""Return a list of (label, open_callable) for IDEs available on this machine.

Args:
project_path: Absolute path to the generated project directory.

Returns:
List of (display_label, callable) tuples where calling the callable
opens the project in the corresponding IDE.
"""
available: list[tuple[str, callable]] = []
current_os = platform.system()
Comment on lines +35 to +46

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotations use the built-in callable (a function) as a type (list[tuple[str, callable]]). With mypy enabled in this repo, this will be flagged as an invalid type. Use collections.abc.Callable (e.g., Callable[[], None] or Callable[[str], None]) and update both the return type and local available annotation accordingly.

Copilot uses AI. Check for mistakes.

# VSCode
if shutil.which("code"):
def _open_vscode(path: str = project_path) -> None:
subprocess.Popen(["code", path])

available.append(("VSCode", _open_vscode))

# CLion
if shutil.which("clion"):
def _open_clion(path: str = project_path) -> None:
subprocess.Popen(["clion", path])

available.append(("CLion", _open_clion))

# Xcode (macOS only)
if current_os == "Darwin" and shutil.which("xcodebuild"):
def _open_xcode(path: str = project_path) -> None:
# Look for an .xcodeproj or .xcworkspace in the project directory
p = Path(path)
for pattern in ("*.xcworkspace", "*.xcodeproj"):
matches = list(p.glob(pattern))
if matches:
subprocess.Popen(["open", str(matches[0])])
return
# Fall back to opening the folder
subprocess.Popen(["open", path])

available.append(("Xcode", _open_xcode))

return available


def _open_in_file_manager(path: str) -> None:
"""Open *path* in the native file manager, cross-platform."""
current_os = platform.system()
if current_os == "Darwin":
subprocess.Popen(["open", path])
elif current_os == "Windows":
subprocess.Popen(["explorer", path])
elif shutil.which("xdg-open"):
# Linux / other POSIX - try xdg-open, fall back to QDesktopServices
subprocess.Popen(["xdg-open", path])
else:
QDesktopServices.openUrl(QUrl.fromLocalFile(path))


def _file_manager_label() -> str:
"""Return the platform-appropriate label for the file manager action."""
current_os = platform.system()
if current_os == "Darwin":
return "Open in Finder"
if current_os == "Windows":
return "Open in Explorer"
return "Open in Files"


class SuccessDialog(QDialog):
"""Dialog displayed after a project is generated successfully.

Features:
- Animated celebration header
- Project name and output location display
- "Open in IDE" buttons for each detected IDE (VSCode, Xcode, CLion)
- Platform-aware "Open in Finder/Explorer/Files" button
- "Close" button
- Green-accented styling for the success state
"""

def __init__(
self,
project_name: str,
output_directory: str,
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self._project_name = project_name
self._output_directory = output_directory
self._animation_index = 0

self.setWindowTitle("Project Generated Successfully")
self.setMinimumWidth(480)
self.setMinimumHeight(280)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)

self._setup_ui()
self._start_animation()

# ------------------------------------------------------------------
# UI construction
# ------------------------------------------------------------------

def _setup_ui(self) -> None:
"""Build the dialog layout."""
root = QVBoxLayout(self)
root.setContentsMargins(24, 24, 24, 20)
root.setSpacing(16)

# Celebration header
header_frame = QFrame()
header_frame.setObjectName("successHeader")
header_frame.setStyleSheet(
"#successHeader {"
" background-color: #1e7e34;"
" border-radius: 8px;"
"}"
)
header_layout = QVBoxLayout(header_frame)
header_layout.setContentsMargins(16, 12, 16, 12)
header_layout.setSpacing(4)

self._animation_label = QLabel("🎉")
self._animation_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._animation_label.setStyleSheet("font-size: 32px;")
header_layout.addWidget(self._animation_label)

success_text = QLabel("Project Generated Successfully!")
success_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
success_text.setStyleSheet(
"color: #ffffff; font-size: 16px; font-weight: bold;"
)
header_layout.addWidget(success_text)

root.addWidget(header_frame)

# Project info panel
info_frame = QFrame()
info_frame.setFrameShape(QFrame.Shape.StyledPanel)
info_layout = QVBoxLayout(info_frame)
info_layout.setContentsMargins(12, 10, 12, 10)
info_layout.setSpacing(6)

name_row = QHBoxLayout()
name_title = QLabel("<b>Project:</b>")
name_title.setFixedWidth(80)
self._name_label = QLabel(self._project_name or "\u2014")
self._name_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
name_row.addWidget(name_title)
name_row.addWidget(self._name_label, stretch=1)
info_layout.addLayout(name_row)

path_row = QHBoxLayout()
path_title = QLabel("<b>Location:</b>")
path_title.setFixedWidth(80)
self._path_label = QLabel(self._output_directory or "\u2014")
self._path_label.setWordWrap(True)
self._path_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
path_row.addWidget(path_title)
path_row.addWidget(self._path_label, stretch=1)
info_layout.addLayout(path_row)

root.addWidget(info_frame)

# Action buttons
actions_layout = QVBoxLayout()
actions_layout.setSpacing(8)

# File-manager button (always shown)
fm_btn = QPushButton(f"\U0001f4c1 {_file_manager_label()}")
fm_btn.setMinimumHeight(36)
fm_btn.setToolTip(f"Open the project folder:\n{self._output_directory}")
fm_btn.clicked.connect(self._on_open_in_file_manager)
actions_layout.addWidget(fm_btn)

# IDE buttons (only for detected IDEs)
self._ide_actions = _detect_ides(self._output_directory)
if self._ide_actions:
ide_row = QHBoxLayout()
ide_row.setSpacing(8)
for ide_label, ide_fn in self._ide_actions:
btn = self._make_ide_button(ide_label, ide_fn)
ide_row.addWidget(btn)
actions_layout.addLayout(ide_row)
Comment on lines +215 to +223

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDE actions are detected even when output_directory is empty (the UI shows an em dash, but IDE buttons may still appear if an IDE is on PATH). Clicking those buttons will launch the IDE with an empty path, which can open the wrong location or fail. Guard IDE detection/button creation behind a truthy self._output_directory (and/or have _detect_ides return [] when the path is empty).

Copilot uses AI. Check for mistakes.

root.addLayout(actions_layout)

# Dialog close button
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
button_box.rejected.connect(self.accept)

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

button_box uses the standard Close button but connects rejected to self.accept(). This makes the dialog return Accepted when the user clicks Close, which is inconsistent with Qt semantics for a Close/RejectRole action. Connect to self.reject() (or self.close()) instead.

Suggested change
button_box.rejected.connect(self.accept)
button_box.rejected.connect(self.reject)

Copilot uses AI. Check for mistakes.
root.addWidget(button_box)

@staticmethod
def _make_ide_button(label: str, callback: callable) -> QPushButton:
"""Return a styled IDE button that calls *callback* when clicked."""
icon_map = {"VSCode": "\U0001f4bb", "Xcode": "\U0001f528", "CLion": "\U0001f6e0"}
icon = icon_map.get(label, "\U0001f5a5")
btn = QPushButton(f"{icon} Open in {label}")
btn.setMinimumHeight(36)
btn.setToolTip(f"Open the project in {label}")
btn.clicked.connect(callback)
return btn
Comment on lines +232 to +241

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_make_ide_button annotates callback as callable, which is not a valid typing annotation under mypy. Use collections.abc.Callable with an appropriate signature (e.g., Callable[[], None]) to match what clicked.connect expects.

Copilot uses AI. Check for mistakes.

# ------------------------------------------------------------------
# Animation
# ------------------------------------------------------------------

def _start_animation(self) -> None:
"""Start the celebration emoji cycling animation."""
self._timer = QTimer(self)
self._timer.setInterval(400)
self._timer.timeout.connect(self._advance_frame)
self._timer.start()

@Slot()
def _advance_frame(self) -> None:
"""Advance to the next animation frame."""
self._animation_index = (self._animation_index + 1) % len(_CELEBRATION_FRAMES)
self._animation_label.setText(_CELEBRATION_FRAMES[self._animation_index])

# ------------------------------------------------------------------
# Slots
# ------------------------------------------------------------------

@Slot()
def _on_open_in_file_manager(self) -> None:
"""Open the project directory in the native file manager."""
if self._output_directory:
_open_in_file_manager(self._output_directory)

def closeEvent(self, event) -> None:
"""Stop the animation timer before closing."""
self._timer.stop()
super().closeEvent(event)

20 changes: 7 additions & 13 deletions src/ui/tabs/generate_tab.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Generate Tab - summary review and project generation."""

from PySide6.QtCore import Qt, QThread, QUrl, Slot
from PySide6.QtGui import QDesktopServices
from PySide6.QtCore import Qt, QThread, Slot
from PySide6.QtWidgets import (
QGroupBox,
QHBoxLayout,
Expand All @@ -19,6 +18,7 @@
from core.base_tab import BaseTab
from core.project_worker import ProjectWorker
from ui.components.validation_footer import ValidationFooter
from ui.dialogs.success_dialog import SuccessDialog


class GenerateTab(BaseTab):
Expand Down Expand Up @@ -515,15 +515,9 @@ def _on_generation_finished(self) -> None:
self._log_text.append("\n=== Generation Complete ===")
self._log_text.append("Project generated successfully!")

output_dir = self._full_config.get("project_info", {}).get("output_directory", "")

msg_box = QMessageBox(self)
msg_box.setWindowTitle("Project Generated")
msg_box.setText(f"Project generated successfully!\n\nLocation: {output_dir}")
msg_box.setIcon(QMessageBox.Icon.Information)
open_btn = msg_box.addButton("Open Folder", QMessageBox.ButtonRole.ActionRole)
msg_box.addButton("Close", QMessageBox.ButtonRole.RejectRole)
msg_box.exec()
project_info = self._full_config.get("project_info", {})
project_name = project_info.get("project_name", "")
output_dir = project_info.get("output_directory", "")

if msg_box.clickedButton() is open_btn and output_dir:
QDesktopServices.openUrl(QUrl.fromLocalFile(output_dir))
dlg = SuccessDialog(project_name, output_dir, parent=self)
dlg.exec()
Loading
Loading