Add Qt5/Qt6 compatibility layer for IDA 9.1+ support

Add qt_compat.py shim that tries PySide6 first, falls back to PyQt5,
and re-exports all Qt classes under unified names. Replace all direct
PySide6 imports across 22 files with qt_compat imports. Normalize
scoped enums to flat enums for PyQt5 compatibility, replace .exec()
with exec_dialog() wrapper, and fix QTimeZone.utc() portability.

Bump version to 1.2.0 and add IDA 9.1 to supported versions.
This commit is contained in:
Jason Tang
2026-03-14 14:42:24 -04:00
parent fcf2e50c7b
commit eefc4c8792
23 changed files with 128 additions and 110 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
"plugin": {
"name": "IDAssist",
"entryPoint": "idassist_plugin.py",
"version": "1.1.0",
"version": "1.2.0",
"description": "AI-powered reverse engineering plugin with LLM analysis, semantic knowledge graphs, RAG search, and SymGraph collaboration",
"license": "MIT",
"urls": {
@@ -16,7 +16,7 @@
}
],
"categories": ["integration-with-third-parties-interoperability", "api-scripting-and-automation"],
"idaVersions": ["9.2", "9.3", "9.4"],
"idaVersions": ["9.1", "9.2", "9.3", "9.4"],
"logoPath": "docs/screenshots/icon.png",
"keywords": ["llm", "ai", "reverse-engineering", "analysis", "knowledge-graph", "symgraph"],
"pythonDependencies": [
+1 -1
View File
@@ -8,7 +8,7 @@ Follows the same patterns as ExplainController and QueryController.
"""
from typing import Optional, Dict, Any, List
from PySide6.QtCore import QThread, Signal, Qt
from ..qt_compat import QThread, Signal, Qt
from src.services.binary_context_service import BinaryContextService, ViewLevel
from src.services.actions_service import ActionsService
from src.services.settings_service import SettingsService
+1 -1
View File
@@ -9,7 +9,7 @@ Follows the same patterns as ExplainLLMThread but specialized for Actions tools.
import asyncio
from typing import List, Dict, Any
from PySide6.QtCore import QThread, Signal
from ..qt_compat import QThread, Signal
from ..services.models.llm_models import ToolCall, ChatRequest, ChatMessage, MessageRole
from src.ida_compat import log
+1 -1
View File
@@ -3,7 +3,7 @@
from typing import Optional, Dict, Any, List
from datetime import datetime
import asyncio
from PySide6.QtCore import QThread, Signal, QTimer
from ..qt_compat import QThread, Signal, QTimer
from src.services.binary_context_service import BinaryContextService, ViewLevel
from src.services.llm_providers.provider_factory import LLMProviderFactory
from src.services.settings_service import SettingsService
+4 -7
View File
@@ -2,7 +2,7 @@
from typing import Optional, Dict, Any, List, Callable
import asyncio
from PySide6.QtCore import QDateTime, QThread, QTimer, Signal, Qt
from ..qt_compat import QDateTime, QThread, QTimer, Signal, Qt, utc_timezone
from src.ida_compat import log
from src.services.binary_context_service import BinaryContextService, ViewLevel
from src.services.llm_providers.provider_factory import LLMProviderFactory
@@ -688,11 +688,9 @@ class QueryController:
def _convert_utc_to_local_display(self, utc_timestamp: str) -> str:
"""Convert UTC timestamp from database to local display format"""
try:
from PySide6.QtCore import QDateTime, QTimeZone
# Parse the UTC timestamp and explicitly set it as UTC time
qdt_utc = QDateTime.fromString(utc_timestamp, "yyyy-MM-dd hh:mm:ss")
qdt_utc.setTimeZone(QTimeZone.utc()) # This time is in UTC
qdt_utc.setTimeZone(utc_timezone()) # This time is in UTC
# Convert to local time zone
qdt_local = qdt_utc.toLocalTime()
@@ -706,8 +704,7 @@ class QueryController:
def _add_chat_to_history_direct(self, chat_id: int, description: str, timestamp: str):
"""Add chat to history table directly without triggering signals (for bulk loading)"""
from PySide6.QtWidgets import QTableWidgetItem
from PySide6.QtCore import Qt
from ..qt_compat import QTableWidgetItem
row_count = self.view.history_table.rowCount()
self.view.history_table.insertRow(row_count)
@@ -1095,7 +1092,7 @@ class QueryController:
return
# Show confirmation dialog
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
chat_list = "\n".join([f"• {name}" for name in chat_names[:5]]) # Show first 5
if len(chat_names) > 5:
chat_list += f"\n• ... and {len(chat_names) - 5} more"
+2 -3
View File
@@ -2,8 +2,7 @@
import os
from typing import List, Optional
from PySide6.QtWidgets import QFileDialog, QMessageBox
from PySide6.QtCore import QThread, Signal
from ..qt_compat import QFileDialog, QMessageBox, QThread, Signal, exec_dialog
from src.services.rag_service import rag_service
from src.services.models.rag_models import IngestRequest, SearchRequest, SearchType
@@ -80,7 +79,7 @@ class RAGController:
])
file_dialog.setWindowTitle("Select Documents to Add to RAG Index")
if file_dialog.exec():
if exec_dialog(file_dialog):
selected_files = file_dialog.selectedFiles()
if selected_files:
self._ingest_documents(selected_files)
+1 -1
View File
@@ -9,7 +9,7 @@ in background with Qt signal/slot communication.
import asyncio
from typing import List, Dict, Any, Optional
from PySide6.QtCore import QThread, Signal
from ..qt_compat import QThread, Signal
from src.services.react.react_orchestrator import ReActOrchestrator
from src.services.models.react_models import ReActResult, ReActConfig
+1 -1
View File
@@ -4,7 +4,7 @@ import asyncio
import json
from typing import Any, Dict, List, Optional, Set
from PySide6.QtCore import QThread, Signal
from ..qt_compat import QThread, Signal
from src.services.analysis_db_service import AnalysisDBService
from src.services.settings_service import SettingsService
+20 -22
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
import asyncio
from PySide6.QtWidgets import QMessageBox, QInputDialog, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QCheckBox, QSpinBox, QComboBox
from PySide6.QtCore import QObject, QThread, Signal
from ..qt_compat import (QMessageBox, QInputDialog, QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QCheckBox, QSpinBox, QComboBox,
QObject, QThread, Signal, exec_dialog)
from src.ida_compat import log
@@ -561,7 +562,7 @@ class ProviderDialog(QDialog):
def on_authenticate_clicked(self):
"""Handle OAuth authentication button click"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
current_data = self.provider_type_combo.currentData()
try:
@@ -586,8 +587,7 @@ class ProviderDialog(QDialog):
def _authenticate_anthropic(self):
"""Handle Anthropic OAuth authentication with automatic callback"""
from PySide6.QtWidgets import QMessageBox, QProgressDialog
from PySide6.QtCore import Qt
from ..qt_compat import QMessageBox, QProgressDialog, Qt
try:
from ..services.llm_providers.oauth_worker import AnthropicOAuthWorker
@@ -636,7 +636,7 @@ class ProviderDialog(QDialog):
def _on_anthropic_auth_complete(self, result: dict):
"""Handle successful Anthropic OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -654,7 +654,7 @@ class ProviderDialog(QDialog):
def _on_anthropic_auth_failed(self, error: str):
"""Handle failed Anthropic OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -681,7 +681,7 @@ class ProviderDialog(QDialog):
def _authenticate_anthropic_manual(self):
"""Manual fallback for Anthropic OAuth authentication"""
from PySide6.QtWidgets import QMessageBox, QInputDialog
from ..qt_compat import QMessageBox, QInputDialog
try:
from ..services.llm_providers.oauth_utils import (
@@ -754,8 +754,7 @@ class ProviderDialog(QDialog):
def _authenticate_codex(self):
"""Handle OpenAI Codex (ChatGPT) OAuth authentication with automatic callback"""
from PySide6.QtWidgets import QMessageBox, QProgressDialog
from PySide6.QtCore import Qt
from ..qt_compat import QMessageBox, QProgressDialog, Qt
try:
from ..services.llm_providers.oauth_worker import OpenAIOAuthWorker
@@ -804,7 +803,7 @@ class ProviderDialog(QDialog):
def _on_codex_auth_complete(self, result: dict):
"""Handle successful OpenAI OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -822,7 +821,7 @@ class ProviderDialog(QDialog):
def _on_codex_auth_failed(self, error: str):
"""Handle failed OpenAI OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -858,7 +857,7 @@ class ProviderDialog(QDialog):
def _authenticate_codex_manual(self):
"""Manual fallback for OpenAI Codex OAuth authentication"""
from PySide6.QtWidgets import QMessageBox, QInputDialog
from ..qt_compat import QMessageBox, QInputDialog
try:
from ..services.llm_providers.oauth_codex_utils import (
@@ -939,8 +938,7 @@ class ProviderDialog(QDialog):
def _authenticate_gemini(self):
"""Handle Google Gemini OAuth authentication with automatic callback"""
from PySide6.QtWidgets import QMessageBox, QProgressDialog
from PySide6.QtCore import Qt
from ..qt_compat import QMessageBox, QProgressDialog, Qt
try:
from ..services.llm_providers.oauth_worker import GeminiOAuthWorker
@@ -990,7 +988,7 @@ class ProviderDialog(QDialog):
def _on_gemini_auth_complete(self, result: dict):
"""Handle successful Gemini OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -1011,7 +1009,7 @@ class ProviderDialog(QDialog):
def _on_gemini_auth_failed(self, error: str):
"""Handle failed Gemini OAuth authentication"""
from PySide6.QtWidgets import QMessageBox
from ..qt_compat import QMessageBox
self._oauth_auth_completed = True
@@ -1051,7 +1049,7 @@ class ProviderDialog(QDialog):
def _authenticate_gemini_manual(self):
"""Manual fallback for Gemini OAuth authentication using headless mode"""
from PySide6.QtWidgets import QMessageBox, QInputDialog
from ..qt_compat import QMessageBox, QInputDialog
import asyncio
try:
@@ -1323,7 +1321,7 @@ class SettingsController(QObject):
def add_llm_provider(self):
dialog = ProviderDialog(self.view)
if dialog.exec() == QDialog.Accepted:
if exec_dialog(dialog) == QDialog.Accepted:
try:
data = dialog.get_provider_data()
is_cli_provider = data.get('provider_type') == 'anthropic_cli'
@@ -1361,7 +1359,7 @@ class SettingsController(QObject):
provider = providers[row]
dialog = ProviderDialog(self.view, provider)
if dialog.exec() == QDialog.Accepted:
if exec_dialog(dialog) == QDialog.Accepted:
data = dialog.get_provider_data()
is_cli_provider = data.get('provider_type') == 'anthropic_cli'
if not data['name'] or not data['model']:
@@ -1498,7 +1496,7 @@ class SettingsController(QObject):
def add_mcp_provider(self):
dialog = MCPProviderDialog(self.view)
if dialog.exec() == QDialog.Accepted:
if exec_dialog(dialog) == QDialog.Accepted:
try:
data = dialog.get_provider_data()
if not all([data['name'], data['url']]):
@@ -1526,7 +1524,7 @@ class SettingsController(QObject):
provider = providers[row]
dialog = MCPProviderDialog(self.view, provider)
if dialog.exec() == QDialog.Accepted:
if exec_dialog(dialog) == QDialog.Accepted:
data = dialog.get_provider_data()
if not all([data['name'], data['url']]):
self.show_error("Validation Error", "Name and URL are required fields.")
+1 -2
View File
@@ -9,8 +9,7 @@ querying, pushing, and pulling symbols and graph data.
import asyncio
import json
from typing import Dict, List, Optional, Any, Callable
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QThread, Signal, QObject
from ..qt_compat import QMessageBox, QThread, Signal, QObject
from src.services.analysis_db_service import analysis_db_service
from src.services.graphrag.graph_store import GraphStore
+38
View File
@@ -0,0 +1,38 @@
"""
Qt5/Qt6 compatibility shim.
PySide6 (IDA >= 9.2) is tried first; falls back to PyQt5 (IDA <= 9.1).
Consumer files should use explicit imports:
from ..qt_compat import QWidget, Signal, exec_dialog, ...
"""
try:
from PySide6.QtCore import * # noqa: F401,F403
from PySide6.QtGui import * # noqa: F401,F403
from PySide6.QtWidgets import * # noqa: F401,F403
from PySide6.QtCore import Signal
from PySide6.QtGui import QAction
QT_BINDING = "PySide6"
except ImportError:
from PyQt5.QtCore import * # noqa: F401,F403
from PyQt5.QtGui import * # noqa: F401,F403
from PyQt5.QtWidgets import * # noqa: F401,F403
from PyQt5.QtCore import pyqtSignal as Signal # noqa: F401
from PyQt5.QtWidgets import QAction # noqa: F401
QT_BINDING = "PyQt5"
QT_AVAILABLE = QT_BINDING is not None
def exec_dialog(obj, *args):
"""Cross-binding .exec() wrapper (PyQt5 uses .exec_())."""
if hasattr(obj, "exec"):
return obj.exec(*args)
return obj.exec_(*args)
def utc_timezone():
"""Cross-binding UTC QTimeZone."""
if hasattr(QTimeZone, "utc"):
return QTimeZone.utc()
return QTimeZone(0)
+6 -6
View File
@@ -17,11 +17,11 @@ import asyncio
from typing import Dict, Any, Optional
try:
from PySide6.QtCore import QThread, Signal
PYSIDE_AVAILABLE = True
from ...qt_compat import QThread, Signal
QT_AVAILABLE = True
except ImportError:
PYSIDE_AVAILABLE = False
# Stub for type hints when PySide6 not available
QT_AVAILABLE = False
# Stub for type hints when Qt not available
class QThread:
pass
class Signal:
@@ -52,8 +52,8 @@ class OAuthWorkerBase(QThread):
timeout: Seconds to wait for authentication (default 5 minutes)
parent: Optional parent QObject
"""
if not PYSIDE_AVAILABLE:
raise ImportError("PySide6 required for OAuth worker")
if not QT_AVAILABLE:
raise ImportError("Qt bindings required for OAuth worker")
super().__init__(parent)
self.timeout = timeout
+1 -1
View File
@@ -212,7 +212,7 @@ class MCPClientService:
deadline = time.time() + timeout
while not future.done() and time.time() < deadline:
try:
from PySide6.QtWidgets import QApplication
from ..qt_compat import QApplication
app = QApplication.instance()
if app:
app.processEvents()
+4 -6
View File
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QLineEdit, QTableWidget, QTableWidgetItem, QListWidget,
QListWidgetItem, QCheckBox, QHeaderView, QAbstractItemView,
QSplitter, QSizePolicy)
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QColor
from ..qt_compat import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QLineEdit, QTableWidget, QTableWidgetItem, QListWidget,
QListWidgetItem, QCheckBox, QHeaderView, QAbstractItemView,
QSplitter, QSizePolicy, Signal, Qt, QColor)
class ActionsTabView(QWidget):
+11 -13
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTextBrowser, QTextEdit, QLineEdit, QSizePolicy, QCheckBox,
QApplication, QGroupBox, QGridLayout, QSplitter, QFrame)
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QKeySequence, QFontDatabase
from ..qt_compat import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTextBrowser, QTextEdit, QLineEdit, QSizePolicy, QCheckBox,
QApplication, QGroupBox, QGridLayout, QSplitter, QFrame,
Signal, Qt, QKeySequence, QFontDatabase, exec_dialog)
import markdown
import re
@@ -42,8 +41,7 @@ class MarkdownCopyBrowser(QTextBrowser):
def contextMenuEvent(self, event):
"""Override context menu to replace Copy action with markdown copy"""
from PySide6.QtWidgets import QMenu
from PySide6.QtGui import QAction
from ..qt_compat import QMenu, QAction
menu = self.createStandardContextMenu()
@@ -55,7 +53,7 @@ class MarkdownCopyBrowser(QTextBrowser):
action.triggered.connect(self._copy_markdown)
break
menu.exec(event.globalPos())
exec_dialog(menu, event.globalPos())
menu.deleteLater()
@@ -176,9 +174,9 @@ class ExplainTabView(QWidget):
self.explain_editor.hide() # Hidden by default
# ESC key discards edits and returns to view mode
from PySide6.QtGui import QShortcut
from ..qt_compat import QShortcut
esc_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self.explain_editor)
esc_shortcut.setContext(Qt.ShortcutContext.WidgetShortcut)
esc_shortcut.setContext(Qt.WidgetShortcut)
esc_shortcut.activated.connect(self.cancel_edit_mode)
def create_line_explanation_panel(self):
@@ -297,7 +295,7 @@ class ExplainTabView(QWidget):
self.explain_browser.set_markdown_source(self.markdown_content)
# Restore panel sizes — give all remaining space to the active text widget
from PySide6.QtCore import QTimer
from ..qt_compat import QTimer
is_edit = self.is_edit_mode
def _restore_sizes():
total = sum(self.main_splitter.sizes())
@@ -321,7 +319,7 @@ class ExplainTabView(QWidget):
self.explain_editor.hide()
self.explain_browser.show()
self.edit_save_button.setText("Edit")
from PySide6.QtCore import QTimer
from ..qt_compat import QTimer
def _restore_sizes():
total = sum(self.main_splitter.sizes())
active_size = total - line_size - security_size
@@ -347,7 +345,7 @@ class ExplainTabView(QWidget):
self.explain_browser.setHtml(self.markdown_to_html(markdown_text))
# Defer scroll restoration until after Qt processes the layout change
from PySide6.QtCore import QTimer
from ..qt_compat import QTimer
if should_auto_scroll:
# Auto-scroll to bottom if user was following the explanation
QTimer.singleShot(0, self._scroll_to_bottom)
+1 -1
View File
@@ -88,7 +88,7 @@ class IDAssistForm(idaapi.PluginForm):
def OnCreate(self, form):
"""Called by IDA when the form is first created."""
try:
from PySide6.QtWidgets import QTabWidget, QVBoxLayout, QWidget
from src.qt_compat import QTabWidget, QVBoxLayout, QWidget
# Convert IDA form handle to a PySide6 widget
parent = self.FormToPyQtWidget(form)
+9 -12
View File
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTextBrowser, QTextEdit, QLineEdit, QTableWidget, QTableWidgetItem,
QSplitter, QPlainTextEdit, QHeaderView, QAbstractItemView, QSizePolicy, QCheckBox,
QApplication)
from PySide6.QtCore import Signal, Qt, QDateTime
from PySide6.QtGui import QKeySequence, QFontDatabase
from ..qt_compat import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTextBrowser, QTextEdit, QLineEdit, QTableWidget, QTableWidgetItem,
QSplitter, QPlainTextEdit, QHeaderView, QAbstractItemView, QSizePolicy, QCheckBox,
QApplication, Signal, Qt, QDateTime, QKeySequence, QFontDatabase, exec_dialog)
import markdown
import re
@@ -44,8 +42,7 @@ class MarkdownCopyBrowser(QTextBrowser):
def contextMenuEvent(self, event):
"""Override context menu to replace Copy action with markdown copy"""
from PySide6.QtWidgets import QMenu
from PySide6.QtGui import QAction
from ..qt_compat import QMenu, QAction
menu = self.createStandardContextMenu()
@@ -57,7 +54,7 @@ class MarkdownCopyBrowser(QTextBrowser):
action.triggered.connect(self._copy_markdown)
break
menu.exec(event.globalPos())
exec_dialog(menu, event.globalPos())
menu.deleteLater()
@@ -187,9 +184,9 @@ class QueryTabView(QWidget):
self.query_editor.hide() # Hidden by default
# ESC key discards edits and returns to view mode
from PySide6.QtGui import QShortcut
from ..qt_compat import QShortcut
esc_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self.query_editor)
esc_shortcut.setContext(Qt.ShortcutContext.WidgetShortcut)
esc_shortcut.setContext(Qt.WidgetShortcut)
esc_shortcut.activated.connect(self.cancel_edit_mode)
def create_history_table(self):
@@ -487,7 +484,7 @@ class QueryTabView(QWidget):
# Defer scroll restoration until after Qt processes the layout change
# This ensures scrollbar.maximum() reflects the new content size
from PySide6.QtCore import QTimer
from ..qt_compat import QTimer
if should_auto_scroll:
# User wants to follow new content - scroll to bottom
def scroll_to_bottom():
+4 -5
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView,
QAbstractItemView, QLabel, QLineEdit, QComboBox,
QTextEdit, QSplitter, QGroupBox)
from PySide6.QtCore import Signal, Qt
from ..qt_compat import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView,
QAbstractItemView, QLabel, QLineEdit, QComboBox,
QTextEdit, QSplitter, QGroupBox, Signal, Qt)
class RagTabView(QWidget):
@@ -8,9 +8,8 @@ Provides organized UI for triggering various analysis operations:
- Individual Operations: Security, Network Flow, Community Detection, Refresh Names
"""
from PySide6.QtCore import Signal
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QPushButton, QLabel,
from ...qt_compat import (
Signal, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QPushButton, QLabel,
QProgressBar, QFrame, QCheckBox
)
+3 -5
View File
@@ -5,12 +5,10 @@ import math
from datetime import datetime
from typing import Any, Dict, List, Optional
from PySide6.QtCore import Qt, Signal, QPointF
from PySide6.QtGui import (
from ..qt_compat import (
Qt, Signal, QPointF,
QFont, QFontDatabase, QTextCursor, QBrush, QColor, QPen,
QPainterPath, QPainter, QPolygonF, QMouseEvent
)
from PySide6.QtWidgets import (
QPainterPath, QPainter, QPolygonF, QMouseEvent,
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTabWidget,
QGroupBox, QGridLayout, QSplitter, QListWidget, QListWidgetItem, QTableWidget,
QTableWidgetItem, QComboBox, QTextEdit, QCheckBox, QSpinBox, QGraphicsView,
+4 -5
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTableWidget, QTableWidgetItem, QComboBox, QTextEdit,
QLineEdit, QCheckBox, QHeaderView, QAbstractItemView,
QGroupBox, QScrollArea, QFileDialog)
from PySide6.QtCore import Signal, Qt
from ..qt_compat import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTableWidget, QTableWidgetItem, QComboBox, QTextEdit,
QLineEdit, QCheckBox, QHeaderView, QAbstractItemView,
QGroupBox, QScrollArea, QFileDialog, Signal, Qt)
from ..services.service_registry import get_service_registry
+8 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
from PySide6.QtWidgets import QTextBrowser, QAbstractScrollArea, QApplication
from PySide6.QtGui import QTextCursor, QKeySequence
from ..qt_compat import (QTextBrowser, QAbstractScrollArea, QApplication,
QTextCursor, QKeySequence, exec_dialog)
from ..services.streaming.render_update import RenderUpdate, UpdateType
from ..services.streaming.streaming_renderer import MARKDOWN_CSS
@@ -91,7 +91,7 @@ class StreamingMarkdownBrowser(QTextBrowser):
action.triggered.disconnect()
action.triggered.connect(self._copy_markdown)
break
menu.exec(event.globalPos())
exec_dialog(menu, event.globalPos())
menu.deleteLater()
# --- Streaming Functionality ---
@@ -116,7 +116,7 @@ class StreamingMarkdownBrowser(QTextBrowser):
delta_html = html_content[len(self._last_rendered_html):]
if delta_html:
cursor = self.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.movePosition(QTextCursor.End)
cursor.insertHtml(delta_html)
self._force_layout()
else:
@@ -153,7 +153,7 @@ class StreamingMarkdownBrowser(QTextBrowser):
# Append pending via cursor (avoids second setHtml)
if update.pending_html:
cursor = self.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.movePosition(QTextCursor.End)
cursor.insertHtml(update.pending_html)
self._force_layout()
else:
@@ -163,13 +163,13 @@ class StreamingMarkdownBrowser(QTextBrowser):
# Delete existing pending content
if self._pending_start < self.document().characterCount():
cursor.setPosition(self._pending_start)
cursor.movePosition(QTextCursor.MoveOperation.End,
QTextCursor.MoveMode.KeepAnchor)
cursor.movePosition(QTextCursor.End,
QTextCursor.KeepAnchor)
cursor.removeSelectedText()
# Insert new pending content
if update.pending_html:
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.movePosition(QTextCursor.End)
cursor.insertHtml(update.pending_html)
self._force_layout()
+3 -4
View File
@@ -6,15 +6,14 @@ This view provides the UI for querying, pushing, and pulling
symbols and graph data from SymGraph.
"""
from PySide6.QtWidgets import (
from ..qt_compat import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView,
QAbstractItemView, QLabel, QGroupBox, QRadioButton,
QCheckBox, QButtonGroup, QSplitter, QFrame, QSlider,
QStackedWidget, QProgressBar, QTabWidget
QStackedWidget, QProgressBar, QTabWidget,
Signal, Qt, QFont
)
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QFont
from ..services.models.symgraph_models import ConflictAction, ConflictEntry, PushScope