File: C:/Users/fred/anaconda3/Lib/site-packages/spyder/plugins/layout/api.py
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""
Layout Plugin API.
"""
# Standard libray imports
import copy
# Third party imports
from qtpy.QtCore import QRectF, Qt
from qtpy.QtWidgets import QGridLayout, QPlainTextEdit, QWidget
# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY
from spyder.api.translations import _
class BaseGridLayoutType:
"""
A base layout type to create custom layouts for Spyder panes.
The API for this plugin is a subset of a QGridLayout, so the same
concepts, like row, column, spans and stretches apply.
Notes
-----
See: https://doc.qt.io/qt-5/qgridlayout.html
"""
ID = None
"""Unique string identifier for the layout."""
def __init__(self, parent_plugin):
self.plugin = parent_plugin
self._areas = []
self._area_rects = []
self._column_stretchs = {}
self._row_stretchs = {}
self._default_added = False
self._default_area = None
self._visible_areas = []
self._rows = 0
self._cols = 0
self._plugin_ids = []
# --- Private API
# ------------------------------------------------------------------------
def _check_layout_validity(self):
"""
Check the current layout is a valid one.
"""
self._visible_areas = []
# Check ID
if self.ID is None:
raise SpyderAPIError("A Layout must define an `ID` class "
"attribute!")
# Check name
self.get_name()
# All layouts need to add at least 1 area
if not self._areas:
raise SpyderAPIError("A Layout must define add least one area!")
default_areas = []
area_zero_zero = False
for area in self._areas:
default_areas.append(area["default"])
if area["default"]:
self._default_area = area
self._visible_areas.append(area["visible"])
if area_zero_zero and area["row"] == 0 and area["column"] == 0:
raise SpyderAPIError(
"Multiple areas defined their row and column as 0!")
if area["row"] == 0 and area["column"] == 0:
area_zero_zero = True
if not set(area["hidden_plugin_ids"]) <= set(area["plugin_ids"]):
raise SpyderAPIError(
"At least 1 hidden plugin id is not being specified "
"in the area plugin ids list!\n SpyderLayout: {}\n "
"hidden_plugin_ids: {}\n"
"plugin_ids: {}".format(self.get_name(),
area["hidden_plugin_ids"],
area["plugin_ids"]))
# Check that there is at least 1 visible!
if not any(self._visible_areas):
raise SpyderAPIError("At least 1 area must be `visible`")
# Check that there is a `default` area!
if not any(default_areas):
raise SpyderAPIError("No area is the `default`!")
# Check that there is 1 `default` area!
if default_areas.count(True) != 1:
raise SpyderAPIError("Only 1 area can be the `default`!")
# Check one area has row zero and column zero
if not area_zero_zero:
raise SpyderAPIError(
"1 area needs to be specified with row 0 and column 0!")
# Check Area
self._check_area()
def _check_area(self):
"""
Check if the current layout added areas cover the entire rectangle.
Rectangle given by the extreme points for the added areas.
"""
self._area_rects = []
height = self._rows + 1
area_float_rects = []
delta = 0.0001
for index, area in enumerate(self._areas):
# These areas are used with a small delta to ensure if they are
# next to each other they will not overlap.
rectf = QRectF()
rectf.setLeft(area["column"] + delta)
rectf.setRight(area["column"] + area["col_span"] - delta)
rectf.setTop(height - area["row"] - delta)
rectf.setBottom(height - area["row"] - area["row_span"] + delta)
rectf.index = index
rectf.plugin_ids = area["plugin_ids"]
area_float_rects.append(rectf)
# These areas are used to calculate the actual total area
rect = QRectF()
rect.setLeft(area["column"])
rect.setRight(area["column"] + area["col_span"])
rect.setTop(height - area["row"])
rect.setBottom(height - area["row"] - area["row_span"])
rect.index = index
rect.plugin_ids = area["plugin_ids"]
self._area_rects.append(rect)
# Check if areas are overlapping!
for rect_1 in area_float_rects:
for rect_2 in area_float_rects:
if rect_1.index != rect_2.index:
if rect_1.intersects(rect_2):
raise SpyderAPIError(
"Area with plugins {0} is overlapping area "
"with plugins {1}".format(rect_1.plugin_ids,
rect_2.plugin_ids))
# Check the total area (using corner points) versus the sum of areas
total_area = 0
tops = []
rights = []
for index, rect in enumerate(self._area_rects):
tops.append(rect.top())
rights.append(rect.right())
area = abs(rect.width() * rect.height())
total_area += area
self._areas[index]["area"] = area
if total_area != max(rights)*max(tops):
raise SpyderAPIError(
"Areas are not covering the entire section!\n"
"Either an area is missing or col_span/row_span are "
"not correctly set!"
)
# --- SpyderGridLayout API
# ------------------------------------------------------------------------
def get_name(self):
"""
Return the layout localized name.
Returns
-------
str
Localized name of the layout.
Notes
-----
This is a method to be able to update localization without a restart.
"""
raise NotImplementedError("A layout must define a `get_name` method!")
# --- Public API
# ------------------------------------------------------------------------
def add_area(self,
plugin_ids,
row,
column,
row_span=1,
col_span=1,
default=False,
visible=True,
hidden_plugin_ids=[]):
"""
Add a new area and `plugin_ids` that will populate it to the layout.
The area will start at row, column spanning row_pan rows and
column_span columns.
Parameters
----------
plugin_ids: list
List of plugin ids that will be in the area
row: int
Initial row where the area starts
column: int
Initial column where the area starts
row_span: int, optional
Number of rows that the area covers
col_span: int, optional
Number of columns the area covers
default: bool, optiona
Defines an area as the default one, i.e all other plugins that where
not passed in the `plugins_ids` will be added to the default area.
By default is False.
visible: bool, optional
Defines if the area is visible when setting up the layout.
Default is True.
Notes
-----
See: https://doc.qt.io/qt-5/qgridlayout.html
"""
if self._default_added and default:
raise SpyderAPIError("A default location has already been "
"defined!")
self._plugin_ids += plugin_ids
self._rows = max(row, self._rows)
self._cols = max(column, self._cols)
self._default_added = default
self._column_stretchs[column] = 1
self._row_stretchs[row] = 1
self._areas.append(
dict(
plugin_ids=plugin_ids,
row=row,
column=column,
row_span=row_span,
col_span=col_span,
default=default,
visible=visible,
hidden_plugin_ids=hidden_plugin_ids,
)
)
def set_column_stretch(self, column, stretch):
"""
Set the factor of column to stretch.
The stretch factor is relative to the other columns in this grid.
Columns with a higher stretch factor take more of the available space.
Parameters
----------
column: int
The column number. The first column is number 0.
stretch: int
Column stretch factor.
Notes
-----
See: https://doc.qt.io/qt-5/qgridlayout.html
"""
self._column_stretchs[column] = stretch
def set_row_stretch(self, row, stretch):
"""
Set the factor of row to stretch.
The stretch factor is relative to the other rows in this grid.
Rows with a higher stretch factor take more of the available space.
Parameters
----------
row: int
The row number. The first row is number 0.
stretch: int
Row stretch factor.
Notes
-----
See: https://doc.qt.io/qt-5/qgridlayout.html
"""
self._row_stretchs[row] = stretch
def preview_layout(self, show_hidden_areas=False):
"""
Show the layout with placeholder texts using a QWidget.
"""
from spyder.utils.qthelpers import qapplication
app = qapplication()
widget = QWidget()
layout = QGridLayout()
for area in self._areas:
label = QPlainTextEdit()
label.setReadOnly(True)
label.setPlainText("\n".join(area["plugin_ids"]))
if area["visible"] or show_hidden_areas:
layout.addWidget(
label,
area["row"],
area["column"],
area["row_span"],
area["col_span"],
)
# label.setVisible(area["visible"])
if area["default"]:
label.setStyleSheet(
"QPlainTextEdit {background-color: #ff0000;}")
if not area["visible"]:
label.setStyleSheet(
"QPlainTextEdit {background-color: #eeeeee;}")
for row, stretch in self._row_stretchs.items():
layout.setRowStretch(row, stretch)
for col, stretch in self._column_stretchs.items():
layout.setColumnStretch(col, stretch)
widget.setLayout(layout)
widget.showMaximized()
app.exec_()
def set_main_window_layout(self, main_window, dockable_plugins):
"""
Set the given mainwindow layout.
First validate the current layout definition, then clear the mainwindow
current layout and finally calculate and set the new layout.
"""
# Define plugins assigned to areas, all the available plugins and
# initial docks for each area
all_plugin_ids = []
# External dockable plugins to show after the layout is applied
external_plugins_to_show = []
# Before applying a new layout all plugins need to be hidden
for plugin in dockable_plugins:
all_plugin_ids.append(plugin.NAME)
# Save currently displayed external plugins
if (
plugin.NAME in PLUGIN_REGISTRY.external_plugins
and plugin.dockwidget.isVisible()
):
external_plugins_to_show.append(plugin.NAME)
plugin.toggle_view(False)
# Add plugins without an area assigned to the default area and made
# them hidden. Deep copy needed since test can run multiple times with
# the same Mainwindow instance when using the 'main_window' fixture
patched_default_area = copy.deepcopy(self._default_area)
unassgined_plugin_ids = list(
set(self._plugin_ids) ^ set(all_plugin_ids))
patched_default_area["plugin_ids"] += unassgined_plugin_ids
patched_default_area["hidden_plugin_ids"] += unassgined_plugin_ids
patched_areas = [
patched_default_area if area["default"] else area
for area in self._areas]
# Define initial dock for each area
docks = {}
for area in patched_areas:
current_area = area
plugin_id = current_area["plugin_ids"][0]
plugin = main_window.get_plugin(plugin_id, error=False)
if plugin:
dock = plugin.dockwidget
docks[(current_area["row"], current_area["column"])] = dock
dock.area = area["area"]
dock.col_span = area["col_span"]
dock.row_span = area["row_span"]
plugin.toggle_view(area["visible"])
# Define base layout (distribution of dockwidgets
# following defined areas)
layout_data = []
# Find dock splits in the horizontal direction
direction = Qt.Horizontal
for row in range(0, self._rows + 1):
dock = None
for col in range(0, self._cols + 1):
key = (row, col)
if key in docks:
if dock is None:
dock = docks[key]
else:
layout_data.append(
(1/docks[key].area,
key,
dock,
docks[key],
direction))
dock = docks[key]
main_window.addDockWidget(
Qt.LeftDockWidgetArea, dock, direction)
# Find dock splits in the vertical direction
direction = Qt.Vertical
for col in range(0, self._cols + 1):
dock = None
for row in range(0, self._rows + 1):
key = (row, col)
if key in docks:
if dock is None:
dock = docks[key]
else:
layout_data.append(
(1/docks[key].area,
key,
dock,
docks[key],
direction))
dock = docks[key]
# We sort based on the inverse of the area, then the row and then
# the column. This allows to make the dock splits in the right order.
sorted_data = sorted(layout_data, key=lambda x: (x[0], x[1]))
for area, key, first, second, direction in sorted_data:
main_window.splitDockWidget(first, second, direction)
plugins_to_tabify = []
for area in patched_areas:
area_visible = area["visible"]
base_plugin = main_window.get_plugin(
area["plugin_ids"][0], error=False)
if base_plugin:
plugin_ids = area["plugin_ids"][1:]
hidden_plugin_ids = area["hidden_plugin_ids"]
for plugin_id in plugin_ids:
current_plugin = main_window.get_plugin(
plugin_id, error=False)
if current_plugin:
if (plugin_id in unassgined_plugin_ids and
hasattr(current_plugin, 'TABIFY')):
plugins_to_tabify.append(
(current_plugin, base_plugin))
else:
self.plugin.tabify_plugins(
base_plugin, current_plugin)
if plugin_id not in hidden_plugin_ids:
current_plugin.toggle_view(area_visible)
else:
current_plugin.toggle_view(False)
# Raise front widget per area
if area["visible"]:
base_plugin.dockwidget.show()
base_plugin.dockwidget.raise_()
# try to use the TABIFY attribute to add the plugin to the layout.
# Otherwise use the default area base plugin
for plugin, base_plugin in plugins_to_tabify:
if not self.plugin.tabify_plugin(plugin):
self.plugin.tabify_plugins(base_plugin, plugin)
current_plugin.toggle_view(False)
column_docks = []
column_stretches = []
for key, dock in docks.items():
for col, stretch in self._column_stretchs.items():
if key[1] == col and dock.col_span == 1:
column_docks.append(dock)
column_stretches.append(stretch)
row_docks = []
row_stretches = []
for key, dock in docks.items():
for row, stretch in self._row_stretchs.items():
if key[0] == row and dock.row_span == 1:
row_docks.append(dock)
row_stretches.append(stretch)
main_window.showMaximized()
main_window.resizeDocks(column_docks, column_stretches, Qt.Horizontal)
main_window.resizeDocks(row_docks, row_stretches, Qt.Vertical)
# Restore displayed external plugins
for plugin_id in external_plugins_to_show:
plugin = main_window.get_plugin(plugin_id, error=False)
if plugin:
# Show plugin
plugin.blockSignals(True)
plugin.dockwidget.show()
plugin.toggle_view_action.setChecked(True)
plugin.blockSignals(False)
# Make visible the first plugin in the dockwidget's tab bar
# because it should be an internal one.
dock_tabbar = plugin.dockwidget.dock_tabbar
if dock_tabbar:
dock_tabbar.setCurrentIndex(0)