diff options
Diffstat (limited to 'llamachat/ui.py')
| -rw-r--r-- | llamachat/ui.py | 114 |
1 files changed, 112 insertions, 2 deletions
diff --git a/llamachat/ui.py b/llamachat/ui.py index f398510..feab3f2 100644 --- a/llamachat/ui.py +++ b/llamachat/ui.py @@ -18,7 +18,9 @@ import json import re from pathlib import Path -from PySide6.QtCore import QObject, QRect, Qt, QThread, Signal, Slot +from PySide6.QtCore import ( + QObject, QRect, QSettings, Qt, QThread, Signal, Slot, +) from PySide6.QtGui import ( QAction, QColor, QDesktopServices, QKeySequence, QPainter, QTextDocument, ) @@ -319,6 +321,7 @@ class ChatWindow(QMainWindow): self.prompt_name = cfg.default_prompt self.prompt_custom = "" self.exact_tokens = 0 + self.sidebar_width = 240 self.session_id: int | None = None self.mode = MODE_ONESHOT @@ -338,6 +341,7 @@ class ChatWindow(QMainWindow): self.resize(1000, 700) self.setAcceptDrops(True) self._build_ui() + self._restore_layout() self.refresh_models() self.refresh_prompts() self.refresh_history() @@ -351,6 +355,14 @@ class ChatWindow(QMainWindow): # Top bar: model, mode, search. top = QHBoxLayout() + self.sidebar_button = QPushButton("☰") + self.sidebar_button.setCheckable(True) + self.sidebar_button.setChecked(True) + self.sidebar_button.setFixedWidth(32) + self.sidebar_button.setToolTip("Show or hide the history panel (Ctrl+\\)") + self.sidebar_button.toggled.connect(self.set_sidebar_visible) + top.addWidget(self.sidebar_button) + self.model_box = QComboBox() self.model_box.setMinimumWidth(220) self.model_box.currentTextChanged.connect(self._on_model_changed) @@ -400,7 +412,8 @@ class ChatWindow(QMainWindow): outer.addLayout(top) # Body: history list beside the transcript and input. - splitter = QSplitter(Qt.Horizontal) + self.splitter = QSplitter(Qt.Horizontal) + splitter = self.splitter self.history_list = QListWidget() self.history_list.setMinimumWidth(200) @@ -484,6 +497,97 @@ class ChatWindow(QMainWindow): send_enter.triggered.connect(self.send) self.addAction(send_enter) + sidebar_action = QAction(self) + sidebar_action.setShortcut(QKeySequence("Ctrl+\\")) + sidebar_action.triggered.connect(self.toggle_sidebar) + self.addAction(sidebar_action) + + # -- sidebar ---------------------------------------------------------- + + def _settings(self) -> QSettings: + """Where window layout is remembered. + + Kept beside the config rather than left to QSettings' default so it + is obvious where it lives, and so tests can point it elsewhere. + """ + return QSettings(str(self.cfg.state_path), QSettings.IniFormat) + + def _restore_layout(self) -> None: + """Bring back the sidebar width and visibility from last time.""" + settings = self._settings() + self.sidebar_width = int(settings.value("sidebar/width", 240)) + visible = settings.value("sidebar/visible", True) + # QSettings round-trips booleans as strings on some backends. + if isinstance(visible, str): + visible = visible.lower() not in ("false", "0") + + # setChecked only emits when the value changes, so apply directly + # rather than relying on the signal to do it. capture=False keeps + # the width just read from disk: the splitter has not been laid out + # yet, so asking it now would overwrite that with a default. + self.sidebar_button.blockSignals(True) + self.sidebar_button.setChecked(bool(visible)) + self.sidebar_button.blockSignals(False) + self.set_sidebar_visible(bool(visible), capture=False) + + def _save_layout(self) -> None: + # Hiding already captured the width, so only refresh it while the + # panel is up and the splitter still has something to report. + if self.sidebar_visible(): + self._capture_width() + + settings = self._settings() + settings.setValue("sidebar/width", self.sidebar_width) + settings.setValue("sidebar/visible", self.sidebar_visible()) + settings.sync() + + def toggle_sidebar(self) -> None: + self.sidebar_button.setChecked(not self.sidebar_button.isChecked()) + + def sidebar_visible(self) -> bool: + """Whether the panel is shown. + + isVisible() is False for every child of a window that has not been + mapped yet, so the button's own state is the authority before the + window first appears. + """ + return self.sidebar_button.isChecked() + + def _capture_width(self) -> None: + """Remember the panel's width while the splitter still reports it. + + Read unconditionally rather than gated on visibility: by the time a + toggle reaches here the button has already flipped, so asking + whether the sidebar is shown would skip the capture. + """ + sizes = self.splitter.sizes() + # A zero first pane means it is already collapsed, so there is no + # meaningful width to keep. + if sizes and sizes[0] > 0: + self.sidebar_width = sizes[0] + + def set_sidebar_visible(self, visible: bool, capture: bool = True) -> None: + """Show or hide the history panel, keeping its width across toggles. + + `capture` is False when restoring saved state, where the width comes + from disk and the splitter has nothing meaningful to report yet. + """ + if not visible and capture: + self._capture_width() + + if self.sidebar_button.isChecked() != visible: + self.sidebar_button.blockSignals(True) + self.sidebar_button.setChecked(visible) + self.sidebar_button.blockSignals(False) + self.history_list.setVisible(visible) + self.sidebar_button.setToolTip( + f"{'Hide' if visible else 'Show'} the history panel (Ctrl+\\)" + ) + if visible: + total = sum(self.splitter.sizes()) or self.width() + width = min(self.sidebar_width, max(total - 200, 0)) + self.splitter.setSizes([width, total - width]) + # -- model handling --------------------------------------------------- def refresh_models(self) -> None: @@ -1127,8 +1231,14 @@ class ChatWindow(QMainWindow): return super().keyPressEvent(event) + def hideEvent(self, event) -> None: + """The daemon rarely exits, so persist layout whenever it hides.""" + self._save_layout() + super().hideEvent(event) + def closeEvent(self, event) -> None: """Closing hides; the daemon keeps running for the next toggle.""" + self._save_layout() event.ignore() self.hide() |
