diff --git a/bcc.py b/bcc.py index f5096b5..9433c61 100644 --- a/bcc.py +++ b/bcc.py @@ -13,8 +13,8 @@ from __future__ import annotations import sys from pathlib import Path -from PySide6.QtCore import QSettings, Qt, QThread, QTimer, Signal -from PySide6.QtGui import QAction, QColor, QGuiApplication +from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, Signal +from PySide6.QtGui import QAction, QColor, QGuiApplication, QPainter from PySide6.QtWidgets import ( QAbstractItemView, QApplication, @@ -361,14 +361,11 @@ class ServerEditor(QFrame): self.command.textChanged.connect(self._emit) v.addWidget(self.command) v.addWidget(self._lbl("Arguments (one per line)")) - self.args = QPlainTextEdit() + self.args = ArgsEdit() self.args.setPlaceholderText( "-y\n@modelcontextprotocol/server-filesystem\n/Users/you/Documents" ) self.args.setMinimumHeight(70) - # One argument per line means wrapping is actively misleading — a long - # path that soft-wraps looks like two separate args. Scroll instead. - self.args.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) self.args.textChanged.connect(self._emit) v.addWidget(self.args, 1) v.addWidget(self._lbl("Environment variables")) @@ -565,6 +562,76 @@ class ServerEditor(QFrame): self.dep_label.setStyleSheet(f"color: {color};") +# --------------------------------------------------------------------------- # +# Arguments editor: one line = one argument, with a numbered gutter so that +# model is visible (a long path on its own line reads as "arg 2", not as a +# wrapped continuation of arg 1). +# --------------------------------------------------------------------------- # +class _ArgsGutter(QWidget): + def __init__(self, editor: ArgsEdit): + super().__init__(editor) + self._editor = editor + + def sizeHint(self): + return QSize(self._editor.gutter_width(), 0) + + def paintEvent(self, event): + self._editor.gutter_paint(event) + + +class ArgsEdit(QPlainTextEdit): + def __init__(self): + super().__init__() + self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + self._gutter = _ArgsGutter(self) + self.blockCountChanged.connect(self._update_gutter_width) + self.updateRequest.connect(self._on_update_request) + self._update_gutter_width() + + def gutter_width(self) -> int: + digits = max(1, len(str(self.blockCount()))) + return 14 + self.fontMetrics().horizontalAdvance("9") * digits + + def _update_gutter_width(self, *_): + self.setViewportMargins(self.gutter_width(), 0, 0, 0) + + def _on_update_request(self, rect, dy): + if dy: + self._gutter.scroll(0, dy) + else: + self._gutter.update(0, rect.y(), self._gutter.width(), rect.height()) + if rect.contains(self.viewport().rect()): + self._update_gutter_width() + + def resizeEvent(self, e): + super().resizeEvent(e) + cr = self.contentsRect() + self._gutter.setGeometry(QRect(cr.left(), cr.top(), self.gutter_width(), cr.height())) + + def gutter_paint(self, event): + painter = QPainter(self._gutter) + painter.setPen(QColor(MUTED)) + fm = self.fontMetrics() + block = self.firstVisibleBlock() + n = block.blockNumber() + top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) + bottom = top + round(self.blockBoundingRect(block).height()) + while block.isValid() and top <= event.rect().bottom(): + if block.isVisible() and bottom >= event.rect().top(): + painter.drawText( + 0, + top, + self._gutter.width() - 8, + fm.height(), + Qt.AlignmentFlag.AlignRight, + str(n + 1), + ) + block = block.next() + top = bottom + bottom = top + round(self.blockBoundingRect(block).height()) + n += 1 + + # --------------------------------------------------------------------------- # # Paste-JSON dialog # --------------------------------------------------------------------------- #