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
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user