Skip to content

Commit b347dd1

Browse files
correcting_column_targeting (#70)
* Removed bug where we deleted wrong column due to index offset * Only allow deletion of optional columns * Fixed bug where columns where not deleted but invalid cells was still wrongly updated * Added a tableviewer class that handles column resizing * Fix bug in conflict resolve.
1 parent 847afc3 commit b347dd1

3 files changed

Lines changed: 137 additions & 25 deletions

File tree

src/petab_gui/controllers/table_controllers.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ def overwrite_df(self, new_df: pd.DataFrame):
170170
)
171171
# test: overwrite the new model as source model
172172
self.proxy_model.setSourceModel(self.model)
173+
# change default sizing
174+
self.view.table_view.reset_column_sizes()
173175
self.overwritten_df.emit()
174176

175177
def append_df(self, new_df: pd.DataFrame):
@@ -210,12 +212,12 @@ def delete_row(self):
210212
for row in sorted(selected_rows, reverse=True):
211213
if row >= self.model.rowCount() - 1:
212214
continue
215+
self.model.delete_row(row)
213216
self.logger.log_message(
214217
f"Deleted row {row} from {self.model.table_type} table."
215218
f" Data: {self.model.get_df().iloc[row].to_dict()}",
216219
color="orange"
217220
)
218-
self.model.delete_row(row)
219221
self.model.something_changed.emit(True)
220222

221223
def add_row(self):
@@ -240,19 +242,27 @@ def delete_column(self):
240242
selected_columns = get_selected(table_view, mode=COLUMN)
241243
if not selected_columns:
242244
return
243-
self.model.update_invalid_cells(selected_columns, mode="columns")
245+
deleted_columns = set()
244246
for column in sorted(selected_columns, reverse=True):
245247
# safely delete potential item delegates
246-
column_name = self.model.get_df().columns[column]
248+
allow_del, column_name = self.model.allow_column_deletion(column)
249+
if not allow_del:
250+
self.logger.log_message(
251+
f"Cannot delete column {column_name}, as it is a "
252+
f"required column!",
253+
color = "red"
254+
)
255+
continue
247256
if column_name in self.completers:
248257
self.view.table_view.setItemDelegateForColumn(column, None)
249258
del self.completers[column_name]
250-
column_name = self.model.get_df().columns[column]
259+
self.model.delete_column(column)
251260
self.logger.log_message(
252261
f"Deleted column '{column_name}' from {self.model.table_type} table.",
253262
color="orange"
254263
)
255-
self.model.delete_column(column)
264+
deleted_columns.add(column)
265+
self.model.update_invalid_cells(deleted_columns, mode="columns")
256266
self.model.something_changed.emit(True)
257267

258268
def add_column(self, column_name: str = None):

src/petab_gui/models/pandas_table_model.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ def __init__(self, data_frame, allowed_columns, table_type, parent=None):
2828
self._data_frame = data_frame
2929
# add a view here, access is needed for selectionModels
3030
self.view = None
31+
# offset for row and column to get from the data_frame to the view
32+
self.row_index_offset = 0
33+
self.column_offset = 0
3134

3235
def rowCount(self, parent=QModelIndex()):
3336
return self._data_frame.shape[0] + 1 # empty row at the end
3437

3538
def columnCount(self, parent=QModelIndex()):
36-
return self._data_frame.shape[1] + 1 # measurement needs other
39+
return self._data_frame.shape[1] + self.column_offset
3740

3841
def data(self, index, role=Qt.DisplayRole):
3942
"""Return the data at the given index and role for the View."""
@@ -275,6 +278,8 @@ def discard_invalid_cell(self, row, column):
275278

276279
def update_invalid_cells(self, selected, mode: str = "rows"):
277280
"""Edits the invalid cells when values are deleted."""
281+
if not selected:
282+
return
278283
old_invalid_cells = self._invalid_cells.copy()
279284
new_invalid_cells = set()
280285
sorted_to_del = sorted(selected)
@@ -312,10 +317,8 @@ def get_value_from_column(self, column_name, row):
312317
return ""
313318

314319
def return_column_index(self, column_name):
315-
"""Return the index of a column."""
316-
if column_name in self._data_frame.columns:
317-
return self._data_frame.columns.get_loc(column_name) + 1
318-
return -1
320+
"""Return the index of a column. Defined in Subclasses"""
321+
pass
319322

