Jeff Long 5b5234d519 grc: formatting for qt-gui
Signed-off-by: Jeff Long <willcode4@gmail.com>
2024-03-15 15:11:27 -04:00

422 lines
15 KiB
Python
Executable File

import logging
from qtpy.QtGui import QPen, QPainter, QBrush, QFont, QFontMetrics
from qtpy.QtCore import Qt, QPointF, QRectF, QUrl
from qtpy.QtWidgets import QGraphicsItem, QApplication, QAction
from . import colors
from ... import Constants
from ... import Utils
from ....core.blocks.block import Block as CoreBlock
from ....core.utils import flow_graph_complexity
from ..dialogs import PropsDialog
# Logging
log = logging.getLogger(f"grc.application.{__name__}")
LONG_VALUE = 20 # maximum length of a param string.
# if exceeded, '...' will be displayed
class Block(CoreBlock):
"""
A block. Accesses its graphical representation with self.gui.
"""
@classmethod
def make_cls_with_base(cls, super_cls):
name = super_cls.__name__
bases = (super_cls,) + cls.__bases__[:-1]
namespace = cls.__dict__.copy()
return type(name, bases, namespace)
def __init__(self, parent, **n):
super(self.__class__, self).__init__(parent, **n)
self.width = 50 # will change immediately after the first paint
self.block_label = self.key
self.old_data = None
if "rotation" not in self.states.keys():
self.states["rotation"] = 0.0
self.gui = GUIBlock(self, parent)
def import_data(self, name, states, parameters, **_):
super(self.__class__, self).import_data(name, states, parameters, **_)
self.gui.setPos(*self.states["coordinate"])
self.gui.setRotation(self.states["rotation"])
self.rewrite()
self.gui.create_shapes_and_labels()
def update_bus_logic(self):
for direc in {'source', 'sink'}:
if direc == 'source':
ports = self.sources
ports_gui = self.filter_bus_port(self.sources)
else:
ports = self.sinks
ports_gui = self.filter_bus_port(self.sinks)
if 'bus' in map(lambda a: a.dtype, ports):
for port in ports_gui:
self.parent_flowgraph.gui.removeItem(port.gui)
super(self.__class__, self).update_bus_logic()
class GUIBlock(QGraphicsItem):
"""
The graphical representation of a block. Accesses the actual block with self.core.
"""
def __init__(self, core, parent, **n):
super(GUIBlock, self).__init__()
self.core = core
self.parent = self.scene()
self.font = QFont("Helvetica", 10)
self.create_shapes_and_labels()
if "coordinate" not in self.core.states.keys():
self.core.states["coordinate"] = (500, 300)
self.setPos(*self.core.states["coordinate"])
self.old_pos = (self.x(), self.y())
self.new_pos = (self.x(), self.y())
self.moving = False
self.props_dialog = None
self.right_click_menu = None
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges)
self.markups_height = 0.0
self.markups_width = 0.0
self.markups = []
self.markup_text = ""
def create_shapes_and_labels(self):
qsettings = QApplication.instance().qsettings
self.force_show_id = qsettings.value('grc/show_block_ids', type=bool)
self.hide_variables = qsettings.value('grc/hide_variables', type=bool)
self.hide_disabled_blocks = qsettings.value('grc/hide_disabled_blocks', type=bool)
self.snap_to_grid = qsettings.value('grc/snap_to_grid', type=bool)
self.show_complexity = qsettings.value('grc/show_complexity', type=bool)
self.show_block_comments = qsettings.value('grc/show_block_comments', type=bool)
self.show_param_expr = qsettings.value('grc/show_param_expr', type=bool)
self.show_param_val = qsettings.value('grc/show_param_val', type=bool)
self.prepareGeometryChange()
self.font.setBold(True)
# figure out height of block based on how many params there are
i = 35
# Check if we need to increase the height to fit all the ports
for key, item in self.core.params.items():
value = item.value
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
i += 20
self.height = i
def get_min_height_for_ports(ports):
min_height = (
2 * Constants.PORT_BORDER_SEPARATION +
len(ports) * Constants.PORT_SEPARATION
)
# If any of the ports are bus ports - make the min height larger
if any([p.dtype == "bus" for p in ports]):
min_height = (
2 * Constants.PORT_BORDER_SEPARATION +
sum(
port.gui.height + Constants.PORT_SPACING
for port in ports
if port.dtype == "bus"
) - Constants.PORT_SPACING
)
else:
if ports:
min_height -= ports[-1].gui.height
return min_height
self.height = max(
self.height,
get_min_height_for_ports(self.core.active_sinks),
get_min_height_for_ports(self.core.active_sources),
)
# Figure out width of block based on widest line of text
fm = QFontMetrics(self.font)
largest_width = fm.width(self.core.label)
for key, item in self.core.params.items():
name = item.name
value = item.value
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
if len(value) > LONG_VALUE:
value = value[:LONG_VALUE - 3] + '...'
value_label = item.options[value] if value in item.options else value
full_line = name + ": " + value_label
if fm.width(full_line) > largest_width:
largest_width = fm.width(full_line)
self.width = largest_width + 15
self.markups = []
self.markup_text = ""
self.markups_width = 0.0
if self.show_complexity and self.core.key == "options":
complexity = flow_graph_complexity.calculate(self.parent)
self.markups.append('Complexity: {num} bal'.format(
num=Utils.num_to_str(complexity)))
if self.show_block_comments and self.core.comment:
self.markups.append(self.core.comment)
self.markup_text = "\n".join(self.markups).strip()
self.markups_height = fm.height() * (self.markup_text.count("\n") + 1)
for line in self.markup_text.split("\n"):
if fm.width(line) > self.markups_width:
self.markups_width = fm.width(line)
# Update the position and size of all the ports
bussified = (
self.core.current_bus_structure["source"],
self.core.current_bus_structure["sink"],
)
for ports, has_busses in zip(
(self.core.active_sources, self.core.active_sinks), bussified
):
if not ports:
continue
port_separation = (
Constants.PORT_SEPARATION
if not has_busses
else ports[0].gui.height + Constants.PORT_SPACING
)
offset = (
self.height - (len(ports) - 1) * port_separation - ports[0].gui.height
) / 2
for port in ports:
if port._dir == "sink":
port.gui.setPos(-15, offset)
else:
port.gui.setPos(self.width, offset)
port.gui.create_shapes_and_labels()
offset += (
Constants.PORT_SEPARATION
if not has_busses
else port.gui.height + Constants.PORT_SPACING
)
self._update_colors()
self.create_port_labels()
self.setTransformOriginPoint(self.width / 2, self.height / 2)
def create_port_labels(self):
for ports in (self.core.active_sinks, self.core.active_sources):
for port in ports:
port.gui.create_shapes_and_labels()
def _update_colors(self):
def get_bg():
"""
Get the background color for this block
Explicit is better than a chain of if/else expressions,
so this was extracted into a nested function.
"""
if self.core.is_dummy_block:
return colors.MISSING_BLOCK_BACKGROUND_COLOR
if self.core.state == "bypassed":
return colors.BLOCK_BYPASSED_COLOR
if self.core.state == "enabled":
if self.core.deprecated:
return colors.BLOCK_DEPRECATED_BACKGROUND_COLOR
return colors.BLOCK_ENABLED_COLOR
return colors.BLOCK_DISABLED_COLOR
self._bg_color = get_bg()
def move(self, x, y):
self.moveBy(x, y)
self.core.states["coordinate"] = (self.x(), self.y())
def paint(self, painter, option, widget):
if (self.hide_variables and (self.core.is_variable or self.core.is_import)) or (self.hide_disabled_blocks and not self.core.enabled):
return
painter.setRenderHint(QPainter.Antialiasing)
self.font.setBold(True)
# TODO: Make sure this is correct
border_color = colors.BORDER_COLOR
if self.isSelected():
border_color = colors.HIGHLIGHT_COLOR
else:
if self.core.is_dummy_block:
border_color = colors.MISSING_BLOCK_BORDER_COLOR
elif self.core.deprecated:
border_color = colors.BLOCK_DEPRECATED_BORDER_COLOR
elif self.core.state == "disabled":
border_color = colors.BORDER_COLOR_DISABLED
pen = QPen(1)
pen = QPen(border_color)
pen.setWidth(3)
painter.setPen(pen)
painter.setBrush(QBrush(self._bg_color))
rect = QRectF(0, 0, self.width, self.height)
painter.drawRect(rect)
painter.setPen(QPen(1))
# Draw block label text
painter.setFont(self.font)
if self.core.is_valid():
painter.setPen(Qt.black)
else:
painter.setPen(Qt.red)
painter.drawText(
QRectF(0, 0 - self.height / 2 + 15, self.width, self.height),
Qt.AlignCenter,
self.core.label,
)
# Draw param text
y_offset = 30 # params start 30 down from the top of the box
for key, item in self.core.params.items():
name = item.name
value = item.value
is_evaluated = item.value != str(item.get_evaluated())
display_value = ""
# Include the value defined by the user (after evaluation)
if not is_evaluated or self.show_param_val or not self.show_param_expr:
display_value += item.options[value] if value in item.options else value # TODO: pretty_print
# Include the expression that was evaluated to get the value
if is_evaluated and self.show_param_expr:
expr_string = value # TODO: Truncate
if display_value: # We are already displaying the value
display_value = expr_string + "=" + display_value
else:
display_value = expr_string
if len(display_value) > LONG_VALUE:
display_value = display_value[:LONG_VALUE - 3] + '...'
value_label = display_value
if (value is not None and item.hide == "none") or (item.dtype == 'id' and self.force_show_id):
if item.is_valid():
painter.setPen(QPen(1))
else:
painter.setPen(Qt.red)
self.font.setBold(True)
painter.setFont(self.font)
painter.drawText(
QRectF(7.5, 0 + y_offset, self.width, self.height),
Qt.AlignLeft,
name + ": ",
)
fm = QFontMetrics(self.font)
self.font.setBold(False)
painter.setFont(self.font)
painter.drawText(
QRectF(
7.5 + fm.width(name + ": "),
0 + y_offset,
self.width,
self.height,
),
Qt.AlignLeft,
value_label,
)
y_offset += 20
if self.markup_text:
painter.setPen(Qt.gray)
painter.drawText(
QRectF(0, self.height + 5, self.markups_width, self.markups_height),
Qt.AlignLeft,
self.markup_text,
)
def boundingRect(self):
# TODO: Comments should be a separate QGraphicsItem
return QRectF(
-2.5, -2.5, self.width + 5 + self.markups_width, self.height + 5 + self.markups_height
)
def set_states(self, states):
for k, v in states.items():
self.core.states[k] = v
self.setPos(*self.core.states["coordinate"])
self.setRotation(self.core.states["rotation"])
def mousePressEvent(self, e):
super(self.__class__, self).mousePressEvent(e)
log.debug(f"{self} clicked")
url_prefix = str(self.core.parent_platform.config.wiki_block_docs_url_prefix)
QApplication.instance().WikiTab.setURL(QUrl(url_prefix + self.core.label.replace(" ", "_")))
self.moveToTop()
def contextMenuEvent(self, e):
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
self.right_click_menu = self.scene()._app().MainWindow.menus["edit"]
example_action = QAction("Examples...")
example_action.triggered.connect(self.view_examples)
self.right_click_menu.addAction(example_action)
self.right_click_menu.exec_(e.screenPos())
def view_examples(self):
self.scene().app.MainWindow.example_browser_triggered(key_filter=self.core.key)
def mouseDoubleClickEvent(self, e):
self.open_properties()
super(self.__class__, self).mouseDoubleClickEvent(e)
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionChange and self.scene() and self.snap_to_grid:
grid_size = 10
value.setX(round(value.x() / grid_size) * grid_size)
value.setY(round(value.y() / grid_size) * grid_size)
return value
else:
return QGraphicsItem.itemChange(self, change, value)
def rotate(self, rotation):
log.debug(f"Rotating {self.core.name}")
new_rotation = self.rotation() + rotation
self.setRotation(new_rotation)
self.core.states["rotation"] = new_rotation
self.create_shapes_and_labels()
for con in self.core.connections():
con.gui.update()
def moveToTop(self):
# TODO: Is there a simpler way to do this?
self.setZValue(self.scene().getMaxZValue() + 1)
def center(self):
return QPointF(self.x() + self.width / 2, self.y() + self.height / 2)
def open_properties(self):
self.props_dialog = PropsDialog(self.core, self.force_show_id)
self.props_dialog.show()