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
26 changes: 23 additions & 3 deletions graphlink_app/graphlink_canvas/graphlink_canvas_dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ class ColorPickerDialog(QDialog):
"""
A small, frameless pop-up dialog for selecting a color from a predefined palette.
It automatically closes when the user clicks outside of it.

Bug-scan finding: the reset/swatch buttons used to connect QPushButton.clicked
to a lambda closing over self (e.g. `lambda: self.color_selected(None, "default")`).
PySide6's GC does not reclaim a self-capturing lambda connected to a
widget-owned signal - confirmed empirically, and notably NOT limited to
custom Signals or worker threads: a stock QPushButton.clicked calling a
plain method (not even a Signal.emit) forms the identical GC-invisible
cycle - so this dialog leaked forever every time it was opened. Fixed by
connecting the default button to a small dedicated bound method, and
storing each swatch's color data via QWidget.setProperty() so every swatch
button can share one bound-method dispatcher that reads
self.sender().property(...).
"""
def __init__(self, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -57,7 +69,7 @@ def __init__(self, parent=None):

default_btn = QPushButton("Reset to Default")
default_btn.setIcon(qta.icon('fa5s.undo', color='white'))
default_btn.clicked.connect(lambda: self.color_selected(None, "default"))
default_btn.clicked.connect(self._on_default_clicked)
main_layout.addWidget(default_btn)

def create_section(title, color_type, names_list):
Expand Down Expand Up @@ -93,8 +105,9 @@ def create_section(title, color_type, names_list):
QPushButton:hover {{ border: 2px solid #FFFFFF; }}
"""
btn.setStyleSheet(style)
btn.clicked.connect(lambda checked, c=color_data: self.color_selected(c["color"], c["type"]))

btn.setProperty("frame_color_data", color_data)
btn.clicked.connect(self._on_swatch_clicked)

grid_layout.addWidget(btn, row, col)
col = (col + 1) % 5
if col == 0: row += 1
Expand Down Expand Up @@ -150,6 +163,13 @@ def changeEvent(self, event):
self.close()
super().changeEvent(event)

def _on_default_clicked(self):
self.color_selected(None, "default")

def _on_swatch_clicked(self):
color_data = self.sender().property("frame_color_data")
self.color_selected(color_data["color"], color_data["type"])

def color_selected(self, color, color_type):
self.selected_color = color
self.selected_type = color_type
Expand Down
40 changes: 33 additions & 7 deletions graphlink_app/graphlink_nodes/graphlink_node_chat_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ class ChatNodeContextMenu(QMenu):
"""
A comprehensive context menu for ChatNode, providing access to text manipulation,
AI actions (summaries, explainers, charts), and organizational tools.

Bug-scan finding: the export/reveal/chart-type actions used to connect
QAction.triggered to a lambda closing over self (e.g.
`lambda: self._handle_export('txt')`). PySide6's GC does not reclaim a
self-capturing lambda connected to a widget-owned signal (confirmed
empirically - a bound-method connection is reclaimed fine), so this menu
- and the ChatNode it stores via self.node - leaked forever on EVERY
right-click. Fixed by storing the per-action variant via
QAction.setData() and connecting every action to one shared bound-method
dispatcher that reads self.sender().data().
"""

def __init__(self, node, parent=None):
Expand Down Expand Up @@ -54,7 +64,8 @@ def __init__(self, node, parent=None):
node_label = docked_node.docked_label() if hasattr(docked_node, "docked_label") else docked_node.__class__.__name__
reveal_action = QAction(node_label, undock_menu)
reveal_action.setIcon(qta.icon('fa5s.expand-arrows-alt', color='white'))
reveal_action.triggered.connect(lambda checked=False, target=docked_node: self.undock_node(target))
reveal_action.setData(docked_node)
reveal_action.triggered.connect(self._on_reveal_action_triggered)
undock_menu.addAction(reveal_action)
self.addMenu(undock_menu)

Expand Down Expand Up @@ -98,7 +109,8 @@ def __init__(self, node, parent=None):
for title, chart_type, icon in chart_types:
action = QAction(title, chart_menu)
action.setIcon(qta.icon(icon, color='white'))
action.triggered.connect(lambda checked, t=chart_type: self.generate_chart(t))
action.setData(chart_type)
action.triggered.connect(self._on_chart_action_triggered)
chart_menu.addAction(action)

self.addMenu(chart_menu)
Expand Down Expand Up @@ -133,27 +145,41 @@ def create_export_menu(self):
export_menu.setIcon(qta.icon('fa5s.file-export', color='white'))

txt_action = QAction("Text File (.txt)", self)
txt_action.triggered.connect(lambda: self._handle_export('txt'))
txt_action.setData('txt')
txt_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(txt_action)

md_action = QAction("Markdown File (.md)", self)
md_action.triggered.connect(lambda: self._handle_export('md'))
md_action.setData('md')
md_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(md_action)

html_action = QAction("HTML Document (.html)", self)
html_action.triggered.connect(lambda: self._handle_export('html'))
html_action.setData('html')
html_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(html_action)

docx_action = QAction("Word Document (.docx)", self)
docx_action.triggered.connect(lambda: self._handle_export('docx'))
docx_action.setData('docx')
docx_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(docx_action)

pdf_action = QAction("PDF Document (.pdf)", self)
pdf_action.triggered.connect(lambda: self._handle_export('pdf'))
pdf_action.setData('pdf')
pdf_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(pdf_action)

return export_menu

def _on_reveal_action_triggered(self):
self.undock_node(self.sender().data())

def _on_chart_action_triggered(self):
self.generate_chart(self.sender().data())

def _on_export_action_triggered(self):
self._handle_export(self.sender().data())

def _handle_export(self, file_format):
exporter = Exporter()
content = self.node.text
Expand Down
30 changes: 24 additions & 6 deletions graphlink_app/graphlink_nodes/graphlink_node_code_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@


class CodeNodeContextMenu(QMenu):
"""Context menu for CodeNode, providing copy, export, and regenerate actions."""
"""Context menu for CodeNode, providing copy, export, and regenerate actions.

Bug-scan finding: the export actions used to connect QAction.triggered to
a lambda closing over self (e.g. `lambda: self._handle_export('txt')`).
PySide6's GC does not reclaim a self-capturing lambda connected to a
widget-owned signal (confirmed empirically - a bound-method connection is
reclaimed fine), so this menu - and the CodeNode it stores via self.node -
leaked forever on EVERY right-click. Fixed by storing the format via
QAction.setData() and connecting every export action to one shared
bound-method dispatcher that reads self.sender().data().
"""

def __init__(self, node, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -49,27 +59,35 @@ def create_export_menu(self):
export_menu.setIcon(qta.icon('fa5s.file-export', color='white'))

py_action = QAction("Python Script (.py)", self)
py_action.triggered.connect(lambda: self._handle_export('py'))
py_action.setData('py')
py_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(py_action)

txt_action = QAction("Text File (.txt)", self)
txt_action.triggered.connect(lambda: self._handle_export('txt'))
txt_action.setData('txt')
txt_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(txt_action)

md_action = QAction("Markdown File (.md)", self)
md_action.triggered.connect(lambda: self._handle_export('md'))
md_action.setData('md')
md_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(md_action)

html_action = QAction("HTML Document (.html)", self)
html_action.triggered.connect(lambda: self._handle_export('html'))
html_action.setData('html')
html_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(html_action)

pdf_action = QAction("PDF Document (.pdf)", self)
pdf_action.triggered.connect(lambda: self._handle_export('pdf'))
pdf_action.setData('pdf')
pdf_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(pdf_action)

return export_menu

def _on_export_action_triggered(self):
self._handle_export(self.sender().data())

def _handle_export(self, file_format):
exporter = Exporter()
content = self.node.code
Expand Down
30 changes: 24 additions & 6 deletions graphlink_app/graphlink_nodes/graphlink_node_document_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@


class DocumentNodeContextMenu(QMenu):
"""Context menu for uploaded file attachments."""
"""Context menu for uploaded file attachments.

Bug-scan finding: the export actions used to connect QAction.triggered to
a lambda closing over self (e.g. `lambda: self._handle_export('txt')`).
PySide6's GC does not reclaim a self-capturing lambda connected to a
widget-owned signal (confirmed empirically - a bound-method connection is
reclaimed fine), so this menu - and the node it stores via self.node -
leaked forever on EVERY right-click. Fixed by storing the format via
QAction.setData() and connecting every export action to one shared
bound-method dispatcher that reads self.sender().data().
"""

def __init__(self, node, parent=None):
super().__init__(parent)
Expand Down Expand Up @@ -66,27 +76,35 @@ def create_export_menu(self):
export_menu.setIcon(qta.icon('fa5s.file-export', color='white'))

txt_action = QAction("Text File (.txt)", self)
txt_action.triggered.connect(lambda: self._handle_export('txt'))
txt_action.setData('txt')
txt_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(txt_action)

md_action = QAction("Markdown File (.md)", self)
md_action.triggered.connect(lambda: self._handle_export('md'))
md_action.setData('md')
md_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(md_action)

html_action = QAction("HTML Document (.html)", self)
html_action.triggered.connect(lambda: self._handle_export('html'))
html_action.setData('html')
html_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(html_action)

docx_action = QAction("Word Document (.docx)", self)
docx_action.triggered.connect(lambda: self._handle_export('docx'))
docx_action.setData('docx')
docx_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(docx_action)

pdf_action = QAction("PDF Document (.pdf)", self)
pdf_action.triggered.connect(lambda: self._handle_export('pdf'))
pdf_action.setData('pdf')
pdf_action.triggered.connect(self._on_export_action_triggered)
export_menu.addAction(pdf_action)

return export_menu

def _on_export_action_triggered(self):
self._handle_export(self.sender().data())

def _handle_export(self, file_format):
exporter = Exporter()
content = self.node.content
Expand Down
Loading
Loading