######################################################################################################################
# Copyright (C) 2017-2021 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""
Classes for custom QListView.
:author: M. Marin (KTH)
:date: 14.11.2018
"""
import textwrap
from PySide2.QtCore import Qt, Signal, Slot, QMimeData
from PySide2.QtGui import QDrag, QIcon, QPainter, QBrush, QColor, QFont, QIconEngine
from PySide2.QtWidgets import QToolButton, QApplication, QToolBar, QWidgetAction
from ..helpers import CharIconEngine, make_icon_background
[docs]class ProjectItemDragMixin:
"""Custom class with dragging support.
"""
[docs] drag_about_to_start = Signal()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.drag_start_pos = None
self.pixmap = None
self.mime_data = None
[docs] def mouseMoveEvent(self, event):
"""Start dragging action if needed"""
super().mouseMoveEvent(event)
if not event.buttons() & Qt.LeftButton:
return
if not self.drag_start_pos:
return
if (event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
return
drag = QDrag(self)
drag.setPixmap(self.pixmap)
drag.setMimeData(self.mime_data)
drag.setHotSpot(self.pixmap.rect().center())
self.drag_start_pos = None
self.pixmap = None
self.mime_data = None
self.drag_about_to_start.emit()
drag.exec_()
[docs] def mouseReleaseEvent(self, event):
"""Forget drag start position"""
super().mouseReleaseEvent(event)
self.drag_start_pos = None
self.pixmap = None
self.mime_data = None
[docs]class ShadeMixin:
[docs] def paintEvent(self, ev):
painter = QPainter(self)
brush = QBrush(QColor(255, 255, 255, a=96))
rect = ev.rect()
painter.fillRect(rect, brush)
painter.end()
super().paintEvent(ev)
[docs]class _ChoppedIcon(QIcon):
def __init__(self, icon, size):
self._engine = _ChoppedIconEngine(icon, size)
super().__init__(self._engine)
[docs] def update(self):
self._engine.update()
[docs]class _ChoppedIconEngine(QIconEngine):
def __init__(self, icon, size):
super().__init__()
self._pixmap = None
self._icon = icon
self._size = size
self.update()
[docs] def update(self):
self._pixmap = self._icon.pixmap(self._icon.actualSize(self._size))
[docs] def pixmap(self, size, mode, state):
return self._pixmap
[docs]class ProjectItemSpecArray(QToolBar):
"""An array of ProjectItemSpecButton that can be expanded/collapsed."""
def __init__(self, toolbox, model, item_type, icon):
"""
Args:
toolbox (ToolboxUI)
model (FilteredSpecificationModel)
item_type (str)
icon (ColoredIcon)
"""
super().__init__()
self._extension_button = next(iter(self.findChildren(QToolButton)))
self._margin = 4
self.layout().setMargin(self._margin)
self._maximum_size = self.maximumSize()
self._model = model
self._toolbox = toolbox
self.item_type = item_type
self._icon = icon
self._visible = False
self._button_base_item = ProjectItemButton(self._toolbox, self.item_type, self._icon)
self._button_base_item.double_clicked.connect(self.toggle_visibility)
self.addWidget(self._button_base_item)
self._button_visible = QToolButton()
font = QFont("Font Awesome 5 Free Solid")
font.setPointSize(8)
self._button_visible.setFont(font)
self._button_visible.setToolTip(f"<p>Show/hide {self.item_type} specifications</p>")
self._update_button_visible_icon_color()
self.addWidget(self._button_visible)
self._button_new = ShadeButton()
self._button_new.setIcon(QIcon(CharIconEngine("\uf067", color=Qt.darkGreen)))
self._button_new.setText("New...")
self._button_new.setToolTip(f"<p>Create new <b>{item_type}</b> specification...</p>")
font = QFont()
font.setPointSize(9)
self._button_new.setFont(font)
self._button_new.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self._action_new = self.addWidget(self._button_new)
self._action_new.setVisible(self._visible)
self._actions = {}
self._chopped_icon = _ChoppedIcon(self._icon, self.iconSize())
self._button_filling = ShadeProjectItemSpecButton(self._toolbox, self.item_type, self._chopped_icon)
self._button_filling.setParent(self)
self._button_filling.setVisible(False)
self._model.rowsInserted.connect(self._insert_specs)
self._model.rowsRemoved.connect(self._remove_specs)
self._model.modelReset.connect(self._reset_specs)
self._button_visible.clicked.connect(self.toggle_visibility)
self._button_new.clicked.connect(self._show_spec_form)
self.orientationChanged.connect(self._update_button_geom)
self.show()
[docs] def set_colored_icons(self, colored):
self._icon.set_colored(colored)
self._chopped_icon.update()
self._update_button_visible_icon_color()
[docs] def set_color(self, color):
bg = make_icon_background(color)
ss = f"QMenu {{background: {bg};}}"
self._extension_button.menu().setStyleSheet(ss)
[docs] def paintEvent(self, ev):
super().paintEvent(ev)
if not self._visible:
return
actions, ind = self._get_first_chopped_index()
self._add_filling(actions, ind)
self._populate_extension_menu(actions, ind)
[docs] def _get_first_chopped_index(self):
"""Returns the index of the first chopped action (chopped = not drawn because of space).
Returns:
list(QAction)
int or NoneType
"""
actions = [*self._actions.values()]
if self.orientation() == Qt.Horizontal:
get_point = lambda ref_geom: (ref_geom.right() + 1, ref_geom.top())
else:
get_point = lambda ref_geom: (ref_geom.left(), ref_geom.bottom() + 1)
ref_widget = self._button_new
for i, act in enumerate(actions):
ref_geom = ref_widget.geometry()
x, y = get_point(ref_geom)
if not self.actionAt(x, y):
return actions, i
ref_widget = self.widgetForAction(act)
return actions, None
[docs] def _add_filling(self, actions, ind):
"""Adds a button to fill empty space after the last visible action.
Args:
actions (list(QAction)): actions
ind (int or NoneType): index of the first chopped one or None if all are visible
"""
if ind is None:
self._button_filling.setVisible(False)
return
if ind > 0:
previous = self.widgetForAction(actions[ind - 1])
else:
previous = self._button_new
x, y, w, h = self._get_filling(previous)
if w <= 0 or h <= 0:
self._button_filling.setVisible(False)
return
self._button_filling.move(x, y)
self._button_filling.setFixedSize(w, h)
self._button_filling.setVisible(True)
button = self.widgetForAction(actions[ind])
self._button_filling.spec_name = button.spec_name
[docs] def _get_filling(self, previous):
"""Returns the position and size of the filling widget.
Args:
previous (QWidget): last visible widget
Returns:
int: position x
int: position y
int: width
int: height
"""
geom = previous.geometry()
style = self.style()
extension_extent = style.pixelMetric(style.PM_ToolBarExtensionExtent)
if self.orientation() == Qt.Horizontal:
toolbar_size = self.width() - extension_extent - 2 * self._margin + 2
x, y = geom.right() + 1, geom.top()
w, h = toolbar_size - geom.right(), geom.height()
else:
toolbar_size = self.height() - extension_extent - 2 * self._margin + 2
x, y = geom.left(), geom.bottom() + 1
w, h = geom.width(), toolbar_size - geom.bottom()
return x, y, w, h
[docs] def showEvent(self, ev):
super().showEvent(ev)
self._update_button_geom()
@Slot(bool)
@Slot(bool)
[docs] def toggle_visibility(self, _checked=False):
self.set_visible(not self._visible)
self._update_button_geom()
[docs] def set_visible(self, visible):
self._visible = visible
for action in self._actions.values():
action.setVisible(self._visible)
self._action_new.setVisible(self._visible)
[docs] def _insert_specs(self, parent, first, last):
for row in range(first, last + 1):
self._add_spec(row)
self._update_button_geom()
[docs] def _remove_specs(self, parent, first, last):
for row in range(first, last + 1):
try:
action = self._actions.pop(row)
self.removeAction(action)
except KeyError:
pass # Happens when Plugins are removed
self._update_button_geom()
[docs] def _reset_specs(self):
for action in self._actions.values():
self.removeAction(action)
self._actions.clear()
for row in range(self._model.rowCount()):
self._add_spec(row)
self._update_button_geom()
[docs] def _add_spec(self, row):
index = self._model.index(row, 0)
source_index = self._model.mapToSource(index)
spec = self._model.sourceModel().specification(source_index.row())
if spec.plugin:
return
button = ShadeProjectItemSpecButton(self._toolbox, spec.item_type, self._icon, spec.name)
button.setIconSize(self.iconSize())
button.set_orientation(self.orientation())
action = self.addWidget(button)
action.setVisible(self._visible)
self._actions[row] = action