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
|
import sys
|
||||||
from pathlib import Path
|
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.QtGui import QAction, QColor, QGuiApplication
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
@@ -33,6 +33,7 @@ from PySide6.QtWidgets import (
|
|||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QSplitter,
|
||||||
QStackedWidget,
|
QStackedWidget,
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
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;
|
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; }}
|
font-size: 12px; background: #16181d; border: 1px solid {BORDER}; border-radius: 8px; }}
|
||||||
QFrame#diagCard {{ background: transparent; border: none; }}
|
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.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
self.table.verticalHeader().setVisible(False)
|
self.table.verticalHeader().setVisible(False)
|
||||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
self.table.setMaximumHeight(150)
|
self.table.setMinimumHeight(90)
|
||||||
self.table.itemChanged.connect(self._changed)
|
self.table.itemChanged.connect(self._changed)
|
||||||
lay.addWidget(self.table)
|
lay.addWidget(self.table, 1)
|
||||||
row = QHBoxLayout()
|
row = QHBoxLayout()
|
||||||
add = QPushButton("+ Add")
|
add = QPushButton("+ Add")
|
||||||
rem = QPushButton("− Remove")
|
rem = QPushButton("− Remove")
|
||||||
@@ -281,7 +285,7 @@ class ServerEditor(QFrame):
|
|||||||
self.stack = QStackedWidget()
|
self.stack = QStackedWidget()
|
||||||
self.stack.addWidget(self._build_stdio_page())
|
self.stack.addWidget(self._build_stdio_page())
|
||||||
self.stack.addWidget(self._build_remote_page())
|
self.stack.addWidget(self._build_remote_page())
|
||||||
outer.addWidget(self.stack)
|
outer.addWidget(self.stack, 1)
|
||||||
|
|
||||||
# Dependency status row
|
# Dependency status row
|
||||||
dep = QHBoxLayout()
|
dep = QHBoxLayout()
|
||||||
@@ -329,7 +333,6 @@ class ServerEditor(QFrame):
|
|||||||
dlay.addLayout(crow)
|
dlay.addLayout(crow)
|
||||||
self.diag_card.setVisible(False)
|
self.diag_card.setVisible(False)
|
||||||
outer.addWidget(self.diag_card)
|
outer.addWidget(self.diag_card)
|
||||||
outer.addStretch()
|
|
||||||
|
|
||||||
self.setEnabled(False)
|
self.setEnabled(False)
|
||||||
|
|
||||||
@@ -353,14 +356,14 @@ class ServerEditor(QFrame):
|
|||||||
self.args.setPlaceholderText(
|
self.args.setPlaceholderText(
|
||||||
"-y\n@modelcontextprotocol/server-filesystem\n/Users/you/Documents"
|
"-y\n@modelcontextprotocol/server-filesystem\n/Users/you/Documents"
|
||||||
)
|
)
|
||||||
self.args.setMaximumHeight(120)
|
self.args.setMinimumHeight(70)
|
||||||
self.args.textChanged.connect(self._emit)
|
self.args.textChanged.connect(self._emit)
|
||||||
v.addWidget(self.args)
|
v.addWidget(self.args, 1)
|
||||||
v.addWidget(self._lbl("Environment variables"))
|
v.addWidget(self._lbl("Environment variables"))
|
||||||
self.env = KeyValueTable(
|
self.env = KeyValueTable(
|
||||||
"Variable", "Value", on_change=self._emit, before_change=self._before_change
|
"Variable", "Value", on_change=self._emit, before_change=self._before_change
|
||||||
)
|
)
|
||||||
v.addWidget(self.env)
|
v.addWidget(self.env, 1)
|
||||||
return w
|
return w
|
||||||
|
|
||||||
def _build_remote_page(self):
|
def _build_remote_page(self):
|
||||||
@@ -382,7 +385,7 @@ class ServerEditor(QFrame):
|
|||||||
self.headers = KeyValueTable(
|
self.headers = KeyValueTable(
|
||||||
"Header", "Value", on_change=self._emit, before_change=self._before_change
|
"Header", "Value", on_change=self._emit, before_change=self._before_change
|
||||||
)
|
)
|
||||||
v.addWidget(self.headers)
|
v.addWidget(self.headers, 1)
|
||||||
return w
|
return w
|
||||||
|
|
||||||
# --- model <-> form -------------------------------------------------- #
|
# --- model <-> form -------------------------------------------------- #
|
||||||
@@ -655,12 +658,18 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
root.addLayout(self._build_topbar())
|
root.addLayout(self._build_topbar())
|
||||||
|
|
||||||
split = QHBoxLayout()
|
# User-draggable divider between the server list and the editor.
|
||||||
split.setSpacing(12)
|
split = QSplitter(Qt.Orientation.Horizontal)
|
||||||
split.addWidget(self._build_left(), 3)
|
split.setChildrenCollapsible(False)
|
||||||
|
split.setHandleWidth(10)
|
||||||
|
split.addWidget(self._build_left())
|
||||||
self.editor = ServerEditor(on_change=self._editor_changed, before_change=self._push_undo)
|
self.editor = ServerEditor(on_change=self._editor_changed, before_change=self._push_undo)
|
||||||
split.addWidget(self.editor, 4)
|
split.addWidget(self.editor)
|
||||||
root.addLayout(split, 1)
|
split.setStretchFactor(0, 3)
|
||||||
|
split.setStretchFactor(1, 4)
|
||||||
|
split.setSizes([390, 520])
|
||||||
|
self.hsplit = split
|
||||||
|
root.addWidget(split, 1)
|
||||||
|
|
||||||
root.addLayout(self._build_actionbar())
|
root.addLayout(self._build_actionbar())
|
||||||
|
|
||||||
@@ -668,8 +677,25 @@ class MainWindow(QMainWindow):
|
|||||||
self.status.setObjectName("statusbar")
|
self.status.setObjectName("statusbar")
|
||||||
root.addWidget(self.status)
|
root.addWidget(self.status)
|
||||||
|
|
||||||
|
self._restore_layout()
|
||||||
self.reload_profiles()
|
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 --------------------------------------------------------- #
|
# --- top bar --------------------------------------------------------- #
|
||||||
def _build_topbar(self):
|
def _build_topbar(self):
|
||||||
bar = QHBoxLayout()
|
bar = QHBoxLayout()
|
||||||
@@ -722,27 +748,48 @@ class MainWindow(QMainWindow):
|
|||||||
head.setObjectName("h1")
|
head.setObjectName("h1")
|
||||||
v.addWidget(head)
|
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 = QLabel("Active")
|
||||||
self.active_label.setObjectName("section")
|
self.active_label.setObjectName("section")
|
||||||
v.addWidget(self.active_label)
|
av.addWidget(self.active_label)
|
||||||
self.active_table = self._make_server_table()
|
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 = QLabel("No active servers. Add one, or Paste JSON.")
|
||||||
self.active_empty.setObjectName("placeholder")
|
self.active_empty.setObjectName("placeholder")
|
||||||
self.active_empty.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.active_empty.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.active_empty.hide()
|
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 = QLabel("⊘ Disabled")
|
||||||
self.disabled_label.setObjectName("sectionDisabled")
|
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 = self._make_server_table(object_name="disabledTable")
|
||||||
self.disabled_table.setMaximumHeight(170)
|
dv.addWidget(self.disabled_table, 1)
|
||||||
v.addWidget(self.disabled_table, 1)
|
|
||||||
self.disabled_empty = QLabel("Nothing disabled.")
|
self.disabled_empty = QLabel("Nothing disabled.")
|
||||||
self.disabled_empty.setObjectName("placeholder")
|
self.disabled_empty.setObjectName("placeholder")
|
||||||
self.disabled_empty.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
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
|
return card
|
||||||
|
|
||||||
# --- bottom action bar ---------------------------------------------- #
|
# --- bottom action bar ---------------------------------------------- #
|
||||||
@@ -1255,6 +1302,7 @@ class MainWindow(QMainWindow):
|
|||||||
if self.dirty and not self._confirm_discard():
|
if self.dirty and not self._confirm_discard():
|
||||||
e.ignore()
|
e.ignore()
|
||||||
return
|
return
|
||||||
|
self._save_layout()
|
||||||
e.accept()
|
e.accept()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user