From 7b2d7d91a6012d67a2891b0725102b2645764128 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Thu, 7 Mar 2024 21:26:17 -0500 Subject: [PATCH 01/12] Plots: Change auto_fit_plotting option to False - That makes the zoom in/out buttons be enabled by default, which is a much better usability experience than having them disabled. - Adjust the plot scale factor according to the scaling of fitted image, so that zoom in/out works as expected. - Fix test_zoom_figure_viewer, which checked that functionality. --- spyder/config/main.py | 4 +-- spyder/plugins/plots/widgets/figurebrowser.py | 26 +++++++++++++++---- .../plots/widgets/tests/test_plots_widgets.py | 13 +++++----- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index f4f03c96901..a0a1625ed31 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -209,7 +209,7 @@ { 'mute_inline_plotting': True, 'show_plot_outline': False, - 'auto_fit_plotting': True + 'auto_fit_plotting': False }), ('editor', { @@ -670,4 +670,4 @@ # or if you want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 3.0.0 to 4.0.0 # 3. You don't need to touch this value if you're just adding a new option -CONF_VERSION = '82.2.0' +CONF_VERSION = '82.3.0' diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 5441be4980b..d5844209f24 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -12,6 +12,7 @@ # Standard library imports import datetime +import math import os.path as osp import sys @@ -427,7 +428,7 @@ def load_figure(self, fig, fmt): """Set a new figure in the figure canvas.""" self.figcanvas.load_figure(fig, fmt) self.sig_figure_loaded.emit() - self.scale_image() + self.scale_image(auto_fit_when_loading=not self.auto_fit_plotting) self.figcanvas.repaint() def eventFilter(self, widget, event): @@ -493,15 +494,15 @@ def zoom_out(self): if self._scalefactor >= self._sfmin: self._scalefactor -= 1 self.scale_image() - self._adjust_scrollbar(1/self._scalestep) + self._adjust_scrollbar(1 / self._scalestep) - def scale_image(self): + def scale_image(self, auto_fit_when_loading=False): """Scale the image size.""" fwidth = self.figcanvas.fwidth fheight = self.figcanvas.fheight # Don't auto fit plotting - if not self.auto_fit_plotting: + if not self.auto_fit_plotting and not auto_fit_when_loading: new_width = int(fwidth * self._scalestep ** self._scalefactor) new_height = int(fheight * self._scalestep ** self._scalefactor) @@ -516,7 +517,9 @@ def scale_image(self): height = (size.height() - style.pixelMetric(QStyle.PM_LayoutTopMargin) - style.pixelMetric(QStyle.PM_LayoutBottomMargin)) + self.figcanvas.setToolTip('') + try: if (fwidth / fheight) > (width / height): new_width = int(width) @@ -528,12 +531,21 @@ def scale_image(self): icon = self.create_icon('broken_image') self.figcanvas._qpix_orig = icon.pixmap(fwidth, fheight) self.figcanvas.setToolTip( - _('The image is broken, please try to generate it again')) + _('The image is broken, please try to generate it again') + ) new_width = fwidth new_height = fheight + auto_fit_when_loading = False if self.figcanvas.size() != QSize(new_width, new_height): self.figcanvas.setFixedSize(new_width, new_height) + + # Adjust the scale factor according to the scaling of the fitted + # image. This is necessary so that zoom in/out increases/decreases + # the image size in factors of of +1/-1 of the one computed below. + if auto_fit_when_loading: + self._scalefactor = self.get_scale_factor() + self.sig_zoom_changed.emit(self.get_scaling()) def get_scaling(self): @@ -545,6 +557,10 @@ def get_scaling(self): else: return 100 + def get_scale_factor(self): + """Get scale factor according to the current scaling.""" + return math.log(self.get_scaling() / 100) / math.log(self._scalestep) + def reset_original_image(self): """Reset the image to its original size.""" self._scalefactor = 0 diff --git a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py index 783e5caeaf4..77d8a9bfb10 100644 --- a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py +++ b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py @@ -18,7 +18,6 @@ # Third party imports import pytest from matplotlib.figure import Figure -from matplotlib.backends.backend_agg import FigureCanvasAgg import numpy as np from qtpy.QtWidgets import QApplication, QStyle from qtpy.QtGui import QPixmap @@ -57,7 +56,6 @@ def create_figure(figname): """Create a matplotlib figure, save it to disk and return its data.""" # Create and save to disk a figure with matplotlib. fig = Figure() - canvas = FigureCanvasAgg(fig) ax = fig.add_axes([0.15, 0.15, 0.7, 0.7]) fig.set_size_inches(6, 4) ax.plot(np.random.rand(10), '.', color='red') @@ -443,20 +441,21 @@ def test_zoom_figure_viewer(figbrowser, tmpdir, fmt): qpix.loadFromData(fig, fmt.upper()) fwidth, fheight = qpix.width(), qpix.height() - assert figbrowser.zoom_disp_value == 100 - assert figcanvas.width() == fwidth - assert figcanvas.height() == fheight + assert figbrowser.zoom_disp_value < 100 + assert figcanvas.width() < fwidth + assert figcanvas.height() < fheight # Zoom in and out the figure in the figure viewer. - scaling_factor = 0 + scaling_factor = figbrowser.figviewer.get_scale_factor() scaling_step = figbrowser.figviewer._scalestep for zoom_step in [1, 1, -1, -1, -1]: if zoom_step == 1: figbrowser.zoom_in() elif zoom_step == -1: figbrowser.zoom_out() + scaling_factor += zoom_step - scale = scaling_step**scaling_factor + scale = scaling_step ** scaling_factor assert (figbrowser.zoom_disp_value == np.round(int(fwidth * scale) / fwidth * 100)) From 87a10142fcf33c81a8a2508f1b3e904444144017 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 30 Mar 2024 12:30:40 -0500 Subject: [PATCH 02/12] Plots: Use a vertical layout in ThumbnailScrollBar That simplifies the layout of that widget and it'll also allow us to change the order of thumbnails in it with drag and drop. --- spyder/plugins/plots/widgets/figurebrowser.py | 16 ++++++++-------- .../plots/widgets/tests/test_plots_widgets.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index d5844209f24..846120d4128 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -653,16 +653,18 @@ def setup_gui(self): def setup_scrollarea(self): """Setup the scrollarea that will contain the FigureThumbnails.""" - self.view = QWidget() + self.view = QWidget(self) - self.scene = QGridLayout(self.view) + self.scene = QVBoxLayout(self.view) + self.scene.setAlignment(Qt.AlignHCenter | Qt.AlignTop) self.scene.setContentsMargins(0, 0, 0, 0) + # The vertical spacing between the thumbnails. # Note that we need to set this value explicitly or else the tests # are failing on macOS. See spyder-ide/spyder#11576. self.scene.setSpacing(5) - self.scrollarea = QScrollArea() + self.scrollarea = QScrollArea(self) self.scrollarea.setWidget(self.view) self.scrollarea.setWidgetResizable(True) self.scrollarea.setFrameStyle(0) @@ -839,13 +841,11 @@ def add_thumbnail(self, fig, fmt): thumbnail.sig_context_menu_requested.connect( lambda point: self.show_context_menu(point, thumbnail)) self._thumbnails.append(thumbnail) + self.scene.addWidget(thumbnail) + self._scroll_to_last_thumbnail = True self._first_thumbnail_shown = True - self.scene.setRowStretch(self.scene.rowCount() - 1, 0) - self.scene.addWidget(thumbnail, self.scene.rowCount() - 1, 0) - self.scene.setRowStretch(self.scene.rowCount(), 100) - # Only select a new thumbnail if the last one was selected select_last = ( len(self._thumbnails) < 2 @@ -963,7 +963,7 @@ def go_next_thumbnail(self): def scroll_to_item(self, index): """Scroll to the selected item of ThumbnailScrollBar.""" - spacing_between_items = self.scene.verticalSpacing() + spacing_between_items = self.scene.spacing() height_view = self.scrollarea.viewport().height() height_item = self.scene.itemAt(index).sizeHint().height() height_view_excluding_item = max(0, height_view - height_item) diff --git a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py index 77d8a9bfb10..e1f0263e044 100644 --- a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py +++ b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py @@ -297,7 +297,7 @@ def test_scroll_to_item(figbrowser, tmpdir, qtbot): scene = figbrowser.thumbnails_sb.scene - spacing = scene.verticalSpacing() + spacing = scene.spacing() height = scene.itemAt(0).sizeHint().height() height_view = figbrowser.thumbnails_sb.scrollarea.viewport().height() @@ -328,7 +328,7 @@ def test_scroll_down_to_newest_plot(figbrowser, tmpdir, qtbot): # value was set to its maximum. height_view = figbrowser.thumbnails_sb.scrollarea.viewport().height() scene = figbrowser.thumbnails_sb.scene - spacing = scene.verticalSpacing() + spacing = scene.spacing() height = scene.itemAt(0).sizeHint().height() expected = (spacing * (nfig - 1)) + (height * nfig) - height_view From 1c0823a801d2393a6a891093d79a3763394cec2c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 30 Mar 2024 14:03:36 -0500 Subject: [PATCH 03/12] Plots: Implement drag and drop of thumbnails in ThumbnailScrollBar --- spyder/plugins/plots/widgets/figurebrowser.py | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 846120d4128..efd073814b5 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -20,8 +20,18 @@ from qtconsole.svg import svg_to_clipboard, svg_to_image from qtpy import PYQT5, PYQT6 from qtpy.compat import getexistingdirectory, getsavefilename -from qtpy.QtCore import QEvent, QPoint, QRect, QSize, Qt, QTimer, Signal, Slot -from qtpy.QtGui import QPainter, QPixmap +from qtpy.QtCore import ( + QEvent, + QMimeData, + QPoint, + QRect, + QSize, + Qt, + QTimer, + Signal, + Slot, +) +from qtpy.QtGui import QDrag, QPainter, QPixmap from qtpy.QtWidgets import (QApplication, QFrame, QGridLayout, QLayout, QScrollArea, QScrollBar, QSplitter, QStyle, QVBoxLayout, QWidget, QStackedLayout) @@ -644,6 +654,9 @@ def __init__(self, figure_viewer, parent=None, background_color=None): self.scrollarea.verticalScrollBar().rangeChanged.connect( self._scroll_to_newest_item) + # To reorganize thumbnails with drag and drop + self.setAcceptDrops(True) + def setup_gui(self): """Setup the main layout of the widget.""" layout = QVBoxLayout(self) @@ -707,6 +720,48 @@ def eventFilter(self, widget, event): self._update_thumbnail_size() return super().eventFilter(widget, event) + def dragEnterEvent(self, event): + """Enable drag events on this widget.""" + event.accept() + + def dropEvent(self, event): + """ + Handle drop events. + + Solution adapted from + https://www.pythonguis.com/faq/pyqt-drag-drop-widgets + """ + # Event variables + pos = event.pos() + dropped_thumbnail = event.source() + + # Avoid accepting drops from other widgets + if not isinstance(dropped_thumbnail, FigureThumbnail): + return + + # Main variables + scrollbar_pos = self.scrollarea.verticalScrollBar().value() + n_thumbnails = self.scene.count() + last_thumbnail = self.scene.itemAt(n_thumbnails - 1).widget() + + # Move thumbnail + if (pos.y() + scrollbar_pos) > last_thumbnail.y(): + # This allows to move a thumbnail to the last position + self.scene.insertWidget(n_thumbnails - 1, dropped_thumbnail) + else: + # This works for any other position, including the first one + for i in range(n_thumbnails): + w = self.scene.itemAt(i).widget() + + if ( + (pos.y() + scrollbar_pos) + < (w.y() + w.size().height() // 5) + ): + self.scene.insertWidget(i - 1, dropped_thumbnail) + break + + event.accept() + # ---- Save Figure def save_all_figures_as(self): """Save all the figures to a file.""" @@ -1109,6 +1164,27 @@ def eventFilter(self, widget, event): return super().eventFilter(widget, event) + def mouseMoveEvent(self, event): + """ + Enable drags to reorganize thumbnails with the mouse in the scrollbar. + + Solution taken from: + https://www.pythonguis.com/faq/pyqt-drag-drop-widgets/ + """ + if event.buttons() == Qt.LeftButton: + # Create drag + drag = QDrag(self) + mime = QMimeData() + drag.setMimeData(mime) + + # Show pixmap of the thumbnail while it's being moved. + pixmap = QPixmap(self.size()) + self.render(pixmap) + drag.setPixmap(pixmap) + + # Execute drag's event loop + drag.exec_(Qt.MoveAction) + class FigureCanvas(QFrame): """ From 97f70d546ae4ac750a8e8d8f324d1d90636712cf Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 30 Mar 2024 15:09:24 -0500 Subject: [PATCH 04/12] Plots: Improve margins of thumbnails area --- spyder/plugins/plots/widgets/figurebrowser.py | 11 ++++++++--- .../plugins/plots/widgets/tests/test_plots_widgets.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index efd073814b5..08424529357 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -41,6 +41,7 @@ from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.utils.misc import getcwd_or_home from spyder.utils.palette import SpyderPalette +from spyder.utils.stylesheet import AppStyle from spyder.widgets.helperwidgets import PaneEmptyWidget @@ -670,18 +671,22 @@ def setup_scrollarea(self): self.scene = QVBoxLayout(self.view) self.scene.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - self.scene.setContentsMargins(0, 0, 0, 0) + self.scene.setContentsMargins( + 0, AppStyle.MarginSize, 0, AppStyle.MarginSize + ) # The vertical spacing between the thumbnails. # Note that we need to set this value explicitly or else the tests # are failing on macOS. See spyder-ide/spyder#11576. - self.scene.setSpacing(5) + self.scene.setSpacing(2 * AppStyle.MarginSize) self.scrollarea = QScrollArea(self) self.scrollarea.setWidget(self.view) self.scrollarea.setWidgetResizable(True) self.scrollarea.setFrameStyle(0) - self.scrollarea.setViewportMargins(2, 2, 2, 2) + self.scrollarea.setViewportMargins( + AppStyle.MarginSize, 0, AppStyle.MarginSize, 0 + ) self.scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scrollarea.setMinimumWidth(self._min_scrollbar_width) diff --git a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py index e1f0263e044..0c4ba490feb 100644 --- a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py +++ b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py @@ -331,7 +331,7 @@ def test_scroll_down_to_newest_plot(figbrowser, tmpdir, qtbot): spacing = scene.spacing() height = scene.itemAt(0).sizeHint().height() - expected = (spacing * (nfig - 1)) + (height * nfig) - height_view + expected = (spacing * (nfig - 1)) + (height * nfig) - height_view + 6 vsb = figbrowser.thumbnails_sb.scrollarea.verticalScrollBar() assert vsb.value() == expected From 654fc614ec3dd7584e56a49db9b1a8128e4cde4e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 31 Mar 2024 10:55:49 -0500 Subject: [PATCH 05/12] Plots: Improve organization of buttons in its main toolbar Also, increase default width of thumbnail bar to go with this change. --- spyder/plugins/plots/widgets/figurebrowser.py | 2 +- spyder/plugins/plots/widgets/main_widget.py | 31 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 08424529357..16ebfbabe6a 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -597,7 +597,7 @@ class ThumbnailScrollBar(QFrame): created when a figure is sent to the IPython console by the kernel and that controls what is displayed in the FigureViewer. """ - _min_scrollbar_width = 100 + _min_scrollbar_width = 130 # Signals sig_redirect_stdio_requested = Signal(bool) diff --git a/spyder/plugins/plots/widgets/main_widget.py b/spyder/plugins/plots/widgets/main_widget.py index b73d2a263ee..36b61e52aa8 100644 --- a/spyder/plugins/plots/widgets/main_widget.py +++ b/spyder/plugins/plots/widgets/main_widget.py @@ -48,12 +48,12 @@ class PlotsWidgetActions: class PlotsWidgetMainToolbarSections: Edit = 'edit_section' - Move = 'move_section' - Zoom = 'zoom_section' + ZoomAndMove = 'zoom_section' class PlotsWidgetToolbarItems: ZoomSpinBox = 'zoom_spin' + ToolbarStretcher = 'toolbar_stretcher' # --- Widgets @@ -199,15 +199,36 @@ def setup(self): # Main toolbar main_toolbar = self.get_main_toolbar() - for item in [save_action, save_all_action, copy_action, remove_action, - remove_all_action, previous_action, next_action, - zoom_in_action, zoom_out_action, self.zoom_disp]: + for item in [ + save_action, + save_all_action, + copy_action, + remove_action, + remove_all_action, + ]: self.add_item_to_toolbar( item, toolbar=main_toolbar, section=PlotsWidgetMainToolbarSections.Edit, ) + stretcher = self.create_stretcher( + PlotsWidgetToolbarItems.ToolbarStretcher + ) + for item in [ + self.zoom_disp, + zoom_out_action, + zoom_in_action, + stretcher, + previous_action, + next_action, + ]: + self.add_item_to_toolbar( + item, + toolbar=main_toolbar, + section=PlotsWidgetMainToolbarSections.ZoomAndMove, + ) + # Context menu context_menu = self.create_menu(PluginMainWidgetMenus.Context) for item in [save_action, copy_action, remove_action]: From 1c4af7dd83567eeb61cac16a95efa998af6b9470 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 31 Mar 2024 12:11:33 -0500 Subject: [PATCH 06/12] Plots: Make doing double click on a plot show it at full size --- spyder/plugins/plots/widgets/figurebrowser.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 16ebfbabe6a..f1c4af30cc8 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -465,14 +465,14 @@ def eventFilter(self, widget, event): # Set ClosedHandCursor: elif event.type() == QEvent.MouseButtonPress: if event.button() == Qt.LeftButton: - QApplication.setOverrideCursor(Qt.ClosedHandCursor) + self.setCursor(Qt.ClosedHandCursor) self._ispanning = True self.xclick = event.globalX() self.yclick = event.globalY() # Reset Cursor: elif event.type() == QEvent.MouseButtonRelease: - QApplication.restoreOverrideCursor() + self.setCursor(Qt.ArrowCursor) self._ispanning = False # Move ScrollBar: @@ -490,6 +490,16 @@ def eventFilter(self, widget, event): scrollBarV = self.verticalScrollBar() scrollBarV.setValue(scrollBarV.value() + dy) + # Show in full size or restore the previous one + elif ( + event.type() == QEvent.MouseButtonDblClick + and self._scalefactor != 0 + ): + # This is necessary so that when calling zoom_in the scale + # factor becomes zero + self._scalefactor = -1 + self.zoom_in() + return QWidget.eventFilter(self, widget, event) # ---- Figure Scaling Handlers From 05ff6a6aaf4b02a0a047e6d71d33d841ac36d220 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 11 May 2024 20:20:27 -0500 Subject: [PATCH 07/12] Plots: Provide a better UX for the auto-fit plots action - Move it to the plugin toolbar and check it by default after every new plot is generated so the plot always fit in the pane. - Unchecking the action makes the plot to zoom in at its full size. - Clicking the zoom in/out buttons unchecks the action, which means the plot doesn't fit into the pane anymore. - Save the auto-fit state in the current thumbnail. This will be useful to restore the zoom level of each plot when loaded. - Remove the auto_fit_plotting option because it's no longer needed. --- spyder/config/main.py | 3 +- spyder/plugins/plots/widgets/figurebrowser.py | 64 ++++++++++------ spyder/plugins/plots/widgets/main_widget.py | 75 ++++++++++++------- .../plots/widgets/tests/test_plots_widgets.py | 9 +-- spyder/utils/icon_manager.py | 1 + 5 files changed, 93 insertions(+), 59 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index a0a1625ed31..8de470a7b91 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -209,7 +209,6 @@ { 'mute_inline_plotting': True, 'show_plot_outline': False, - 'auto_fit_plotting': False }), ('editor', { @@ -670,4 +669,4 @@ # or if you want to *rename* options, then you need to do a MAJOR update in # version, e.g. from 3.0.0 to 4.0.0 # 3. You don't need to touch this value if you're just adding a new option -CONF_VERSION = '82.3.0' +CONF_VERSION = '83.0.0' diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index f1c4af30cc8..af5f57ef5c6 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -222,13 +222,10 @@ def setup(self, options): """Setup the figure browser with provided options.""" self.splitter.setContentsMargins(0, 0, 0, 0) for option, value in options.items(): - if option == 'auto_fit_plotting': - self.change_auto_fit_plotting(value) - elif option == 'mute_inline_plotting': + if option == 'mute_inline_plotting': self.mute_inline_plotting = value if self.shellwidget: self.shellwidget.set_mute_inline_plotting(value) - elif option == 'show_plot_outline': self.show_fig_outline_in_viewer(value) elif option == 'save_dir': @@ -264,10 +261,6 @@ def show_fig_outline_in_viewer(self, state): self.figviewer.figcanvas.setStyleSheet( "FigureCanvas{border: 0px;}") - def change_auto_fit_plotting(self, state): - """Change the auto_fit_plotting option and scale images.""" - self.figviewer.auto_fit_plotting = state - def set_shellwidget(self, shellwidget): """Bind the shellwidget instance to the figure browser""" self.shellwidget = shellwidget @@ -387,6 +380,7 @@ def __init__(self, parent=None, background_color=None): self.setFrameStyle(0) self.background_color = background_color + self.current_thumbnail = None self._scalefactor = 0 self._scalestep = 1.2 self._sfmax = 10 @@ -411,13 +405,17 @@ def auto_fit_plotting(self, value): Set whether to automatically fit the plot to the scroll area size. """ self._auto_fit_plotting = value + + if self.current_thumbnail is not None: + self.current_thumbnail.auto_fit = value + if value: + self.scale_image() self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) else: self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.scale_image() def setup_figcanvas(self): """Setup the FigureCanvas.""" @@ -435,11 +433,16 @@ def show_context_menu(self, qpoint): point = self.figcanvas.mapToGlobal(qpoint) self.sig_context_menu_requested.emit(point) + def set_current_thumbnail(self, thumbnail): + """Set the current thumbnail displayed in the viewer.""" + self.current_thumbnail = thumbnail + def load_figure(self, fig, fmt): """Set a new figure in the figure canvas.""" + self.auto_fit_plotting = self.current_thumbnail.auto_fit self.figcanvas.load_figure(fig, fmt) self.sig_figure_loaded.emit() - self.scale_image(auto_fit_when_loading=not self.auto_fit_plotting) + self.scale_image() self.figcanvas.repaint() def eventFilter(self, widget, event): @@ -495,16 +498,21 @@ def eventFilter(self, widget, event): event.type() == QEvent.MouseButtonDblClick and self._scalefactor != 0 ): - # This is necessary so that when calling zoom_in the scale - # factor becomes zero - self._scalefactor = -1 - self.zoom_in() + self.auto_fit_plotting = False + self.zoom_in(to_full_size=True) + + # Necessary to correctly set the state of the fit_action button + self.sig_figure_loaded.emit() return QWidget.eventFilter(self, widget, event) # ---- Figure Scaling Handlers - def zoom_in(self): + def zoom_in(self, to_full_size=False): """Scale the image up by one scale step.""" + # This is necessary so that the scale factor becomes zero below + if to_full_size: + self._scalefactor = -1 + if self._scalefactor <= self._sfmax: self._scalefactor += 1 self.scale_image() @@ -517,13 +525,13 @@ def zoom_out(self): self.scale_image() self._adjust_scrollbar(1 / self._scalestep) - def scale_image(self, auto_fit_when_loading=False): + def scale_image(self): """Scale the image size.""" fwidth = self.figcanvas.fwidth fheight = self.figcanvas.fheight # Don't auto fit plotting - if not self.auto_fit_plotting and not auto_fit_when_loading: + if not self.auto_fit_plotting: new_width = int(fwidth * self._scalestep ** self._scalefactor) new_height = int(fheight * self._scalestep ** self._scalefactor) @@ -556,7 +564,7 @@ def scale_image(self, auto_fit_when_loading=False): ) new_width = fwidth new_height = fheight - auto_fit_when_loading = False + self.auto_fit_plotting = False if self.figcanvas.size() != QSize(new_width, new_height): self.figcanvas.setFixedSize(new_width, new_height) @@ -564,7 +572,7 @@ def scale_image(self, auto_fit_when_loading=False): # Adjust the scale factor according to the scaling of the fitted # image. This is necessary so that zoom in/out increases/decreases # the image size in factors of of +1/-1 of the one computed below. - if auto_fit_when_loading: + if self.auto_fit_plotting: self._scalefactor = self.get_scale_factor() self.sig_zoom_changed.emit(self.get_scaling()) @@ -903,7 +911,8 @@ def add_thumbnail(self, fig, fmt): stick_at_end = True thumbnail = FigureThumbnail( - parent=self, background_color=self.background_color) + parent=self, background_color=self.background_color + ) thumbnail.canvas.load_figure(fig, fmt) thumbnail.sig_canvas_clicked.connect(self.set_current_thumbnail) thumbnail.sig_remove_figure_requested.connect(self.remove_thumbnail) @@ -949,6 +958,7 @@ def remove_all_thumbnails(self): self._thumbnails = [] self.current_thumbnail = None + self.figure_viewer.auto_fit_plotting = False self.figure_viewer.figcanvas.clear_canvas() def remove_thumbnail(self, thumbnail): @@ -1011,8 +1021,10 @@ def set_current_thumbnail(self, thumbnail): if self.current_thumbnail is not None: self.current_thumbnail.highlight_canvas(False) self.current_thumbnail = thumbnail + self.figure_viewer.set_current_thumbnail(thumbnail) self.figure_viewer.load_figure( - thumbnail.canvas.fig, thumbnail.canvas.fmt) + thumbnail.canvas.fig, thumbnail.canvas.fmt + ) self.current_thumbnail.highlight_canvas(True) def go_previous_thumbnail(self): @@ -1122,10 +1134,14 @@ class FigureThumbnail(QWidget): The QPoint in global coordinates where the menu was requested. """ - def __init__(self, parent=None, background_color=None): + def __init__(self, parent=None, background_color=None, auto_fit=True): super().__init__(parent) - self.canvas = FigureCanvas(parent=self, - background_color=background_color) + + self.auto_fit = auto_fit + self.canvas = FigureCanvas( + parent=self, + background_color=background_color + ) self.canvas.sig_context_menu_requested.connect( self.sig_context_menu_requested) self.canvas.installEventFilter(self) diff --git a/spyder/plugins/plots/widgets/main_widget.py b/spyder/plugins/plots/widgets/main_widget.py index 36b61e52aa8..d5a7655785b 100644 --- a/spyder/plugins/plots/widgets/main_widget.py +++ b/spyder/plugins/plots/widgets/main_widget.py @@ -11,6 +11,7 @@ # Third party imports from qtpy.QtCore import Qt, Signal from qtpy.QtWidgets import QSpinBox +from superqt.utils import signals_blocked # Local imports from spyder.api.config.decorators import on_conf_change @@ -111,11 +112,11 @@ def setup(self): ) self.fit_action = self.create_action( name=PlotsWidgetActions.ToggleAutoFitPlotting, - text=_("Fit plots to window"), - tip=_("Automatically fit plots to Plot pane size."), - toggled=True, - initial=self.get_conf('auto_fit_plotting'), - option='auto_fit_plotting' + text=_("Fit plots to pane"), + tip=_("Fit plot to the pane size"), + icon=self.create_icon("plot.fit_to_pane"), + toggled=self.fit_to_pane, + initial=False, ) # Toolbar actions @@ -195,7 +196,6 @@ def setup(self): options_menu = self.get_options_menu() self.add_item_to_menu(self.mute_action, menu=options_menu) self.add_item_to_menu(self.outline_action, menu=options_menu) - self.add_item_to_menu(self.fit_action, menu=options_menu) # Main toolbar main_toolbar = self.get_main_toolbar() @@ -219,6 +219,7 @@ def setup(self): self.zoom_disp, zoom_out_action, zoom_in_action, + self.fit_action, stretcher, previous_action, next_action, @@ -238,22 +239,26 @@ def update_actions(self): value = False widget = self.current_widget() figviewer = None + if widget and not self.is_current_widget_empty(): figviewer = widget.figviewer - thumbnails_sb = widget.thumbnails_sb value = figviewer.figcanvas.fig is not None widget.set_pane_empty(not value) + with signals_blocked(self.fit_action): + self.fit_action.setChecked(figviewer.auto_fit_plotting) + for __, action in self.get_actions().items(): try: - if action and action not in [self.mute_action, - self.outline_action, - self.fit_action, - self.undock_action, - self.close_action, - self.dock_action, - self.toggle_view_action, - self.lock_unlock_action]: + if action and action not in [ + self.mute_action, + self.outline_action, + self.undock_action, + self.close_action, + self.dock_action, + self.toggle_view_action, + self.lock_unlock_action, + ]: action.setEnabled(value) # IMPORTANT: Since we are defining the main actions in here @@ -262,6 +267,7 @@ def update_actions(self): # for shortcuts to work if figviewer: figviewer_actions = figviewer.actions() + thumbnails_sb = widget.thumbnails_sb thumbnails_sb_actions = thumbnails_sb.actions() if action not in figviewer_actions: @@ -274,15 +280,9 @@ def update_actions(self): self.zoom_disp.setEnabled(value) - # Disable zoom buttons if autofit - if value: - value = not self.get_conf('auto_fit_plotting') - self.get_action(PlotsWidgetActions.ZoomIn).setEnabled(value) - self.get_action(PlotsWidgetActions.ZoomOut).setEnabled(value) - self.zoom_disp.setEnabled(value) - - @on_conf_change(option=['auto_fit_plotting', 'mute_inline_plotting', - 'show_plot_outline', 'save_dir']) + @on_conf_change( + option=["mute_inline_plotting", "show_plot_outline", "save_dir"] + ) def on_section_conf_change(self, option, value): for index in range(self.count()): widget = self._stack.widget(index) @@ -324,10 +324,11 @@ def close_widget(self, fig_browser): fig_browser.setParent(None) def switch_widget(self, fig_browser, old_fig_browser): - option_keys = [('auto_fit_plotting', True), - ('mute_inline_plotting', True), - ('show_plot_outline', True), - ('save_dir', getcwd_or_home())] + option_keys = [ + ("mute_inline_plotting", True), + ("show_plot_outline", True), + ("save_dir", getcwd_or_home()), + ] conf_values = {k: self.get_conf(k, d) for k, d in option_keys} fig_browser.setup(conf_values) @@ -447,10 +448,28 @@ def zoom_in(self): """Perform a zoom in on the main figure.""" widget = self.current_widget() if widget: + with signals_blocked(self.fit_action): + self.fit_action.setChecked(False) + widget.figviewer.auto_fit_plotting = False widget.zoom_in() def zoom_out(self): """Perform a zoom out on the main figure.""" widget = self.current_widget() if widget: + with signals_blocked(self.fit_action): + self.fit_action.setChecked(False) + widget.figviewer.auto_fit_plotting = False widget.zoom_out() + + def fit_to_pane(self, state): + """Fit current plot to the pane size.""" + widget = self.current_widget() + if widget: + figviewer = widget.figviewer + if state: + figviewer.auto_fit_plotting = True + figviewer.scale_image() + else: + figviewer.auto_fit_plotting = False + figviewer.zoom_in(to_full_size=True) diff --git a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py index 0c4ba490feb..301abfe31c5 100644 --- a/spyder/plugins/plots/widgets/tests/test_plots_widgets.py +++ b/spyder/plugins/plots/widgets/tests/test_plots_widgets.py @@ -38,9 +38,8 @@ def figbrowser(qtbot): figbrowser = FigureBrowser() figbrowser.set_shellwidget(Mock()) options = { - 'mute_inline_plotting': True, - 'show_plot_outline': False, - 'auto_fit_plotting': False, + "mute_inline_plotting": True, + "show_plot_outline": False, } figbrowser.setup(options) qtbot.addWidget(figbrowser) @@ -434,7 +433,7 @@ def test_zoom_figure_viewer(figbrowser, tmpdir, fmt): figcanvas = figbrowser.figviewer.figcanvas # Set `Fit plots to windows` to False before the test. - figbrowser.change_auto_fit_plotting(False) + figbrowser.figviewer.auto_fit_plotting = False # Calculate original figure size in pixels. qpix = QPixmap() @@ -479,7 +478,7 @@ def test_autofit_figure_viewer(figbrowser, tmpdir, fmt): # Test when `Fit plots to window` is set to True. # Otherwise, test should fall into `test_zoom_figure_viewer` - figbrowser.change_auto_fit_plotting(True) + figbrowser.figviewer.auto_fit_plotting = True size = figviewer.size() style = figviewer.style() diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py index 76d852867eb..25d630797c1 100644 --- a/spyder/utils/icon_manager.py +++ b/spyder/utils/icon_manager.py @@ -205,6 +205,7 @@ def __init__(self): 'home': [('mdi.home',), {'color': self.MAIN_FG_COLOR}], 'show': [('mdi.eye',), {'color': self.MAIN_FG_COLOR}], 'plot': [('mdi.chart-bar',), {'color': self.MAIN_FG_COLOR}], + 'plot.fit_to_pane': [('mdi6.fit-to-screen',), {'color': self.MAIN_FG_COLOR}], 'hist': [('mdi.chart-histogram',), {'color': self.MAIN_FG_COLOR}], 'imshow': [('mdi.image',), {'color': self.MAIN_FG_COLOR}], 'insert': [('mdi.login',), {'color': self.MAIN_FG_COLOR}], From c3e7e546a4ea222cfe1388db608df8947f3cde0e Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 12 May 2024 20:57:43 -0500 Subject: [PATCH 08/12] Plots: Display each plot with the scale factor set by users for it --- spyder/plugins/plots/widgets/figurebrowser.py | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index af5f57ef5c6..f624940b744 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -381,7 +381,8 @@ def __init__(self, parent=None, background_color=None): self.background_color = background_color self.current_thumbnail = None - self._scalefactor = 0 + self.scalefactor = 0 + self._scalestep = 1.2 self._sfmax = 10 self._sfmin = -10 @@ -417,6 +418,18 @@ def auto_fit_plotting(self, value): self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + @property + def scalefactor(self): + """Return the current scale factor.""" + return self._scalefactor + + @scalefactor.setter + def scalefactor(self, value): + """Set the scale factor value.""" + self._scalefactor = value + if self.current_thumbnail is not None: + self.current_thumbnail.scalefactor = value + def setup_figcanvas(self): """Setup the FigureCanvas.""" self.figcanvas = FigureCanvas(parent=self, @@ -440,11 +453,21 @@ def set_current_thumbnail(self, thumbnail): def load_figure(self, fig, fmt): """Set a new figure in the figure canvas.""" self.auto_fit_plotting = self.current_thumbnail.auto_fit + + # Let scale_image compute the scale factor for the thumbnail if it + # hasn't one yet. + if self.current_thumbnail.scalefactor is not None: + self.scalefactor = self.current_thumbnail.scalefactor + self.figcanvas.load_figure(fig, fmt) self.sig_figure_loaded.emit() self.scale_image() self.figcanvas.repaint() + # Save the computed scale factor by scale_image in the thumbnail + if self.current_thumbnail.scalefactor is None: + self.current_thumbnail.scalefactor = self.scalefactor + def eventFilter(self, widget, event): """A filter to control the zooming and panning of the figure canvas.""" @@ -496,7 +519,7 @@ def eventFilter(self, widget, event): # Show in full size or restore the previous one elif ( event.type() == QEvent.MouseButtonDblClick - and self._scalefactor != 0 + and self.scalefactor != 0 ): self.auto_fit_plotting = False self.zoom_in(to_full_size=True) @@ -511,17 +534,17 @@ def zoom_in(self, to_full_size=False): """Scale the image up by one scale step.""" # This is necessary so that the scale factor becomes zero below if to_full_size: - self._scalefactor = -1 + self.scalefactor = -1 - if self._scalefactor <= self._sfmax: - self._scalefactor += 1 + if self.scalefactor <= self._sfmax: + self.scalefactor += 1 self.scale_image() self._adjust_scrollbar(self._scalestep) def zoom_out(self): """Scale the image down by one scale step.""" - if self._scalefactor >= self._sfmin: - self._scalefactor -= 1 + if self.scalefactor >= self._sfmin: + self.scalefactor -= 1 self.scale_image() self._adjust_scrollbar(1 / self._scalestep) @@ -532,8 +555,8 @@ def scale_image(self): # Don't auto fit plotting if not self.auto_fit_plotting: - new_width = int(fwidth * self._scalestep ** self._scalefactor) - new_height = int(fheight * self._scalestep ** self._scalefactor) + new_width = int(fwidth * self._scalestep ** self.scalefactor) + new_height = int(fheight * self._scalestep ** self.scalefactor) # Auto fit plotting # Scale the image to fit the figviewer size while respecting the ratio. @@ -573,7 +596,7 @@ def scale_image(self): # image. This is necessary so that zoom in/out increases/decreases # the image size in factors of of +1/-1 of the one computed below. if self.auto_fit_plotting: - self._scalefactor = self.get_scale_factor() + self.scalefactor = self.get_scale_factor() self.sig_zoom_changed.emit(self.get_scaling()) @@ -592,7 +615,7 @@ def get_scale_factor(self): def reset_original_image(self): """Reset the image to its original size.""" - self._scalefactor = 0 + self.scalefactor = 0 self.scale_image() def _adjust_scrollbar(self, f): @@ -1138,6 +1161,8 @@ def __init__(self, parent=None, background_color=None, auto_fit=True): super().__init__(parent) self.auto_fit = auto_fit + self.scalefactor = None + self.canvas = FigureCanvas( parent=self, background_color=background_color From 7777e45f71f50494451ae35169466fba5ca20da8 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 12 May 2024 21:19:50 -0500 Subject: [PATCH 09/12] Plots: Enable panning with no auto fit and the scrollbars are visible --- spyder/plugins/plots/widgets/figurebrowser.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index f624940b744..788fb6644bc 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -490,7 +490,14 @@ def eventFilter(self, widget, event): # ---- Panning # Set ClosedHandCursor: elif event.type() == QEvent.MouseButtonPress: - if event.button() == Qt.LeftButton: + if ( + event.button() == Qt.LeftButton + and not self.auto_fit_plotting + and ( + self.verticalScrollBar().isVisible() + or self.horizontalScrollBar().isVisible() + ) + ): self.setCursor(Qt.ClosedHandCursor) self._ispanning = True self.xclick = event.globalX() From 5a95c0721541bb32f37e342b260123e0a1acf755 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 13 May 2024 10:51:34 -0500 Subject: [PATCH 10/12] Plots: Save scrollbars positions and restore them after loading a plot --- spyder/plugins/plots/widgets/figurebrowser.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index 788fb6644bc..d9d64da5b42 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -393,6 +393,14 @@ def __init__(self, parent=None, background_color=None): # An internal flag that tracks when the figure is being panned. self._ispanning = False + # To save scrollbar values in the current thumbnail + self.verticalScrollBar().valueChanged.connect( + self._set_vscrollbar_value + ) + self.horizontalScrollBar().valueChanged.connect( + self._set_hscrollbar_value + ) + @property def auto_fit_plotting(self): """ @@ -468,6 +476,20 @@ def load_figure(self, fig, fmt): if self.current_thumbnail.scalefactor is None: self.current_thumbnail.scalefactor = self.scalefactor + # Restore scrollbar values + QTimer.singleShot( + 20, + lambda: self.verticalScrollBar().setValue( + self.current_thumbnail.vscrollbar_value + ), + ) + QTimer.singleShot( + 20, + lambda: self.horizontalScrollBar().setValue( + self.current_thumbnail.hscrollbar_value + ), + ) + def eventFilter(self, widget, event): """A filter to control the zooming and panning of the figure canvas.""" @@ -638,6 +660,16 @@ def _adjust_scrollbar(self, f): vb = self.verticalScrollBar() vb.setValue(int(f * vb.value() + ((f - 1) * vb.pageStep()/2))) + def _set_vscrollbar_value(self, value): + """Save vertical scrollbar value in current thumbnail.""" + if self.current_thumbnail is not None: + self.current_thumbnail.vscrollbar_value = value + + def _set_hscrollbar_value(self, value): + """Save horizontal scrollbar value in current thumbnail.""" + if self.current_thumbnail is not None: + self.current_thumbnail.hscrollbar_value = value + class ThumbnailScrollBar(QFrame): """ @@ -1169,6 +1201,8 @@ def __init__(self, parent=None, background_color=None, auto_fit=True): self.auto_fit = auto_fit self.scalefactor = None + self.vscrollbar_value = 0 + self.hscrollbar_value = 0 self.canvas = FigureCanvas( parent=self, From 0f68022df28db47b92af1f7db8bde86e9a25b3be Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 13 May 2024 21:26:30 -0500 Subject: [PATCH 11/12] Plots: Add shortcut for the auto-fit action - Also, change shortcuts for save/close all plots to avoid issues in Eastern European languages. - Add clarifying comment the usage of timers to set the scrollbar values after a figure is loaded. --- spyder/config/main.py | 5 +++-- spyder/plugins/plots/widgets/figurebrowser.py | 6 ++++-- spyder/plugins/plots/widgets/main_widget.py | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/spyder/config/main.py b/spyder/config/main.py index 8de470a7b91..0a5b0ebdb5d 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -544,11 +544,12 @@ 'plots/previous figure': 'Ctrl+PgUp', 'plots/next figure': 'Ctrl+PgDown', 'plots/save': 'Ctrl+S', - 'plots/save all': 'Ctrl+Alt+S', + 'plots/save all': 'Alt+Shift+S', 'plots/close': 'Ctrl+W', - 'plots/close all': 'Ctrl+Shift+W', + 'plots/close all': 'Alt+Shift+W', 'plots/zoom in': "Ctrl++", 'plots/zoom out': "Ctrl+-", + 'plots/auto fit': "Ctrl+0", # -- Files -- 'explorer/copy file': 'Ctrl+C', 'explorer/paste file': 'Ctrl+V', diff --git a/spyder/plugins/plots/widgets/figurebrowser.py b/spyder/plugins/plots/widgets/figurebrowser.py index d9d64da5b42..d03eb92a3ed 100644 --- a/spyder/plugins/plots/widgets/figurebrowser.py +++ b/spyder/plugins/plots/widgets/figurebrowser.py @@ -476,7 +476,9 @@ def load_figure(self, fig, fmt): if self.current_thumbnail.scalefactor is None: self.current_thumbnail.scalefactor = self.scalefactor - # Restore scrollbar values + # Restore scrollbar values. + # We need to use timers for this because trying to set those values + # immediately after the figure is loaded doesn't work. QTimer.singleShot( 20, lambda: self.verticalScrollBar().setValue( @@ -545,7 +547,7 @@ def eventFilter(self, widget, event): scrollBarV = self.verticalScrollBar() scrollBarV.setValue(scrollBarV.value() + dy) - # Show in full size or restore the previous one + # Show in full size elif ( event.type() == QEvent.MouseButtonDblClick and self.scalefactor != 0 diff --git a/spyder/plugins/plots/widgets/main_widget.py b/spyder/plugins/plots/widgets/main_widget.py index d5a7655785b..76088661af0 100644 --- a/spyder/plugins/plots/widgets/main_widget.py +++ b/spyder/plugins/plots/widgets/main_widget.py @@ -44,7 +44,7 @@ class PlotsWidgetActions: # Toggles ToggleMuteInlinePlotting = 'toggle_mute_inline_plotting_action' ToggleShowPlotOutline = 'toggle_show_plot_outline_action' - ToggleAutoFitPlotting = 'toggle_auto_fit_plotting_action' + ToggleAutoFitPlotting = 'auto fit' class PlotsWidgetMainToolbarSections: @@ -117,6 +117,7 @@ def setup(self): icon=self.create_icon("plot.fit_to_pane"), toggled=self.fit_to_pane, initial=False, + register_shortcut=True, ) # Toolbar actions From d3515996f0f38e1581db6548a0f5f024b89b527b Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 15 May 2024 12:07:08 -0500 Subject: [PATCH 12/12] Testing: Add tests to check the new UX of the Plots plugin --- spyder/plugins/plots/tests/__init__.py | 7 ++ spyder/plugins/plots/tests/test_plugin.py | 142 ++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 spyder/plugins/plots/tests/__init__.py create mode 100644 spyder/plugins/plots/tests/test_plugin.py diff --git a/spyder/plugins/plots/tests/__init__.py b/spyder/plugins/plots/tests/__init__.py new file mode 100644 index 00000000000..1079d53b902 --- /dev/null +++ b/spyder/plugins/plots/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +"""Plots plugin tests.""" diff --git a/spyder/plugins/plots/tests/test_plugin.py b/spyder/plugins/plots/tests/test_plugin.py new file mode 100644 index 00000000000..532593f94d5 --- /dev/null +++ b/spyder/plugins/plots/tests/test_plugin.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Tests for the Plots plugin. +""" + +from unittest.mock import Mock + +import pytest + +from spyder.config.manager import CONF +from spyder.plugins.plots.plugin import Plots +from spyder.plugins.plots.widgets.main_widget import PlotsWidgetActions +from spyder.plugins.plots.widgets.tests.test_plots_widgets import ( + add_figures_to_browser, +) +from spyder.utils.stylesheet import APP_STYLESHEET + + +@pytest.fixture +def plots_plugin(qapp, qtbot): + plots = Plots(None, configuration=CONF) + plots.get_widget().setMinimumSize(700, 500) + plots.get_widget().add_shellwidget(Mock()) + qtbot.addWidget(plots.get_widget()) + qapp.setStyleSheet(str(APP_STYLESHEET)) + plots.get_widget().show() + return plots + + +def test_fit_action(plots_plugin, tmpdir, qtbot): + """Test that the fit action works as expected.""" + main_widget = plots_plugin.get_widget() + + # Action should not be checked when no plots are available + assert not main_widget.fit_action.isChecked() + + # Add some plots + figbrowser = main_widget.current_widget() + figviewer = figbrowser.figviewer + add_figures_to_browser(figbrowser, 2, tmpdir) + + # Action should be checked now and zoom factor less than 100% + assert main_widget.fit_action.isChecked() + assert main_widget.zoom_disp.value() < 100 + + # Plot should be zoomed in at full size when unchecking action + main_widget.fit_action.setChecked(False) + assert main_widget.zoom_disp.value() == 100 + qtbot.wait(200) + assert figviewer.horizontalScrollBar().isVisible() + + # Action should be checked for next plot + main_widget.next_plot() + assert main_widget.fit_action.isChecked() + + # Action should be unchecked when going back to the previous plot + main_widget.previous_plot() + assert not main_widget.fit_action.isChecked() + + # Plot should be fitted when checking the action again + main_widget.fit_action.setChecked(True) + assert main_widget.zoom_disp.value() < 100 + qtbot.wait(200) + assert not figviewer.horizontalScrollBar().isVisible() + + +def test_zoom_actions(plots_plugin, qtbot, tmpdir): + """Test that the behavior of the zoom actions work as expected.""" + main_widget = plots_plugin.get_widget() + zoom_in_action = main_widget.get_action(PlotsWidgetActions.ZoomIn) + zoom_out_action = main_widget.get_action(PlotsWidgetActions.ZoomOut) + + # Zoom in/out actions should be disabled when no plots are available + assert not zoom_in_action.isEnabled() + assert not zoom_out_action.isEnabled() + + # Add some plots + figbrowser = main_widget.current_widget() + figviewer = figbrowser.figviewer + add_figures_to_browser(figbrowser, 3, tmpdir) + + # Zoom in/out actions should be enabled now + assert zoom_in_action.isEnabled() + assert zoom_out_action.isEnabled() + + # Zoom in first plot twice + for __ in range(2): + main_widget.zoom_in() + qtbot.wait(100) + + # Save zoom and scrollbar values to test them later + qtbot.wait(200) + zoom_1 = main_widget.zoom_disp.value() + vscrollbar = figviewer.verticalScrollBar() + hscrollbar = figviewer.horizontalScrollBar() + vscrollbar_value_1 = vscrollbar.value() + hscrollbar_value_1 = hscrollbar.value() + + # Fit action should be unchecked now + assert not main_widget.fit_action.isChecked() + + # Next plot should be still fitted + main_widget.next_plot() + assert main_widget.fit_action.isChecked() + assert main_widget.zoom_disp.value() < 100 + assert not hscrollbar.isVisible() + assert not vscrollbar.isVisible() + + # Zoom out twice this plot + for __ in range(2): + main_widget.zoom_out() + qtbot.wait(100) + + # Fit action should be unchecked now + assert not main_widget.fit_action.isChecked() + + # Save zoom level for later + zoom_2 = main_widget.zoom_disp.value() + + # Next plot should be still fitted + main_widget.next_plot() + assert main_widget.fit_action.isChecked() + + # Return to the first plot + for __ in range(2): + main_widget.previous_plot() + qtbot.wait(100) + + # Check zoom level and scrollbars are restored + qtbot.wait(200) + assert main_widget.zoom_disp.value() == zoom_1 + assert vscrollbar.value() == vscrollbar_value_1 + assert hscrollbar.value() == hscrollbar_value_1 + + # Move to next plot and check zoom level is restored + main_widget.next_plot() + assert main_widget.zoom_disp.value() == zoom_2