Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use unique indices to identify datasets for appending #449

Merged
merged 29 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1e65e58
Change the append dialog appearance, get_compatibles and append_data …
bkloeckl Nov 17, 2024
88d0fa2
Add changelog entry
bkloeckl Nov 17, 2024
b48dfbe
Ruff formating
bkloeckl Nov 17, 2024
8e29b31
ruff formating
bkloeckl Nov 17, 2024
87acf54
ruff formating imports
bkloeckl Nov 17, 2024
c61c35f
adapted tests
bkloeckl Nov 17, 2024
14c6f3e
adjust indices when needed after auto_duplicate()
Nov 17, 2024
e7aaabc
adjust test
Nov 17, 2024
22845c2
delete wrong test_model, commit correct one
Nov 17, 2024
4baff69
change append dialog styling and add drag-drop functionality
Nov 19, 2024
9b56574
table styling changes
Nov 20, 2024
84f46a6
align correction
Nov 20, 2024
211dabf
Fix changelog
cbrnr Nov 29, 2024
dde2a3a
Improve readability
cbrnr Nov 29, 2024
674051e
Minor fix
cbrnr Nov 29, 2024
22c305b
Fix style
cbrnr Nov 29, 2024
378d197
fix move-bug by sorting rows
bkloeckl Nov 30, 2024
b3be530
drag&drop between source and destiantion table now possible
bkloeckl Nov 30, 2024
d6501cc
Style
bkloeckl Nov 30, 2024
90849b9
ruff error?
bkloeckl Nov 30, 2024
ae03379
Fix style
cbrnr Dec 2, 2024
fcac24e
Reduce redundancy
cbrnr Dec 2, 2024
e9e3350
Use global constant for row height
cbrnr Dec 2, 2024
335942d
Remove viewport update (not necessary)
cbrnr Dec 2, 2024
ae7d492
Remove from paintEvent
cbrnr Dec 2, 2024
538c2b6
Simplify (no set necessary)
cbrnr Dec 3, 2024
809ecf6
Restore horizontal line
cbrnr Dec 3, 2024
e6bfe35
Remove leftover code
cbrnr Dec 3, 2024
7f57aa0
Remove another set
cbrnr Dec 3, 2024
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
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## [UNRELEASED] - YYYY-MM-DD
### Fixed
- Fix a bug where appending data would not be correctly displayed in the history ([#446]) https://github.com/cbrnr/mnelab/pull/446 by [Benedikt Kloeckl](https://github.com/bkloeckl))
### Changed
- Change the append dialog appearance to include original indices used for identifying the data ([#449](https://github.com/cbrnr/mnelab/pull/449) by [Benedikt Klöckl](https://github.com/bkloeckl))

### Fixed
- Fix a bug where appending data would not be correctly displayed in the history ([#446](https://github.com/cbrnr/mnelab/pull/446) by [Benedikt Klöckl](https://github.com/bkloeckl))

## [0.9.2] - 2024-10-20
### Added
Expand Down
162 changes: 134 additions & 28 deletions src/mnelab/dialogs/append.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,119 @@
# License: BSD (3-clause)

from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QColor, QPainter
from PySide6.QtWidgets import (
QAbstractItemView,
QDialog,
QDialogButtonBox,
QGridLayout,
QHeaderView,
QLabel,
QListWidget,
QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
)

ROW_HEIGHT = 10


class DragDropTableWidget(QTableWidget):
def __init__(self, parent=None, items=None):
super().__init__(parent)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDragDropMode(QAbstractItemView.DragDrop)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setDefaultDropAction(Qt.MoveAction)
self.setDropIndicatorShown(False)
self.setDragDropOverwriteMode(False)
self.drop_row = -1

self.setColumnCount(2)
self.horizontalHeader().hide()
self.verticalHeader().hide()
self.setShowGrid(False)
self.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
self.horizontalHeader().setStretchLastSection(True)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
if items is not None:
self.setRowCount(len(items))
for i, (idx, name) in enumerate(items):
self.setItem(i, 0, QTableWidgetItem(str(idx)))
self.setItem(i, 1, QTableWidgetItem(name))
self.style_rows()

def style_rows(self):
for i in range(self.rowCount()):
self.resizeRowToContents(i)
self.setRowHeight(i, ROW_HEIGHT)
self.item(i, 0).setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.item(i, 0).setForeground(QColor("gray"))
self.item(i, 0).setFlags(self.item(i, 0).flags() & ~Qt.ItemIsEditable)
self.item(i, 1).setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.item(i, 1).setFlags(self.item(i, 1).flags() & ~Qt.ItemIsEditable)

def dragMoveEvent(self, event):
drop_row = self.indexAt(event.pos()).row()
if drop_row == -1:
drop_row = self.rowCount()
if drop_row != self.drop_row:
self.drop_row = drop_row
event.accept()
super().dragMoveEvent(event)

def dragLeaveEvent(self, event):
self.drop_row = -1
self.style_rows()
event.accept()

def paintEvent(self, event):
super().paintEvent(event)
if self.drop_row >= 0:
painter = QPainter(self.viewport())
if self.drop_row < self.rowCount():
y = self.visualRect(self.model().index(self.drop_row, 0)).top()
else:
y = self.visualRect(self.model().index(self.rowCount() - 1, 0)).bottom()
painter.drawLine(0, y, self.viewport().width(), y)

def dropEvent(self, event):
source_table = event.source()
drop_row = self.indexAt(event.pos()).row()
if drop_row == -1:
drop_row = self.rowCount()

selected_rows = sorted(
index.row() for index in source_table.selectionModel().selectedRows()
)

if selected_rows:
row_data = []
for row in selected_rows:
row_data.append(
[
source_table.item(row, col).text()
for col in range(source_table.columnCount())
]
)

if source_table == self:
for row in selected_rows:
if row < drop_row:
drop_row -= 1

for row in reversed(selected_rows):
source_table.removeRow(row)

for i, data in enumerate(row_data):
self.insertRow(drop_row + i)
for col, value in enumerate(data):
self.setItem(drop_row + i, col, QTableWidgetItem(value))
self.style_rows()
event.accept()
self.drop_row = -1


class AppendDialog(QDialog):
def __init__(self, parent, compatibles, title="Append data"):
Expand All @@ -26,23 +128,15 @@ def __init__(self, parent, compatibles, title="Append data"):
grid.addWidget(QLabel("Source"), 0, 0, Qt.AlignCenter)
grid.addWidget(QLabel("Destination"), 0, 2, Qt.AlignCenter)

self.source = QListWidget(self)
self.source.setAcceptDrops(True)
self.source.setDragEnabled(True)
self.source.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.source.setDefaultDropAction(Qt.DropAction.MoveAction)
self.source.insertItems(0, [d["name"] for d in compatibles])
grid.addWidget(self.source, 1, 0)
self.source = DragDropTableWidget(self, items=compatibles)

self.move_button = QPushButton("→")
self.move_button.setEnabled(False)
grid.addWidget(self.move_button, 1, 1, Qt.AlignHCenter)

self.destination = QListWidget(self)
self.destination.setAcceptDrops(True)
self.destination.setDragEnabled(True)
self.destination.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.destination.setDefaultDropAction(Qt.DropAction.MoveAction)
self.destination = DragDropTableWidget(self)

grid.addWidget(self.source, 1, 0)
grid.addWidget(self.destination, 1, 2)
vbox.addLayout(grid)

Expand All @@ -62,16 +156,17 @@ def __init__(self, parent, compatibles, title="Append data"):
self.toggle_move_destination()

@property
def names(self):
names = []
for it in range(self.destination.count()):
names.append(self.destination.item(it).text())
return names
def selected_idx(self):
selected = []
for it in range(self.destination.rowCount()):
index_item = self.destination.item(it, 0)
if index_item:
selected.append(int(index_item.text()))
return selected

@Slot()
def toggle_ok_button(self):
"""Toggle OK button."""
if self.destination.count() > 0:
if self.destination.rowCount() > 0:
self.buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)
else:
self.buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)
Expand All @@ -96,11 +191,22 @@ def toggle_move_destination(self):

@Slot()
def move(self):
if self.source.selectedItems():
for item in self.source.selectedItems():
self.destination.addItem(item.text())
self.source.takeItem(self.source.row(item))
elif self.destination.selectedItems():
for item in self.destination.selectedItems():
self.source.addItem(item.text())
self.destination.takeItem(self.destination.row(item))
source_table = self.source if self.source.selectedRanges() else self.destination
destination_table = (
self.destination if self.source.selectedRanges() else self.source
)

rows = sorted(
index.row() for index in source_table.selectionModel().selectedRows()
)

for row in rows:
idx_item = source_table.item(row, 0).clone()
name_item = source_table.item(row, 1).clone()
row_count = destination_table.rowCount()
destination_table.insertRow(row_count)
destination_table.setItem(row_count, 0, idx_item)
destination_table.setItem(row_count, 1, name_item)

for row in reversed(rows):
source_table.removeRow(row)
8 changes: 6 additions & 2 deletions src/mnelab/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,12 @@ def append_data(self):
compatibles = self.model.get_compatibles()
dialog = AppendDialog(self, compatibles)
if dialog.exec():
self.auto_duplicate()
self.model.append_data(dialog.names)
idx_list = dialog.selected_idx
if self.auto_duplicate(): # adjust for index change if duplicated
idx_list = [
idx + 1 if idx >= self.model.index else idx for idx in idx_list
]
self.model.append_data(idx_list)

def plot_data(self):
"""Plot data."""
Expand Down
25 changes: 12 additions & 13 deletions src/mnelab/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,14 +492,14 @@ def crop(self, start, stop):
self.history.append(f"data.crop({start}, {stop})")

def get_compatibles(self):
"""Return a list of data sets that are compatible with the current one.
"""Return indices and names of data sets compatible with the current one.

This function checks which data sets can be appended to the current data set.

Returns
-------
compatibles : list
List with compatible data sets.
compatibles: List[Tuple[int, str]]
List of tuples (index, name) with compatible data sets.
"""
compatibles = []
data = self.current["data"]
Expand Down Expand Up @@ -531,27 +531,26 @@ def get_compatibles(self):
continue
if d["data"].baseline != data.baseline:
continue
compatibles.append(d)
compatibles.append((idx, d["name"]))
return compatibles

@data_changed
def append_data(self, names):
def append_data(self, selected_idx):
"""Append the given raw data sets."""
self.current["name"] += " (appended)"
files = [self.current["data"]]
datasets = [self.current["data"]]
indices = []

for idx, d in enumerate(self.data):
if d["name"] in names:
files.append(d["data"])
indices.append(f"datasets[{idx}]")
for idx in selected_idx:
datasets.append(self.data[idx]["data"])
indices.append(f"datasets[{idx}]")

if self.current["dtype"] == "raw":
self.current["data"] = mne.concatenate_raws(files)
self.current["data"] = mne.concatenate_raws(datasets)
self.history.append(f"mne.concatenate_raws(data, {', '.join(indices)})")
elif self.current["dtype"] == "epochs":
self.current["data"] = mne.concatenate_epochs(files)
self.history.append(f"mne.concatenate_epochs({', '.join(indices)})")
self.current["data"] = mne.concatenate_epochs(datasets)
self.history.append(f"mne.concatenate_epochs(data, {', '.join(indices)})")

@data_changed
def apply_ica(self):
Expand Down
8 changes: 5 additions & 3 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ def test_append_data(edf_files, duplicate_data):
edf_files
), "Number of data sets in model is not equal to number of files after loading"

model.index = 0 # set to sample_0
if duplicate_data:
idx_list = [1, 2] # data sets to append
model.index = 0 # set current data set
if duplicate_data: # adjust for index change if duplicated
model.duplicate_data()
idx_list = [idx + 1 if idx >= model.index else idx for idx in idx_list]

model.append_data(["sample_1", "sample_2"])
model.append_data(idx_list)

assert (
len(model.data) == len(edf_files) + 1 if duplicate_data else len(edf_files)
Expand Down