import logging
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from uwsift.common import LayerVisibility
LOG = logging.getLogger(__name__)
[docs]
class PieDial:
"""Widget for displaying visibility and opacity of a layer.
The available look and behaviour of the QDial widget is not suitable for
the purpose of displaying the state of visibility and opacity of a layer.
Therefore, a separate widget system was developed.
The opacity is indicated by the filled area of a pie:
- If, for example, the pie is completely filled, then the opacity
is 100%.
- If the cake is e.g. only half filled, the opacity is 50%.
Visibility is indicated by crossing out or not crossing out the circle.
If the cake is crossed out, the layer is invisible and vice versa.
The span angle value and the visibility value are changed via
the PieDialEditor.
The source code used as the basis for the custom dial widget is:
https://pastebin.com/yN3B3fc2
https://stackoverflow.com/questions/12011147/how-to-create-a-3-color-gradient-dial-indicator-the-one-that-shows-green-yellow
"""
def __init__(
self,
visible: bool = True,
opacity: float = 1.0,
pie_brush=QtCore.Qt.blue,
pie_pen=QtCore.Qt.NoPen,
strike_out_brush=QtCore.Qt.red,
strike_out_pen=QtCore.Qt.NoPen,
):
self._opacity = opacity
self._visible = visible
self._pie_brush = pie_brush
self._pie_pen = pie_pen
self._strike_out_brush = strike_out_brush
self._strike_out_pen = strike_out_pen
def _configure_appearance_of_drawn_pie(self, painter: QtGui.QPainter, rect: QtCore.QRect):
"""
:param painter:
:param rect:
"""
if isinstance(self._pie_brush, QtGui.QConicalGradient):
self._pie_brush.setCenter(QtCore.QPointF(rect.center()))
painter.setPen(self._pie_pen)
painter.setBrush(self._pie_brush)
def _configure_appearance_of_drawn_strike_out(self, painter: QtGui.QPainter):
"""
:param painter:
"""
painter.setPen(self._strike_out_pen)
painter.setBrush(self._strike_out_brush)
def _convert_current_value_to_degree(self) -> float:
"""Maps the current value of the widget to suitable degree value,
so that this converted value can be used e.g. for span angle of the
drawn pie by PyQt.
:return: converted value which is mapped to suitable degree value
"""
return -360 * self.opacity
[docs]
def paint(self, painter: QtGui.QPainter, rect: QtCore.QRect):
"""
:param painter:
:param rect:
"""
painter.save()
painter.setRenderHint(painter.Antialiasing, True)
pie_rect = QtCore.QRect(rect)
pie_rect_size_min = min(pie_rect.size().width(), pie_rect.size().height())
pie_rect.setSize(QtCore.QSize(pie_rect_size_min - 2, pie_rect_size_min - 2))
pie_rect.moveCenter(rect.center())
self._configure_appearance_of_drawn_pie(painter, pie_rect)
painter.drawPie(pie_rect, int(90.0 * 16), int(self._convert_current_value_to_degree() * 16))
if not self._visible:
blocked_rect = QtCore.QRect(pie_rect)
current_size = blocked_rect.size()
current_width = current_size.width()
current_size.setWidth(int(current_width / 4))
current_size.setHeight(int(current_size.height() * 1.5))
blocked_rect.setSize(current_size)
blocked_rect.moveCenter(QtCore.QPointF(pie_rect.center()).toPoint())
center = blocked_rect.center()
transform = (
QtGui.QTransform().translate(center.x(), center.y()).rotate(45.0).translate(-center.x(), -center.y())
)
rotated_rect = transform.mapToPolygon(blocked_rect)
self._configure_appearance_of_drawn_strike_out(painter)
painter.drawPolygon(rotated_rect)
painter.restore()
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, visible: bool):
self._visible = visible
@property
def opacity(self):
return self._opacity
@opacity.setter
def opacity(self, opacity: float):
self._opacity = float(min(max(opacity, 0.0), 1.0))
[docs]
class PieDialEditor(QtWidgets.QWidget):
editingFinished = QtCore.pyqtSignal()
def __init__(self, parent: QtWidgets.QWidget, pie_dial: PieDial):
super(PieDialEditor, self).__init__(parent=parent)
self.setAutoFillBackground(True)
self.pie_dial = pie_dial
self._layer_opacity_popup = SliderPopup(parent=self)
self._layer_opacity_popup.get_slider().valueChanged.connect(self._set_opacity_in_percent)
self._current_mouse_pos = QtCore.QPoint(0, 0)
def _set_opacity_in_percent(self, val: int) -> None:
self.pie_dial.opacity = val / 100
self.editingFinished.emit()
[docs]
def set_pie_dial(self, pie_dial: PieDial):
"""
:param pie_dial:
"""
self.pie_dial = pie_dial
[docs]
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
"""
:param event:
"""
painter = QtGui.QPainter(self)
if self.pie_dial:
self.pie_dial.paint(painter, self.rect())
[docs]
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
"""
:param event:
"""
if event.button() == QtCore.Qt.RightButton and not self._layer_opacity_popup.active:
global_pos = self.mapToGlobal(event.pos())
self._layer_opacity_popup.show_at(global_pos, self.pie_dial.opacity)
if event.button() == QtCore.Qt.LeftButton and not self._layer_opacity_popup.active:
self.pie_dial.visible = not self.pie_dial.visible
self.editingFinished.emit()
# The event must not fall through to the parent ItemView, which might
# react to it otherwise, which would feel strange, thus:
event.accept()
[docs]
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
# The event must not fall through to the parent ItemView, which might
# react to it otherwise, which would feel strange, thus:
event.accept()
[docs]
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
# The event must not fall through to the parent ItemView, which might
# react to it otherwise, which would feel strange.
# TODO here the feature to directly modify visibility/opacity by mouse
# moving could be implemented.
event.accept()
[docs]
def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
"""
:param event:
"""
if event.angleDelta().y() > 0 and not self._layer_opacity_popup.active:
self.pie_dial.opacity = self.pie_dial.opacity + 0.05
self.editingFinished.emit()
elif event.angleDelta().y() < 0 and not self._layer_opacity_popup.active:
self.pie_dial.opacity = self.pie_dial.opacity - 0.05
self.editingFinished.emit()
# The event must not fall through to the parent ItemView, which would
# scroll its view area otherwise, thus:
event.accept()
[docs]
class PieDialDelegate(QtWidgets.QStyledItemDelegate):
""" """
def __init__(self, *args, **kwargs):
super(PieDialDelegate, self).__init__(*args, **kwargs)
[docs]
def createEditor(
self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex
) -> QtWidgets.QWidget:
"""
:param parent:
:param option:
:param index:
:return:
"""
editor = PieDialEditor(parent, PieDial())
editor.editingFinished.connect(self._commit_and_close_editor)
return editor
[docs]
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None:
layer_visibility: LayerVisibility = index.data()
pie_dial = PieDial(visible=layer_visibility.visible, opacity=layer_visibility.opacity)
pie_dial.paint(painter, option.rect)
def _commit_and_close_editor(self):
""" """
editor = self.sender()
self.commitData.emit(editor)
[docs]
def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex) -> None:
"""
:param editor:
:param index:
"""
layer_visibility: LayerVisibility = index.data()
if not editor.pie_dial:
pie_dial = PieDial(visible=layer_visibility.visible, opacity=layer_visibility.opacity)
editor.set_pie_dial(pie_dial)
else:
editor.pie_dial.visible = layer_visibility.visible
editor.pie_dial.opacity = layer_visibility.opacity
editor.update()
[docs]
def setModelData(
self, editor: QtWidgets.QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex
) -> None:
"""
:param editor:
:param model:
:param index:
"""
layer_visibility = LayerVisibility(editor.pie_dial.visible, editor.pie_dial.opacity)
if index.data() != layer_visibility:
model.setData(index, layer_visibility)
editor.update()
[docs]
def updateEditorGeometry(
self, editor: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex
) -> None:
"""
:param editor:
:param option:
:param index:
"""
editor.setGeometry(option.rect)
[docs]
def main():
app = QtWidgets.QApplication(sys.argv)
example = QtWidgets.QWidget()
example.setGeometry(0, 0, 40, 40)
layout = QtWidgets.QVBoxLayout(example)
pie_dial_editor = PieDialEditor(example)
pie_dial = PieDial(visible=True, opacity=1.0)
pie_dial_editor.set_pie_dial(pie_dial)
layout.addWidget(pie_dial_editor)
example.show()
example.raise_()
sys.exit(app.exec_())
if __name__ == "__main__":
main()