Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cc157d3
Improve sidebar discoverability and selection visibility
hoechenberger Feb 27, 2026
d1e0a8d
Name -> Filename
hoechenberger Feb 28, 2026
27c36e0
Filename -> Dataset
hoechenberger Feb 28, 2026
d230498
Merge branch 'main' of github.com:cbrnr/mnelab into sidebar
hoechenberger Feb 28, 2026
bac0f15
Apply style change only on macOS
hoechenberger Feb 28, 2026
efc635e
Visually merge columns
hoechenberger Feb 28, 2026
731c079
Change column alignment
hoechenberger Feb 28, 2026
de2d84f
Make delete button bigger and add a tooltip
hoechenberger Feb 28, 2026
cd6b4d6
Formatting
hoechenberger Feb 28, 2026
7e9a2c5
Start dataset numbering from 1 (instead of 0)
hoechenberger Feb 28, 2026
f9019b6
Allow sidebar focus, remove stylesheet
hoechenberger Feb 28, 2026
c11f6af
Fix focus on first load
hoechenberger Feb 28, 2026
86c539c
Enable keyboard navigation (arrow up/down)
hoechenberger Feb 28, 2026
1b1a127
Tab should not move between datasets
hoechenberger Feb 28, 2026
2eaa315
Show close button of "next" row immediately after current how has bee…
hoechenberger Feb 28, 2026
3ea1591
Add pill border
hoechenberger Feb 28, 2026
627af5a
Update CHANGELOG.md
hoechenberger Feb 28, 2026
409a8dd
Apply suggestion from @hoechenberger
hoechenberger Feb 28, 2026
60e386f
Merge branch 'main' of github.com:cbrnr/mnelab into sidebar
hoechenberger Mar 1, 2026
598bbdb
Fix changelog
hoechenberger Mar 1, 2026
64010e2
Review feedback, first pass
hoechenberger Mar 1, 2026
7115d9f
Apply suggestion from @cbrnr
hoechenberger Mar 1, 2026
552b220
Remove unused import
hoechenberger Mar 1, 2026
7ea8033
Revert "Fix changelog"
hoechenberger Mar 1, 2026
2352266
Fix changelog
hoechenberger Mar 1, 2026
d984c70
Merge branch 'sidebar' of github.com:hoechenberger/mnelab into sidebar
hoechenberger Mar 1, 2026
619c55b
Apply suggestions from code review
hoechenberger Mar 1, 2026
31a5a42
Changelog again
hoechenberger Mar 1, 2026
a4e3d5a
Merge branch 'sidebar' of github.com:hoechenberger/mnelab into sidebar
hoechenberger Mar 1, 2026
5ccba59
Do not display Remove button on keyboard navigation
hoechenberger Mar 2, 2026
219d55f
Improve sidebar dataset name readability
hoechenberger Mar 2, 2026
5da8acc
Suppress focus outline & background in sidebar table on Linux & Windows
hoechenberger Mar 2, 2026
5ac70c9
Merge branch 'main' into sidebar
hoechenberger Mar 3, 2026
3fdb55a
Use original type labels and reduce close icon padding
cbrnr Mar 4, 2026
60a0943
Split 40-60 by default
cbrnr Mar 4, 2026
69b2c81
Merge branch 'main' into sidebar
cbrnr Mar 4, 2026
9ca513b
Revise changelog entries
cbrnr Mar 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
## [UNRELEASED] · YYYY-MM-DD
### ✨ Added
- The dataset pane now contains badges that help you easily recognize the data type of each entry ([#576](https://github.com/cbrnr/mnelab/pull/576) by [Richard Höchenberger](https://github.com/hoechenberger))
- The dataset pane now contains badges that make it easy to recognize the data type of each entry ([#576](https://github.com/cbrnr/mnelab/pull/576) by [Richard Höchenberger](https://github.com/hoechenberger))
- Add mask argument to Find Events dialog, which is useful for parsing events in BDF files ([#584](https://github.com/cbrnr/mnelab/pull/584) by [Clemens Brunner](https://github.com/cbrnr))
- The info pane now displays a copy button when hovering over the file name ([#578](https://github.com/cbrnr/mnelab/pull/578) and [#581](https://github.com/cbrnr/mnelab/pull/581) by [Richard Höchenberger](https://github.com/hoechenberger))

### 🔧 Fixed
- Fix toolbar button hover effects on macOS ([#565](https://github.com/cbrnr/mnelab/pull/565) by [Clemens Brunner](https://github.com/cbrnr))
- Add option to navigate between datasets in the sidebar with the up and down arrow keys ([#573](https://github.com/cbrnr/mnelab/pull/573) by [Richard Höchenberger](https://github.com/hoechenberger))
- Improve sidebar styling ([#573](https://github.com/cbrnr/mnelab/pull/573) by [Richard Höchenberger](https://github.com/hoechenberger))
- Improve dark mode theme for the Artifact Detection preview dialog ([#585](https://github.com/cbrnr/mnelab/pull/585) by [Fabian Schellander](https://github.com/SchellanderF))

### 🌀 Changed
Expand Down
53 changes: 42 additions & 11 deletions src/mnelab/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
import numpy as np
from mne import channel_type
from pybvrf import read_bvrf_header
from PySide6.QtCore import QEvent, QMetaObject, QModelIndex, Qt, QUrl, Slot
from PySide6.QtCore import (
QByteArray,
QEvent,
QMetaObject,
QModelIndex,
Qt,
QTimer,
QUrl,
Slot,
)
from PySide6.QtGui import QAction, QDesktopServices, QIcon, QKeySequence
from PySide6.QtWidgets import (
QApplication,
Expand Down Expand Up @@ -426,21 +435,35 @@ def __init__(self, model: Model):
self.sidebar.hide()
self.sidebar.rowsMoved.connect(self._sidebar_move_event)
self.sidebar.itemDelegate().commitData.connect(self._sidebar_edit_event)
self.sidebar.cellClicked.connect(self._update_data)
self.sidebar.currentCellChanged.connect(
lambda row, col, *_: self._update_data(row, col)
)

splitter = QSplitter()
splitter.addWidget(self.sidebar)
self.splitter = QSplitter()
self.splitter.setObjectName("main_splitter")
self.splitter.addWidget(self.sidebar)

self.infowidget = QStackedWidget()
self.infowidget.addWidget(InfoWidget())
emptywidget = EmptyWidget(
itemgetter("open_file", "history", "settings")(self.actions)
)
self.infowidget.addWidget(emptywidget)
splitter.addWidget(self.infowidget)
width = splitter.size().width()
splitter.setSizes((int(width * 0.3), int(width * 0.7)))
self.setCentralWidget(splitter)
self.splitter.addWidget(self.infowidget)
saved_splitter = settings["splitter"]
if saved_splitter:
self.splitter.restoreState(saved_splitter)
else:
QTimer.singleShot(
0,
lambda: self.splitter.setSizes(
[
int(self.splitter.size().width() * 0.4),
int(self.splitter.size().width() * 0.6),
]
),
)
self.setCentralWidget(self.splitter)

self.status_label = QLabel()
self.statusBar().addPermanentWidget(self.status_label)
Expand Down Expand Up @@ -512,12 +535,15 @@ def data_changed(self):
# update sidebar
if len(self.model.data) > 0:
self.sidebar.show()
# block signals during rebuild: setRowCount(0) would otherwise
# emit currentCellChanged with row=-1, corrupting model.index
self.sidebar.blockSignals(True)
self.sidebar.setRowCount(0)
self.sidebar.setRowCount(len(self.model.names))
self.sidebar.setColumnCount(4)

for row_index, name in enumerate(self.model.names):
item_index = QTableWidgetItem(str(row_index))
item_index = QTableWidgetItem(str(row_index + 1))
self.sidebar.setItem(row_index, 0, item_index)

item_name = QTableWidgetItem(name)
Expand All @@ -529,6 +555,8 @@ def data_changed(self):

self.sidebar.style_rows()
self.sidebar.selectRow(self.model.index)
self.sidebar.blockSignals(False)
self.sidebar.setFocus()
else:
self.sidebar.hide()

Expand Down Expand Up @@ -1575,7 +1603,6 @@ def _update_data(self, row, column):
if row != self.model.index:
self.model.index = row
self.data_changed()
self.sidebar.showCloseButton(row)
self.model.history.append(f"data = datasets[{self.model.index}]")

@Slot()
Expand Down Expand Up @@ -1612,7 +1639,11 @@ def _plot_closed(self, event=None):

def event(self, event):
if event.type() == QEvent.Close:
write_settings(size=self.size(), pos=self.pos())
write_settings(
size=self.size(),
pos=self.pos(),
splitter=QByteArray(self.splitter.saveState()),
)
if self.model.history:
print("\n# Command History\n")
print(format_code("\n".join(self.model.history)))
Expand Down
11 changes: 10 additions & 1 deletion src/mnelab/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
from pathlib import Path

from mne import get_config_path
from PySide6.QtCore import QPoint, QSettings, QSize, QStandardPaths, QUrl, Slot
from PySide6.QtCore import (
QByteArray,
QPoint,
QSettings,
QSize,
QStandardPaths,
QUrl,
Slot,
)
from PySide6.QtGui import QDesktopServices, Qt
from PySide6.QtWidgets import (
QComboBox,
Expand Down Expand Up @@ -36,6 +44,7 @@
"size": QSize(700, 500),
"pos": QPoint(100, 100),
"plot_backend": "Matplotlib",
"splitter": QByteArray(),
}


Expand Down
72 changes: 63 additions & 9 deletions src/mnelab/widgets/sidebar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
#
# License: BSD (3-clause)

from PySide6.QtCore import QEvent, QRectF, Qt, Signal
from PySide6.QtGui import QColor, QIcon, QPainter
import sys

from PySide6.QtCore import QEvent, QRect, QRectF, Qt, Signal
from PySide6.QtGui import QColor, QCursor, QIcon, QPainter
from PySide6.QtWidgets import (
QAbstractItemView,
QFrame,
QHeaderView,
QPushButton,
QStyledItemDelegate,
QTableWidget,
QTableWidgetItem,
QToolButton,
)

ROW_HEIGHT = 30

# settings for the data type badges in the sidebar
DTYPE_COLORS = {
"raw": ("#2563EB", "#FFFFFF"), # blue-600 bg, white text
"epochs": ("#92400E", "#FFFFFF"), # amber-800 bg, white text
Expand Down Expand Up @@ -44,6 +47,11 @@ def paint(self, painter, option, index):
painter.setBrush(QColor(bg_hex))
painter.drawRoundedRect(badge_rect, badge_h / 2, badge_h / 2)

# add a subtle border
painter.setPen(QColor(0, 0, 0, 40))
painter.setBrush(Qt.NoBrush)
painter.drawRoundedRect(badge_rect, badge_h / 2, badge_h / 2)

font = painter.font()
font.setPointSizeF(max(6.0, font.pointSizeF() - 1))
painter.setFont(font)
Expand All @@ -58,6 +66,26 @@ def sizeHint(self, option, index):
return hint


class SpanningHeaderView(QHeaderView):
"""Horizontal header where a contiguous range of sections is painted as one."""

def __init__(self, orientation, span_start=0, span_count=1, parent=None):
super().__init__(orientation, parent)
self._span_start = span_start
self._span_count = span_count

def paintSection(self, painter, rect, logical_index):
span_cols = range(self._span_start, self._span_start + self._span_count)
if logical_index in span_cols and logical_index != self._span_start:
return
if logical_index == self._span_start:
total_width = sum(
self.sectionSize(self._span_start + i) for i in range(self._span_count)
)
rect = QRect(rect.x(), rect.y(), total_width, rect.height())
super().paintSection(painter, rect, logical_index)


class SidebarTableWidget(QTableWidget):
rowsMoved = Signal(int, int) # custom signal emitted when drag-and-dropping rows

Expand All @@ -73,25 +101,42 @@ def __init__(self, parent):
self.setDropIndicatorShown(False)
self.setDragDropOverwriteMode(False)
self.setFrameStyle(QFrame.NoFrame)
self.setFocusPolicy(Qt.NoFocus)
self.setEditTriggers(QAbstractItemView.DoubleClicked)
self.setColumnCount(4)
self.setShowGrid(False)
if sys.platform != "darwin":
# disable cell style changes upon focusing (clicking); not needed on macOS
self.setStyleSheet("""
QTableWidget { outline: 0; }
QTableWidget::item:focus {
background: palette(highlight);
color: palette(highlighted-text);
}
""")
self.drop_row = -1

self.horizontalHeader().hide()
header = SpanningHeaderView(
Qt.Horizontal, span_start=1, span_count=3, parent=self
)
self.setHorizontalHeader(header)
self.setHorizontalHeaderLabels(["#", "Dataset", "", ""])
self.horizontalHeaderItem(0).setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.horizontalHeaderItem(1).setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.verticalHeader().hide()
self.horizontalHeader().setStretchLastSection(False)
self.horizontalHeader().setMinimumSectionSize(0)
self.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed)
self.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed)
self.horizontalHeader().setSectionResizeMode(3, QHeaderView.Fixed)
self.setColumnWidth(2, 56)
self.setColumnWidth(3, 20)
self.setColumnWidth(3, 28)
self.resizeColumnToContents(0)
self.setItemDelegateForColumn(2, TypeBadgeDelegate(self))

self.horizontalHeader().hide()
self.setAccessibleName("Opened datasets")
self.setMouseTracking(True)
self.setTabKeyNavigation(False)
self.viewport().installEventFilter(self)

def mousePressEvent(self, event):
Expand Down Expand Up @@ -180,6 +225,11 @@ def style_rows(self):
self.item(i, 1).setFlags(self.item(i, 1).flags() | Qt.ItemIsEditable)
if self.item(i, 2) is not None:
self.item(i, 2).setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
# after a rebuild (e.g. row removed), the cursor may already be hovering
# over a row without a MouseMove firing — update the close button immediately
pos = self.viewport().mapFromGlobal(QCursor.pos())
index = self.indexAt(pos)
self.showCloseButton(index.row() if index.isValid() else -1)

def update_vertical_header(self):
row_count = self.rowCount()
Expand All @@ -200,10 +250,14 @@ def eventFilter(self, source, event):
def showCloseButton(self, row_index):
for i in range(self.rowCount()):
if i == row_index:
delete_button = QPushButton(self)
delete_button = QToolButton(self)
delete_button.setFocusPolicy(Qt.NoFocus)
delete_button.setFixedSize(24, ROW_HEIGHT)
delete_button.setIcon(QIcon.fromTheme("close-data"))
delete_button.setToolTip("Close dataset")
delete_button.setAutoRaise(True)
delete_button.setStyleSheet(
"background: transparent; border: none; margin: auto;"
"QToolButton { background: transparent; border: none; }"
)
delete_button.clicked.connect(
lambda _, index=row_index: self.parent.model.remove_data(index)
Expand Down