Skip to content

Commit

Permalink
Backport PR #23376 on branch 6.x (PR: Display a button to select the …
Browse files Browse the repository at this point in the history
…entire row when hovering it in `CollectionsEditor` (Variable Explorer)) (#23470)
  • Loading branch information
meeseeksmachine authored Jan 15, 2025
1 parent 479880d commit 7f8423a
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 8 deletions.
108 changes: 105 additions & 3 deletions spyder/plugins/variableexplorer/widgets/collectionsdelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,30 @@

# Third party imports
from qtpy.compat import to_qvariant
from qtpy.QtCore import QDateTime, QModelIndex, Qt, Signal
from qtpy.QtCore import (
QDateTime,
QEvent,
QItemSelection,
QItemSelectionModel,
QModelIndex,
QRect,
QSize,
Qt,
Signal,
)
from qtpy.QtGui import QMouseEvent
from qtpy.QtWidgets import (
QAbstractItemDelegate, QDateEdit, QDateTimeEdit, QItemDelegate, QLineEdit,
QMessageBox, QTableView)
QAbstractItemDelegate,
QApplication,
QDateEdit,
QDateTimeEdit,
QItemDelegate,
QLineEdit,
QMessageBox,
QStyle,
QStyleOptionButton,
QTableView,
)
from spyder_kernels.utils.lazymodules import (
FakeObject, numpy as np, pandas as pd, PIL)
from spyder_kernels.utils.nsview import (display_to_value, is_editable_type,
Expand All @@ -33,10 +53,12 @@
from spyder.plugins.variableexplorer.widgets.dataframeeditor import (
DataFrameEditor)
from spyder.plugins.variableexplorer.widgets.texteditor import TextEditor
from spyder.utils.icon_manager import ima


LARGE_COLLECTION = 1e5
LARGE_ARRAY = 5e6
SELECT_ROW_BUTTON_SIZE = 22


class CollectionsDelegate(QItemDelegate, SpyderFontsMixin):
Expand Down Expand Up @@ -472,6 +494,86 @@ def updateEditorGeometry(self, editor, option, index):
super(CollectionsDelegate, self).updateEditorGeometry(
editor, option, index)

def paint(self, painter, option, index):
"""Actions to take when painting a cell."""
if (
# Do this only for the last column
index.column() == 3
# Do this when the row is hovered or if it's selected
and (
index.row() == self.parent().hovered_row
or index.row() in self.parent().selected_rows()
)
):
# Paint regular contents
super().paint(painter, option, index)

# Paint an extra button to select the entire row. This is necessary
# because in Spyder 6 is not intuitive how to do that since we use
# a single click to open the editor associated to the cell.
# Fixes spyder-ide/spyder#22524
# Solution adapted from https://stackoverflow.com/a/11778012/438386

# Getting the cell's rectangle
rect = option.rect

# Button left/top coordinates
x = rect.left() + rect.width() - SELECT_ROW_BUTTON_SIZE
y = rect.top() + rect.height() // 2 - SELECT_ROW_BUTTON_SIZE // 2

# Create and paint button
button = QStyleOptionButton()
button.rect = QRect(
x, y, SELECT_ROW_BUTTON_SIZE, SELECT_ROW_BUTTON_SIZE
)
button.text = ""
button.icon = (
ima.icon("select_row")
if index.row() not in self.parent().selected_rows()
else ima.icon("deselect_row")
)
button.iconSize = QSize(20, 20)
button.state = QStyle.State_Enabled
QApplication.style().drawControl(
QStyle.CE_PushButtonLabel, button, painter
)
else:
super().paint(painter, option, index)

def editorEvent(self, event, model, option, index):
"""Actions to take when interacting with a cell."""
if event.type() == QEvent.MouseButtonRelease and index.column() == 3:
# Getting the position of the mouse click
click_x = QMouseEvent(event).x()
click_y = QMouseEvent(event).y()

# Getting the cell's rectangle
rect = option.rect

# Region for the select row button
x = rect.left() + rect.width() - SELECT_ROW_BUTTON_SIZE
y = rect.top()

# Select/deselect row when clicking on the button
if click_x > x and (y < click_y < (y + SELECT_ROW_BUTTON_SIZE)):
row = index.row()
if row in self.parent().selected_rows():
# Deselect row if selected
index_left = index.sibling(row, 0)
index_right = index.sibling(row, 3)
selection = QItemSelection(index_left, index_right)
self.parent().selectionModel().select(
selection, QItemSelectionModel.Deselect
)
else:
self.parent().selectRow(row)
else:
super().editorEvent(event, model, option, index)
else:
super().editorEvent(event, model, option, index)

return False


class ToggleColumnDelegate(CollectionsDelegate):
"""ToggleColumn Item Delegate"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# -----------------------------------------------------------------------------

# Standard library imports
from functools import lru_cache
import logging
from typing import Any, Callable, Optional

Expand Down Expand Up @@ -161,6 +162,8 @@ class ToggleColumnTreeView(QTreeView, ToggleColumnMixIn):
A QTreeView where right clicking on the header allows the user to
show/hide columns.
"""
# Dummy conf section to avoid a warning
CONF_SECTION = ""

def __init__(
self,
Expand All @@ -180,11 +183,19 @@ def __init__(
self.expanded.connect(self.resize_columns_to_contents)
self.collapsed.connect(self.resize_columns_to_contents)

# Dummy attribute to be compatible with BaseTableView
self.hovered_row = -1

@Slot()
def resize_columns_to_contents(self):
"""Resize all the columns to its contents."""
self._horizontal_header().resizeSections(QHeaderView.ResizeToContents)

@lru_cache(maxsize=1)
def selected_rows(self):
"""Dummy method to be compatible with BaseTableView."""
return set()

def _horizontal_header(self):
"""
Returns the horizontal header (of type QHeaderView).
Expand Down
2 changes: 2 additions & 0 deletions spyder/utils/icon_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ def __init__(self):
'rename': [('mdi.rename-box',), {'color': self.MAIN_FG_COLOR}],
'move': [('mdi.file-move',), {'color': self.MAIN_FG_COLOR}],
'edit_add': [('mdi.plus-box',), {'color': self.MAIN_FG_COLOR}],
'select_row': [('mdi.plus-box-outline',), {'color': self.MAIN_FG_COLOR}],
'deselect_row': [('mdi.minus-box-outline',), {'color': self.MAIN_FG_COLOR}],
'duplicate_row': [('ph.rows',), {'color': self.MAIN_FG_COLOR}],
'duplicate_column': [('ph.columns',), {'color': self.MAIN_FG_COLOR}],
'collapse_column': [('mdi.arrow-collapse-horizontal',), {'color': self.MAIN_FG_COLOR}],
Expand Down
87 changes: 82 additions & 5 deletions spyder/widgets/collectionseditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@

# Standard library imports
import datetime
from functools import lru_cache
import io
import re
import sys
import warnings
import textwrap
from typing import Any, Callable, Optional
import warnings

# Third party imports
from qtpy.compat import getsavefilename, to_qvariant
Expand Down Expand Up @@ -55,7 +57,9 @@
from spyder.utils.qthelpers import mimedata2url
from spyder.utils.stringmatching import get_search_scores, get_search_regex
from spyder.plugins.variableexplorer.widgets.collectionsdelegate import (
CollectionsDelegate)
CollectionsDelegate,
SELECT_ROW_BUTTON_SIZE,
)
from spyder.plugins.variableexplorer.widgets.importwizard import ImportWizard
from spyder.widgets.helperwidgets import CustomSortFilterProxy
from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog
Expand Down Expand Up @@ -479,6 +483,15 @@ def data(self, index, role=Qt.DisplayRole):
else:
display = value
if role == Qt.ToolTipRole:
if self.parent().over_select_row_button:
if index.row() in self.parent().selected_rows():
tooltip = _("Click to deselect this row")
else:
tooltip = _(
"Click to select this row. Maintain pressed Ctrl (Cmd "
"on macOS) for multiple rows"
)
return '\n'.join(textwrap.wrap(tooltip, 50))
return display
if role == Qt.UserRole:
if isinstance(value, NUMERIC_TYPES):
Expand Down Expand Up @@ -653,6 +666,7 @@ class BaseTableView(QTableView, SpyderWidgetMixin):
def __init__(self, parent):
super().__init__(parent=parent)

# Main attributes
self.array_filename = None
self.menu = None
self.empty_ws_menu = None
Expand All @@ -678,6 +692,8 @@ def __init__(self, parent):
self.source_model = None
self.setAcceptDrops(True)
self.automatic_column_width = True

# Headder attributes
self.setHorizontalHeader(BaseHeaderView(parent=self))
self.horizontalHeader().sig_user_resized_section.connect(
self.user_resize_columns)
Expand All @@ -698,15 +714,26 @@ def __init__(self, parent):
self._edit_value_timer.setSingleShot(True)
self._edit_value_timer.timeout.connect(self._edit_value)

# To paint the select row button and check if we are over it
self.hovered_row = -1
self.over_select_row_button = False

def setup_table(self):
"""Setup table"""
self.horizontalHeader().setStretchLastSection(True)
self.horizontalHeader().setSectionsMovable(True)
self.adjust_columns()

# Sorting columns
self.setSortingEnabled(True)
self.sortByColumn(0, Qt.AscendingOrder)

# Actions to take when the selection changes
self.selectionModel().selectionChanged.connect(self.refresh_menu)
self.selectionModel().selectionChanged.connect(
# We need this because selected_rows is cached
self.selected_rows.cache_clear
)

def setup_menu(self):
"""Setup actions and context menu"""
Expand Down Expand Up @@ -1037,9 +1064,17 @@ def set_data(self, data):
def _edit_value(self):
self.edit(self.__index_clicked)

def _update_hovered_row(self, event):
current_index = self.indexAt(event.pos())
if current_index.isValid():
self.hovered_row = current_index.row()
self.viewport().update()
else:
self.hovered_row = -1

def mousePressEvent(self, event):
"""Reimplement Qt method"""
if event.button() != Qt.LeftButton:
if event.button() != Qt.LeftButton or self.over_select_row_button:
QTableView.mousePressEvent(self, event)
return

Expand Down Expand Up @@ -1070,9 +1105,26 @@ def mouseDoubleClickEvent(self, event):
pass

def mouseMoveEvent(self, event):
"""Change cursor shape."""
"""Actions to take when the mouse moves over the widget."""
self.over_select_row_button = False
self._update_hovered_row(event)

if self.rowAt(event.y()) != -1:
self.setCursor(Qt.PointingHandCursor)
# The +3 here is necessary to avoid mismatches when trying to click
# the button in a position too close to its left border.
select_row_button_width = SELECT_ROW_BUTTON_SIZE + 3

# Include scrollbar width when computing the select row button
# width
if self.verticalScrollBar().isVisible():
select_row_button_width += self.verticalScrollBar().width()

# Decide if the cursor is on top of the select row button
if (self.width() - event.x()) < select_row_button_width:
self.over_select_row_button = True
self.setCursor(Qt.ArrowCursor)
else:
self.setCursor(Qt.PointingHandCursor)
else:
self.setCursor(Qt.ArrowCursor)

Expand Down Expand Up @@ -1124,6 +1176,16 @@ def dropEvent(self, event):
else:
event.ignore()

def leaveEvent(self, event):
"""Actions to take when the mouse leaves the widget."""
self.hovered_row = -1
super().leaveEvent(event)

def wheelEvent(self, event):
"""Actions to take on mouse wheel."""
self._update_hovered_row(event)
super().wheelEvent(event)

def showEvent(self, event):
"""Resize columns when the widget is shown."""
# This is probably the best we can do to adjust the columns width to
Expand Down Expand Up @@ -1508,6 +1570,21 @@ def paste(self):
QMessageBox.warning(self, _( "Empty clipboard"),
_("Nothing to be imported from clipboard."))

@lru_cache(maxsize=1)
def selected_rows(self):
"""
Get the rows currently selected.
Notes
-----
The result of this function is cached because it's called in the paint
method of CollectionsDelegate. So, we need it to run as quickly as
possible.
"""
return {
index.row() for index in self.selectionModel().selectedRows()
}


class CollectionsEditorTableView(BaseTableView):
"""CollectionsEditor table view"""
Expand Down
Loading

0 comments on commit 7f8423a

Please sign in to comment.