320323
def unique_values(self, column_name):
321324
"""Return the unique values in a column."""
@@ -333,8 +336,8 @@ def delete_row(self, row):
333336

334337
def delete_column(self, column_index):
335338
"""Delete a column from the DataFrame."""
339+
column_name = self._data_frame.columns[column_index - self.column_offset]
336340
self.beginRemoveColumns(QModelIndex(), column_index, column_index)
337-
column_name = self._data_frame.columns[column_index]
338341
self._data_frame.drop(columns=[column_name], inplace=True)
339342
self.endRemoveColumns()
340343

@@ -437,6 +440,15 @@ def determine_background_color(self, row, column):
437440
return QColor(144, 190, 109, 102)
438441
return QColor(177, 217, 231, 102)
439442

443+
def allow_column_deletion(self, column: int) -> bool:
444+
"""Checks whether the column can safely be deleted"""
445+
if column == 0 and self._has_named_index:
446+
return False, self._data_frame.index.name
447+
column_name = self._data_frame.columns[column-self.column_offset]
448+
if column_name not in self._allowed_columns.keys():
449+
return True, column_name
450+
return self._allowed_columns[column_name]["optional"], column_name
451+
440452

441453
class IndexedPandasTableModel(PandasTableModel):
442454
"""Table model for tables with named index."""
@@ -450,6 +462,7 @@ def __init__(self, data_frame, allowed_columns, table_type, parent=None):
450462
parent=parent
451463
)
452464
self._has_named_index = True
465+
self.column_offset = 1
453466

454467
def handle_named_index(self, index, value):
455468
"""Handle the named index column."""
@@ -499,9 +512,6 @@ def __init__(self, data_frame, parent=None):
499512
parent=parent
500513
)
501514

502-
def columnCount(self, parent=QModelIndex()):
503-
return self._data_frame.shape[1]
504-
505515
def data(self, index, role=Qt.DisplayRole):
506516
"""Return the data at the given index and role for the View."""
507517
if not index.isValid():

src/petab_gui/views/table_view.py

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from PySide6.QtWidgets import QDockWidget, QVBoxLayout, QTableView, QWidget,\
2-
QCompleter, QLineEdit, QStyledItemDelegate, QComboBox
3-
from PySide6.QtCore import Qt
4-
from PySide6.QtGui import QGuiApplication
1+
from PySide6.QtWidgets import (QDockWidget, QHeaderView, QTableView,
2+
QCompleter, QLineEdit, QStyledItemDelegate,
3+
QComboBox)
4+
from PySide6.QtCore import Qt, QPropertyAnimation, QRect
5+
from PySide6.QtGui import QGuiApplication, QColor
56

