"""PyQt6 help system - reuses Textual TUI help logic and components."""
import logging
from typing import Union, Callable, Optional
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QTextEdit, QScrollArea, QWidget, QMessageBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QCursor, QGuiApplication
# REUSE the actual working Textual TUI help components
from python_introspect import DocstringExtractor
from pyqt_reactive.theming import ColorScheme
from pyqt_reactive.theming import StyleSheetGenerator
logger = logging.getLogger(__name__)
[docs]
class BaseHelpWindow(QDialog):
"""Base class for all PyQt6 help windows - reuses Textual TUI help logic."""
[docs]
def __init__(self, title: str = "Help", color_scheme: Optional[ColorScheme] = None, parent=None):
super().__init__(parent)
# Initialize color scheme and style generator
self.color_scheme = color_scheme or ColorScheme()
self.style_generator = StyleSheetGenerator(self.color_scheme)
self.setWindowTitle(title)
self.setModal(False) # Allow interaction with main window
# Setup UI
self.setup_ui()
# Apply centralized styling
self.setStyleSheet(self.style_generator.generate_dialog_style())
[docs]
def setup_ui(self):
"""Setup the base help window UI."""
layout = QVBoxLayout(self)
# Content area (to be filled by subclasses)
self.content_area = QScrollArea()
self.content_area.setWidgetResizable(True)
self.content_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.content_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
layout.addWidget(self.content_area)
# Close button - styled like other buttons
button_layout = QHBoxLayout()
button_layout.addStretch()
close_btn = QPushButton("Close")
close_btn.clicked.connect(self.close)
close_btn.setStyleSheet(f"""
QPushButton {{
background-color: {self.color_scheme.to_hex(self.color_scheme.button_normal_bg)};
color: {self.color_scheme.to_hex(self.color_scheme.button_text)};
border: none;
padding: 6px 12px;
border-radius: 3px;
font-weight: normal;
}}
QPushButton:hover {{
background-color: {self.color_scheme.to_hex(self.color_scheme.button_hover_bg)};
}}
QPushButton:pressed {{
background-color: {self.color_scheme.to_hex(self.color_scheme.button_pressed_bg)};
}}
""")
button_layout.addWidget(close_btn)
layout.addLayout(button_layout)
[docs]
class DocstringHelpWindow(BaseHelpWindow):
"""Help window for functions and classes - reuses Textual TUI DocstringExtractor."""
[docs]
def __init__(self, target: Union[Callable, type], title: Optional[str] = None,
color_scheme: Optional[ColorScheme] = None, parent=None):
self.target = target
# REUSE Textual TUI docstring extraction logic
self.docstring_info = DocstringExtractor.extract(target)
# Generate title from target if not provided
if title is None:
if hasattr(target, '__name__'):
title = f"Help: {target.__name__}"
else:
title = "Help"
super().__init__(title, color_scheme, parent)
self.populate_content()
[docs]
def populate_content(self):
"""Populate the help content with minimal styling."""
import logging
logger = logging.getLogger(__name__)
logger.info(f"🔍 populate_content() CALLED")
logger.info(f"🔍 docstring_info.summary: {bool(self.docstring_info.summary)}")
logger.info(f"🔍 docstring_info.description: {bool(self.docstring_info.description)}")
logger.info(f"🔍 docstring_info.parameters: {bool(self.docstring_info.parameters)}")
logger.info(f"🔍 docstring_info.returns: {bool(self.docstring_info.returns)}")
logger.info(f"🔍 docstring_info.examples: {bool(self.docstring_info.examples)}")
logger.info(f"🔍 Window parent: {self.parent()}, type: {type(self.parent()).__name__ if self.parent() else 'None'}")
logger.info(f"🔍 Color scheme: {self.color_scheme}")
content_widget = QWidget()
layout = QVBoxLayout(content_widget)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# Function/class summary
if self.docstring_info.summary:
logger.debug(f"🔍 populate_content: summary={self.docstring_info.summary[:50]}...")
summary_label = QLabel(self.docstring_info.summary)
summary_label.setWordWrap(True)
summary_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
summary_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; background-color: {self.color_scheme.to_hex(self.color_scheme.panel_bg)}; padding: 5px;")
layout.addWidget(summary_label)
logger.info(f"🔍 Added summary_label with style: {summary_label.styleSheet()}")
# Full description
if self.docstring_info.description:
logger.debug(f"🔍 populate_content: description={self.docstring_info.description[:50]}...")
desc_label = QLabel(self.docstring_info.description)
desc_label.setWordWrap(True)
desc_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
desc_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; background-color: {self.color_scheme.to_hex(self.color_scheme.panel_bg)}; padding: 5px;")
layout.addWidget(desc_label)
# Parameters section
if self.docstring_info.parameters:
params_label = QLabel("Parameters:")
params_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
params_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;")
layout.addWidget(params_label)
for param_name, param_desc in self.docstring_info.parameters.items():
# Parameter name
name_label = QLabel(f"• {param_name}")
name_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
name_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 5px; margin-top: 3px;")
layout.addWidget(name_label)
# Parameter description
if param_desc:
desc_label = QLabel(param_desc)
desc_label.setWordWrap(True)
desc_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
desc_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 20px;")
layout.addWidget(desc_label)
# Returns section
if self.docstring_info.returns:
returns_label = QLabel("Returns:")
returns_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
returns_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;")
layout.addWidget(returns_label)
returns_desc = QLabel(self.docstring_info.returns)
returns_desc.setWordWrap(True)
returns_desc.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
returns_desc.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; font-size: 12px; margin-left: 5px;")
layout.addWidget(returns_desc)
# Examples section
if self.docstring_info.examples:
examples_label = QLabel("Examples:")
examples_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
examples_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-size: 14px; font-weight: bold; margin-top: 8px;")
layout.addWidget(examples_label)
examples_text = QTextEdit()
examples_text.setPlainText(self.docstring_info.examples)
examples_text.setReadOnly(True)
examples_text.setMaximumHeight(150)
examples_text.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
examples_text.setStyleSheet(f"""
QTextEdit {{
background-color: transparent;
color: {self.color_scheme.to_hex(self.color_scheme.text_primary)};
border: none;
font-family: monospace;
font-size: 11px;
}}
QTextEdit:hover {{
background-color: transparent;
}}
""")
layout.addWidget(examples_text)
layout.addStretch()
self.content_area.setWidget(content_widget)
logger.info(f"🔍 populate_content() COMPLETED - content_widget children: {content_widget.children()}")
logger.info(f"🔍 Window stylesheet: {self.styleSheet()[:200]}...")
# Auto-size to content
self.adjustSize()
# Set reasonable min/max sizes
self.setMaximumSize(800, 600)
[docs]
class HelpWindowManager:
"""PyQt6 help window manager - unified window for all help content."""
# Class-level window reference for singleton behavior
_help_window = None
@classmethod
def _position_window_near_cursor(cls, window: QDialog) -> None:
"""Position help window near the mouse cursor within screen bounds."""
cursor_pos = QCursor.pos()
screen = QGuiApplication.screenAt(cursor_pos)
if screen is None:
screen = QGuiApplication.primaryScreen()
if screen is None:
return
available = screen.availableGeometry()
window.adjustSize()
size = window.size()
x = cursor_pos.x() - size.width() - 16
y = cursor_pos.y() - size.height() - 16
if x < available.left():
x = available.left()
if y < available.top():
y = available.top()
if x + size.width() > available.right():
x = max(available.left(), available.right() - size.width())
if y + size.height() > available.bottom():
y = max(available.top(), available.bottom() - size.height())
window.move(x, y)
[docs]
@classmethod
def show_docstring_help(cls, target: Union[Callable, type], title: Optional[str] = None, parent=None):
"""Show help for a function or class - reuses Textual TUI extraction logic."""
import logging
logger = logging.getLogger(__name__)
logger.info(f"🔍 show_docstring_help() CALLED - target={target}, title={title}")
logger.info(f"🔍 show_docstring_help() parent={parent}, parent_type={type(parent).__name__ if parent else 'None'}")
try:
# Check if existing window is still valid
if isinstance(cls._help_window, QDialog):
try:
if not cls._help_window.isHidden():
logger.info(f"🔍 Reusing existing help window")
cls._help_window.target = target
cls._help_window.docstring_info = DocstringExtractor.extract(target)
cls._help_window.setWindowTitle(title or f"Help: {getattr(target, '__name__', 'Unknown')}")
cls._help_window.populate_content()
cls._position_window_near_cursor(cls._help_window)
cls._help_window.raise_()
cls._help_window.activateWindow()
return
except RuntimeError:
# Window was deleted, clear reference
cls._help_window = None
# Create new window
logger.info(f"🔍 Creating new DocstringHelpWindow")
cls._help_window = DocstringHelpWindow(target, title=title, parent=parent)
logger.info(f"🔍 DocstringHelpWindow created, calling show()")
cls._help_window.show()
cls._position_window_near_cursor(cls._help_window)
logger.info(f"🔍 DocstringHelpWindow shown")
except Exception as e:
logger.error(f"Failed to show docstring help: {e}")
QMessageBox.warning(parent, "Help Error", f"Failed to show help: {e}")
[docs]
@classmethod
def show_parameter_help(cls, param_name: str, param_description: str, param_type: type = None, parent=None):
"""Show help for a parameter - creates a fake docstring object and uses DocstringHelpWindow."""
import logging
logger = logging.getLogger(__name__)
try:
# Create a fake docstring info object for the parameter
from dataclasses import dataclass
@dataclass
class FakeDocstringInfo:
summary: str = ""
description: str = ""
parameters: dict = None
returns: str = ""
examples: str = ""
# Build parameter display - combine everything into summary to create single QLabel
type_str = f" ({getattr(param_type, '__name__', str(param_type))})" if param_type else ""
param_desc = param_description or "No description available"
fake_info = FakeDocstringInfo(
summary=f"• {param_name}{type_str}\n\n{param_desc}",
description="", # Empty description so only 1 QLabel is created
parameters={},
returns="",
examples=""
)
logger.debug(f"🔍 show_parameter_help: param_name={param_name}, param_description={param_description[:50] if param_description else 'None'}")
# Check if existing window is still valid
if isinstance(cls._help_window, QDialog):
try:
if not cls._help_window.isHidden():
cls._help_window.docstring_info = fake_info
cls._help_window.setWindowTitle(f"Parameter: {param_name}")
cls._help_window.populate_content()
cls._position_window_near_cursor(cls._help_window)
cls._help_window.raise_()
cls._help_window.activateWindow()
return
except RuntimeError:
# Window was deleted, clear reference
cls._help_window = None
# Create new window with fake target
class FakeTarget:
__name__ = param_name
cls._help_window = DocstringHelpWindow(FakeTarget, title=f"Parameter: {param_name}", parent=parent)
cls._help_window.docstring_info = fake_info
cls._help_window.populate_content()
cls._help_window.show()
cls._position_window_near_cursor(cls._help_window)
except Exception as e:
logger.error(f"Failed to show parameter help: {e}")
QMessageBox.warning(parent, "Help Error", f"Failed to show help: {e}")