From d5c89e9602530103b8edf4505d253e002faa5713 Mon Sep 17 00:00:00 2001 From: AJ Date: Wed, 1 Jul 2026 23:51:40 -0400 Subject: [PATCH] feat: user-resizable panes with persisted layout - Horizontal splitter between the server list and the editor panel - Vertical splitter between the Active and Disabled tables (drops the fixed 170px cap on the disabled list) - Editor fields (args, env/header tables) now grow with the window instead of being pinned to fixed max heights - Splitter positions and window geometry persist across launches via QSettings; handles highlight on hover/drag --- bcc.py | 90 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/bcc.py b/bcc.py index 5416827..1f9218a 100644 --- a/bcc.py +++ b/bcc.py @@ -13,7 +13,7 @@ from __future__ import annotations import sys from pathlib import Path -from PySide6.QtCore import Qt, QThread, QTimer, Signal +from PySide6.QtCore import QSettings, Qt, QThread, QTimer, Signal from PySide6.QtGui import QAction, QColor, QGuiApplication from PySide6.QtWidgets import ( QAbstractItemView, @@ -33,6 +33,7 @@ from PySide6.QtWidgets import ( QMessageBox, QPlainTextEdit, QPushButton, + QSplitter, QStackedWidget, QTableWidget, QTableWidgetItem, @@ -98,6 +99,9 @@ QTableWidget#disabledTable::item:selected {{ background: {ACCENT}; color: #1a120 QPlainTextEdit#diag {{ font-family: "SF Mono", "Cascadia Code", "Consolas", "DejaVu Sans Mono", monospace; font-size: 12px; background: #16181d; border: 1px solid {BORDER}; border-radius: 8px; }} QFrame#diagCard {{ background: transparent; border: none; }} +QSplitter::handle {{ background: transparent; }} +QSplitter::handle:hover {{ background: {BORDER}; border-radius: 4px; }} +QSplitter::handle:pressed {{ background: {ACCENT}; border-radius: 4px; }} """ @@ -136,9 +140,9 @@ class KeyValueTable(QWidget): self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.table.verticalHeader().setVisible(False) self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - self.table.setMaximumHeight(150) + self.table.setMinimumHeight(90) self.table.itemChanged.connect(self._changed) - lay.addWidget(self.table) + lay.addWidget(self.table, 1) row = QHBoxLayout() add = QPushButton("+ Add") rem = QPushButton("− Remove") @@ -281,7 +285,7 @@ class ServerEditor(QFrame): self.stack = QStackedWidget() self.stack.addWidget(self._build_stdio_page()) self.stack.addWidget(self._build_remote_page()) - outer.addWidget(self.stack) + outer.addWidget(self.stack, 1) # Dependency status row dep = QHBoxLayout() @@ -329,7 +333,6 @@ class ServerEditor(QFrame): dlay.addLayout(crow) self.diag_card.setVisible(False) outer.addWidget(self.diag_card) - outer.addStretch() self.setEnabled(False) @@ -353,14 +356,14 @@ class ServerEditor(QFrame): self.args.setPlaceholderText( "-y\n@modelcontextprotocol/server-filesystem\n/Users/you/Documents" ) - self.args.setMaximumHeight(120) + self.args.setMinimumHeight(70) self.args.textChanged.connect(self._emit) - v.addWidget(self.args) + v.addWidget(self.args, 1) v.addWidget(self._lbl("Environment variables")) self.env = KeyValueTable( "Variable", "Value", on_change=self._emit, before_change=self._before_change ) - v.addWidget(self.env) + v.addWidget(self.env, 1) return w def _build_remote_page(self): @@ -382,7 +385,7 @@ class ServerEditor(QFrame): self.headers = KeyValueTable( "Header", "Value", on_change=self._emit, before_change=self._before_change ) - v.addWidget(self.headers) + v.addWidget(self.headers, 1) return w # --- model <-> form -------------------------------------------------- # @@ -655,12 +658,18 @@ class MainWindow(QMainWindow): root.addLayout(self._build_topbar()) - split = QHBoxLayout() - split.setSpacing(12) - split.addWidget(self._build_left(), 3) + # User-draggable divider between the server list and the editor. + split = QSplitter(Qt.Orientation.Horizontal) + split.setChildrenCollapsible(False) + split.setHandleWidth(10) + split.addWidget(self._build_left()) self.editor = ServerEditor(on_change=self._editor_changed, before_change=self._push_undo) - split.addWidget(self.editor, 4) - root.addLayout(split, 1) + split.addWidget(self.editor) + split.setStretchFactor(0, 3) + split.setStretchFactor(1, 4) + split.setSizes([390, 520]) + self.hsplit = split + root.addWidget(split, 1) root.addLayout(self._build_actionbar()) @@ -668,8 +677,25 @@ class MainWindow(QMainWindow): self.status.setObjectName("statusbar") root.addWidget(self.status) + self._restore_layout() self.reload_profiles() + # --- layout persistence ---------------------------------------------- # + def _restore_layout(self): + st = QSettings("BCC", "BetterClaudeConfig") + if geo := st.value("window/geometry"): + self.restoreGeometry(geo) + if hs := st.value("window/hsplit"): + self.hsplit.restoreState(hs) + if vs := st.value("window/vsplit"): + self.vsplit.restoreState(vs) + + def _save_layout(self): + st = QSettings("BCC", "BetterClaudeConfig") + st.setValue("window/geometry", self.saveGeometry()) + st.setValue("window/hsplit", self.hsplit.saveState()) + st.setValue("window/vsplit", self.vsplit.saveState()) + # --- top bar --------------------------------------------------------- # def _build_topbar(self): bar = QHBoxLayout() @@ -722,27 +748,48 @@ class MainWindow(QMainWindow): head.setObjectName("h1") v.addWidget(head) + # Active and Disabled sections live in a vertical splitter so the user + # can drag the divider instead of being stuck with a fixed-height + # disabled list. + active_box = QWidget() + av = QVBoxLayout(active_box) + av.setContentsMargins(0, 0, 0, 0) + av.setSpacing(8) self.active_label = QLabel("Active") self.active_label.setObjectName("section") - v.addWidget(self.active_label) + av.addWidget(self.active_label) self.active_table = self._make_server_table() - v.addWidget(self.active_table, 3) + av.addWidget(self.active_table, 1) self.active_empty = QLabel("No active servers. Add one, or Paste JSON.") self.active_empty.setObjectName("placeholder") self.active_empty.setAlignment(Qt.AlignmentFlag.AlignCenter) self.active_empty.hide() - v.addWidget(self.active_empty) + av.addWidget(self.active_empty) + disabled_box = QWidget() + dv = QVBoxLayout(disabled_box) + dv.setContentsMargins(0, 0, 0, 0) + dv.setSpacing(8) self.disabled_label = QLabel("⊘ Disabled") self.disabled_label.setObjectName("sectionDisabled") - v.addWidget(self.disabled_label) + dv.addWidget(self.disabled_label) self.disabled_table = self._make_server_table(object_name="disabledTable") - self.disabled_table.setMaximumHeight(170) - v.addWidget(self.disabled_table, 1) + dv.addWidget(self.disabled_table, 1) self.disabled_empty = QLabel("Nothing disabled.") self.disabled_empty.setObjectName("placeholder") self.disabled_empty.setAlignment(Qt.AlignmentFlag.AlignCenter) - v.addWidget(self.disabled_empty) + dv.addWidget(self.disabled_empty) + + vsplit = QSplitter(Qt.Orientation.Vertical) + vsplit.setChildrenCollapsible(False) + vsplit.setHandleWidth(10) + vsplit.addWidget(active_box) + vsplit.addWidget(disabled_box) + vsplit.setStretchFactor(0, 3) + vsplit.setStretchFactor(1, 1) + vsplit.setSizes([360, 150]) + self.vsplit = vsplit + v.addWidget(vsplit, 1) return card # --- bottom action bar ---------------------------------------------- # @@ -1255,6 +1302,7 @@ class MainWindow(QMainWindow): if self.dirty and not self._confirm_discard(): e.ignore() return + self._save_layout() e.accept()