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

PR: Fix issues with scrolling in dataframe editor (Variable Explorer) #21913

Merged
merged 2 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 36 additions & 27 deletions spyder/plugins/variableexplorer/widgets/dataframeeditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,10 @@ def __init__(self, parent, model, header, hscroll, vscroll,
self.resize_action = None
self.resize_columns_action = None

self.menu = self.setup_menu()
self.menu_header_h = self.setup_menu_header()
self.config_shortcut(self.copy, 'copy', self)

self.setModel(model)
self.setHorizontalScrollBar(hscroll)
self.setVerticalScrollBar(vscroll)
Expand All @@ -671,14 +675,9 @@ def __init__(self, parent, model, header, hscroll, vscroll,
self.show_header_menu)
self.header_class.sectionClicked.connect(self.sortByColumn)
self.data_function = data_function
self.menu = self.setup_menu()
self.menu_header_h = self.setup_menu_header()
self.config_shortcut(self.copy, 'copy', self)
self.horizontalScrollBar().valueChanged.connect(
self._load_more_columns)
self.verticalScrollBar().valueChanged.connect(self._load_more_rows)
self.selectionModel().selectionChanged.connect(self.refresh_menu)
self.refresh_menu()

def _load_more_columns(self, value):
"""Load more columns to display."""
Expand Down Expand Up @@ -713,6 +712,17 @@ def load_more_data(self, value, rows=False, columns=False):
# See spyder-ide/spyder#7880.
pass

def setModel(self, model: DataFrameModel) -> None:
"""
Set the model for the view to present.

This overrides the function in QTableView so that we can enable or
disable actions when appropriate if the selection changes.
"""
super().setModel(model)
self.selectionModel().selectionChanged.connect(self.refresh_menu)
self.refresh_menu()

def sortByColumn(self, index):
"""Implement a column sort."""
if self.sort_old == [None]:
Expand Down Expand Up @@ -1417,22 +1427,8 @@ def __init__(self, model, axis, use_monospace_font=False):

self.total_rows = self.model.shape[0]
self.total_cols = self.model.shape[1]
size = self.total_rows * self.total_cols

# Use paging when the total size, number of rows or number of
# columns is too large
if size > LARGE_SIZE:
self.rows_loaded = ROWS_TO_LOAD
self.cols_loaded = COLS_TO_LOAD
else:
if self.total_cols > LARGE_COLS:
self.cols_loaded = COLS_TO_LOAD
else:
self.cols_loaded = self.total_cols
if self.total_rows > LARGE_NROWS:
self.rows_loaded = ROWS_TO_LOAD
else:
self.rows_loaded = self.total_rows
self.cols_loaded = self.model.cols_loaded
self.rows_loaded = self.model.rows_loaded

if self.axis == 0:
self.total_cols = self.model.shape[1]
Expand Down Expand Up @@ -1687,6 +1683,10 @@ def data(self, index, role):
return None


class EmptyDataFrame:
shape = (0, 0)


class DataFrameEditor(BaseDialog, SpyderWidgetMixin):
"""
Dialog for displaying and editing DataFrame and related objects.
Expand Down Expand Up @@ -1790,9 +1790,23 @@ def setup_ui(self, title: str) -> None:
# Create menu to allow edit index
self.menu_header_v = self.setup_menu_header(self.table_index)

# Create the model and view of the data
empty_data = EmptyDataFrame()
self.dataModel = DataFrameModel(empty_data, parent=self)
self.dataModel.dataChanged.connect(self.save_and_close_enable)
self.create_data_table()

self.glayout.addWidget(self.hscroll, 2, 0, 1, 2)
self.glayout.addWidget(self.vscroll, 0, 2, 2, 1)

# autosize columns on-demand
self._autosized_cols = set()

# Set limit time to calculate column sizeHint to 300ms,
# See spyder-ide/spyder#11060
self._max_autosize_ms = 300
self.dataTable.installEventFilter(self)

avg_width = self.fontMetrics().averageCharWidth()
self.min_trunc = avg_width * 12 # Minimum size for columns
self.max_width = avg_width * 64 # Maximum size for columns
Expand Down Expand Up @@ -1853,7 +1867,7 @@ def set_data_and_check(self, data) -> bool:
# Create the model and view of the data
self.dataModel = DataFrameModel(data, parent=self)
self.dataModel.dataChanged.connect(self.save_and_close_enable)
self.create_data_table()
self.dataTable.setModel(self.dataModel)

# autosize columns on-demand
self._autosized_cols = set()
Expand Down Expand Up @@ -2034,11 +2048,6 @@ def create_table_index(self):

def create_data_table(self):
"""Create the QTableView that will hold the data model."""
if self.dataTable:
self.layout.removeWidget(self.dataTable)
self.dataTable.deleteLater()
self.dataTable = None

self.dataTable = DataFrameView(self, self.dataModel,
self.table_header.horizontalHeader(),
self.hscroll, self.vscroll,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from spyder.utils.test import close_message_box
from spyder.plugins.variableexplorer.widgets import dataframeeditor
from spyder.plugins.variableexplorer.widgets.dataframeeditor import (
DataFrameEditor, DataFrameModel)
DataFrameEditor, DataFrameModel, COLS_TO_LOAD, LARGE_COLS)


# =============================================================================
Expand Down Expand Up @@ -150,6 +150,77 @@ def test_dataframe_to_type(qtbot):
assert editor.btn_save_and_close.isEnabled()


def test_dataframe_editor_shows_scrollbar(qtbot):
"""
Test the dataframe editor shows a scrollbar when opening a large dataframe.
Regression test for spyder-ide/spyder#21627 .
"""
df = DataFrame(numpy.zeros((100, 100)))
editor = DataFrameEditor()
editor.setup_and_check(df)
with qtbot.waitExposed(editor):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice! I didn't know about this functionality in qtbot.

editor.show()

assert editor.dataTable.horizontalScrollBar().isVisible()


def test_dataframe_editor_scroll(qtbot):
"""
Test that when opening a "large" dataframe, only a part of it is initially
loaded in the editor window. When scrolling past that part, the rest is
loaded. When moving to the right-most column and sorting it, the view
stay scrolled to the right end.

Regression test for spyder-ide/spyder#21627 .
"""

# Make DataFrame with LARGE_COLS + 5 columns
df = DataFrame(numpy.zeros((10, LARGE_COLS + 5)))
editor = DataFrameEditor()
editor.setup_and_check(df)
model = editor.dataModel
with qtbot.waitExposed(editor):
editor.show()

# Check that initially, COLS_TO_LOAD columns are loaded in the editor
assert model.rowCount() == 10
assert model.columnCount() == COLS_TO_LOAD

# Press the End key to move to the right and wait
view = editor.dataTable
view.setCurrentIndex(view.model().index(0, 0))
qtbot.keyPress(view, Qt.Key_End)

# Check that now all the columns are loaded in the editor
def check_column_count():
assert model.columnCount() == LARGE_COLS + 5

qtbot.waitUntil(check_column_count)

# Press the End key to move to the right and wait
qtbot.keyPress(view, Qt.Key_End)
scrollbar = editor.dataTable.horizontalScrollBar()

# Check that we are at the far right
def check_at_far_right():
assert scrollbar.value() == scrollbar.maximum()

qtbot.waitUntil(check_at_far_right)

# Sort the rightmost column
old_index_model = editor.table_index.model()
view.sortByColumn(model.columnCount() - 1)

# Wait until the model for the index is updated
def check_index_model_updated():
assert editor.table_index.model() != old_index_model

qtbot.waitUntil(check_index_model_updated)

# Check that we are at the far right
assert scrollbar.value() == scrollbar.maximum()


def test_dataframe_datetimeindex(qtbot):
"""Regression test for spyder-ide/spyder#11129 ."""
ds = Series(
Expand Down