67
from ..utils import get_selected, get_selected_rectangles
78
from ..C import INDEX
@@ -15,13 +16,9 @@ def __init__(self, title, parent=None):
1516
self.setAllowedAreas(
1617
Qt.AllDockWidgetAreas
1718
)
18-
widget = QWidget()
19-
self.setWidget(widget)
20-
layout = QVBoxLayout(widget)
21-
2219
# Create the QTableView for the table content
23-
self.table_view = QTableView()
24-
layout.addWidget(self.table_view)
20+
self.table_view = CustomTableView()
21+
self.setWidget(self.table_view)
2522
# Dictionary to store column-specific completers
2623
self.completers = {}
2724
self.table_view.setAlternatingRowColors(True)
@@ -48,7 +45,8 @@ def paste_from_clipboard(self):
4845
model = self.table_view.model()
4946
row_start, col_start = start_index.row(), start_index.column()
5047
# identify which invalid cells are being pasted into
51-
pasted_data = [line.split("\t") for line in text.split("\n") if line.strip()]
48+
pasted_data = [line.split("\t") for line in text.split("\n") if
49+
line.strip()]
5250
num_rows = len(pasted_data)
5351
num_cols = max([len(line) for line in pasted_data])
5452
overridden_cells = {
@@ -85,6 +83,7 @@ def createEditor(self, parent, option, index):
8583
class SingleSuggestionDelegate(QStyledItemDelegate):
8684
"""Suggest a single option based the current row and the value in
8785
`column_name`."""
86+
8887
def __init__(self, model, suggestions_column, afix=None, parent=None):
8988
super().__init__(parent)
9089
self.model = model # The main model to retrieve data from
@@ -110,6 +109,7 @@ def createEditor(self, parent, option, index):
110109

111110
return editor
112111

112+
113113
class ColumnSuggestionDelegate(QStyledItemDelegate):
114114
"""Suggest options based on all unique values in the specified column."""
115115
def __init__(
@@ -171,3 +171,95 @@ def createEditor(self, parent, option, index):
171171
editor.setCompleter(completer)
172172

173173
return editor
174+
175+
176+
class CustomTableView(QTableView):
177+
"""Custom Table View to Handle Copy Paste events, resizing policies etc."""
178+
179+
def __init__(self, parent=None):
180+
super().__init__(parent)
181+
self.setSizeAdjustPolicy(QTableView.AdjustToContents)
182+
self.horizontalHeader().setSectionResizeMode(
183+
QHeaderView.ResizeToContents
184+
)
185+
self.horizontalHeader().setStretchLastSection(
186+
False
187+
) # Prevent last column from stretching
188+
189+
self.horizontalHeader().sectionDoubleClicked.connect(
190+
self.autofit_column
191+
)
192+
193+
def setModel(self, model):
194+
"""Ensures selection model exists before connecting signals"""
195+
super().setModel(model)
196+
if self.selectionModel():
197+
self.selectionModel().currentColumnChanged.connect(self.highlight_active_column)
198+
199+
def reset_column_sizes(self):
200+
"""Resets column sizes with refinements"""
201+
header = self.horizontalHeader()
202+
total_width = self.viewport().width()
203+
max_width = total_width // 4 # 1/4th of total table width
204+
205+
header.setSectionResizeMode(QHeaderView.ResizeToContents)
206+
self.resizeColumnsToContents()
207+
header.setSectionResizeMode(QHeaderView.Interactive)
208+
209+
# Enforce max width but allow expanding into empty neighbors
210+
for col in range(self.model().columnCount()):
211+
optimal_width = self.columnWidth(col)
212+
if optimal_width > max_width:
213+
self.setColumnWidth(col, max_width)
214+
else:
215+
self.setColumnWidth(col, optimal_width)
216+
217+
# self.adjust_for_empty_neighbors()
218+
self.collapse_empty_columns()
219+
self.updateGeometry()
220+
221+
def adjust_for_empty_neighbors(self):
222+
"""Expands column if adjacent columns are empty"""
223+
model = self.model()
224+
for col in range(model.columnCount()):
225+
if self.columnWidth(col) == self.viewport().width() // 4: # If maxed out
226+
next_col = col + 1
227+
if next_col < model.columnCount():
228+
if all(model.index(row, next_col).data() in [None, ""] for row in range(model.rowCount())):
229+
new_width = self.columnWidth(
230+
col) + self.columnWidth(next_col)
231+
self.setColumnWidth(col, new_width)
232+
self.setColumnWidth(next_col, 0) # Hide empty column
233+
234+
def collapse_empty_columns(self):
235+
"""Collapses columns that only contain empty values"""
236+
model = self.model()
237+
for col in range(model.columnCount()):
238+
if all(model.index(row, col).data() in [None, "", " "] for row in
239+
range(model.rowCount())):
240+
self.setColumnWidth(col, 10) # Minimal width
241+
242+
def autofit_column(self, col):
243+
"""Expands column width on double-click"""
244+
self.horizontalHeader().setSectionResizeMode(col,
245+
QHeaderView.ResizeToContents)
246+
self.resizeColumnToContents(col)
247+
self.horizontalHeader().setSectionResizeMode(col,
248+
QHeaderView.Interactive)
249+
250+
def highlight_active_column(self, index):
251+
"""Highlights the active column"""
252+
for row in range(self.model().rowCount()):
253+
self.model().setData(self.model().index(row, index.column()),
254+
QColor("#cce6ff"), Qt.BackgroundRole)
255+
256+
def animate_column_resize(self, col, new_width):
257+
"""Smoothly animates column resizing"""
258+
anim = QPropertyAnimation(self, b"geometry")
259+
anim.setDuration(200)
260+
anim.setStartValue(QRect(self.columnViewportPosition(col), 0,
261+
self.columnWidth(col), self.height()))
262+
anim.setEndValue(
263+
QRect(self.columnViewportPosition(col), 0, new_width,
264+
self.height()))
265+
anim.start()

0 commit comments

Comments
 (0)