drag it! animate it!
This commit is contained in:
parent
f73d04f7a9
commit
a3ccf68845
|
@ -1,9 +1,12 @@
|
|||
from typing import Tuple, Optional, Union, Dict, List
|
||||
from typing import Tuple, Optional, Union, Dict, List, Set
|
||||
from collections import defaultdict
|
||||
from math import sqrt, sin, cos, pi
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
from threading import Lock
|
||||
import logging
|
||||
|
||||
from PySide6.QtCore import QRectF, QPointF
|
||||
from PySide6.QtCore import QRectF, QPointF, QTimeLine, QEasingCurve, QMarginsF, QTimer, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QBrush, QPainterPath, QPen
|
||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsRectItem, QGraphicsSimpleTextItem, QHBoxLayout, QGraphicsScene, \
|
||||
QGraphicsEllipseItem
|
||||
|
@ -21,6 +24,8 @@ from .hierarchy_graph import HierarchicalGraph, RelativeAtomGroup, RelativeAtomO
|
|||
from .data import Prop, DataKind
|
||||
from .relative_graph import RelativeAtom
|
||||
|
||||
l = logging.getLogger(__name__)
|
||||
|
||||
TEXT_COLOR = QColor(0, 0, 0)
|
||||
TEXT_FONT = QFont("default", 10)
|
||||
BACKGROUND_COLOR = QColor(255, 255, 255)
|
||||
|
@ -30,6 +35,9 @@ BOX_BORDER_SELECTED_COLOR = QColor(0x60, 0x60, 0xff)
|
|||
BOX_BORDER_WIDTH = 1
|
||||
BOX_BORDER_SELECTED_WIDTH = 2
|
||||
|
||||
SCENE_MARGIN = 200
|
||||
DPI = 72
|
||||
|
||||
def kind_to_color(kind: DataKind) -> QColor:
|
||||
if kind == DataKind.Pointer:
|
||||
return QColor(0xe0, 0x10, 0x10)
|
||||
|
@ -50,122 +58,63 @@ class HierarchicalGraphView(BaseView):
|
|||
layout.addWidget(self.graph)
|
||||
self.setLayout(layout)
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
@dataclass(frozen=True)
|
||||
class ExternNode:
|
||||
goes_out: bool
|
||||
goes_in: bool
|
||||
adj_to: RelativeAtomOrGroup
|
||||
|
||||
RelativeAtomOrGroupOrExtern = Union[RelativeAtomOrGroup, ExternNode]
|
||||
|
||||
class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
||||
def __init__(self, parent, instance: Instance, hg: HierarchicalGraph, current_group: Optional[RelativeAtomGroup]=None):
|
||||
self.hg = hg
|
||||
self.current_group: Union[ObjectContainer, RelativeAtomGroup] = ObjectContainer(hg.root_group if current_group is None else current_group)
|
||||
self.current_node: Union[ObjectContainer, RelativeAtomOrGroup, None] = ObjectContainer(None)
|
||||
self.selected_nodes: Union[ObjectContainer, Set[RelativeAtomOrGroupOrExtern]] = ObjectContainer(set())
|
||||
self.instance = instance
|
||||
self._labels = {}
|
||||
self.nodes: Dict[RelativeAtomOrGroup, PropChartHG] = {}
|
||||
self.qnodes: List[HGNode] = []
|
||||
self.qedges: List[HGArrow] = []
|
||||
self.layout_prog = 'neato'
|
||||
self.layout_social_distancing = 200 / DPI
|
||||
self.layout_overlap = 'vpsc'
|
||||
|
||||
self.ng = None
|
||||
self.ug = None
|
||||
self.ag = None
|
||||
self.ug_lookup: List[RelativeAtomOrGroupOrExtern] = []
|
||||
self.ug_reverse: Dict[RelativeAtomOrGroupOrExtern, int] = {}
|
||||
self.adding_positions = {}
|
||||
self._layout_lock = Lock() # ??? should this be a qt mutex?
|
||||
self._layout_animation = QTimer()
|
||||
self._layout_animation_controller = QTimer()
|
||||
self._mouse_is_down = False
|
||||
self._selection_event_occurring = False
|
||||
self._layout_event_occurring = False
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setScene(QGraphicsScene())
|
||||
self._layout()
|
||||
self.current_group.am_subscribe(self.on_new_group)
|
||||
self.current_group.am_subscribe(self._on_change_group)
|
||||
self.selected_nodes.am_subscribe(self._on_selection_changed)
|
||||
self._layout_animation.setInterval(16)
|
||||
self._layout_animation.setSingleShot(False)
|
||||
self._layout_animation.timeout.connect(self._iterate_layout)
|
||||
self._layout_animation_controller.setSingleShot(True)
|
||||
self._layout_animation_controller.timeout.connect(self._layout_animation.stop)
|
||||
|
||||
def on_new_group(self, **kwargs):
|
||||
self._layout()
|
||||
self._on_change_group(src='__init__')
|
||||
|
||||
def navigate(self, node: RelativeAtomOrGroup):
|
||||
# public interfaces
|
||||
|
||||
@property
|
||||
def selected_node(self) -> Optional[RelativeAtomOrGroupOrExtern]:
|
||||
return self.selected_nodes[0] if len(self.selected_nodes) == 1 else None
|
||||
|
||||
def navigate(self, node: RelativeAtomOrGroupOrExtern):
|
||||
if isinstance(node, RelativeAtomGroup):
|
||||
self.current_group.am_obj = node
|
||||
self.current_group.am_event()
|
||||
elif isinstance(node, RelativeAtom):
|
||||
self.instance.workspace.jump_to(node.atom.loc.ins_addr)
|
||||
else:
|
||||
assert False
|
||||
|
||||
def select(self, node: Optional[RelativeAtomOrGroup]):
|
||||
try:
|
||||
self.nodes[self.current_node.am_obj].update()
|
||||
except KeyError:
|
||||
pass
|
||||
self.current_node.am_obj = node
|
||||
self.current_node.am_event()
|
||||
try:
|
||||
self.nodes[self.current_node.am_obj].update()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _layout(self):
|
||||
for item in list(self.scene().items()):
|
||||
self.scene().removeItem(item)
|
||||
self.nodes.clear()
|
||||
|
||||
word_height = 40
|
||||
byte_height = word_height / self.hg.kp.kb._project.arch.bytes
|
||||
|
||||
current_group = self.current_group.am_obj
|
||||
ng = self.hg.local_graph(current_group)
|
||||
ug = networkx.Graph(ng)
|
||||
for edge in ug.edges:
|
||||
del ug.edges[edge]['prev']
|
||||
del ug.edges[edge]['next']
|
||||
enodes = []
|
||||
if current_group in ug.nodes:
|
||||
for adj in list(ug.adj[current_group]):
|
||||
enode = ExternNode(goes_out=current_group in ng.succ[adj], goes_in=current_group in ng.pred[adj])
|
||||
enodes.append(enode)
|
||||
ug.remove_edge(current_group, adj)
|
||||
ug.add_edge(adj, enode)
|
||||
assert len(ug.adj[current_group]) == 0
|
||||
ug.remove_node(current_group)
|
||||
|
||||
lookup = list(ug.nodes())
|
||||
reverse = {x: i for i, x in enumerate(lookup)}
|
||||
qnodes: List[PropNode] = [PropExtern(None) if isinstance(node, ExternNode) else PropChartHG(
|
||||
None,
|
||||
self.hg.prop(node),
|
||||
byte_height=byte_height,
|
||||
nav=node,
|
||||
hgw=self,
|
||||
label=self.label(node)) for node in lookup]
|
||||
networkx.relabel_nodes(ug, reverse, copy=False)
|
||||
|
||||
ag = networkx.drawing.nx_agraph.to_agraph(ug)
|
||||
for node in ag.nodes_iter():
|
||||
idx = int(node)
|
||||
qnodes[idx].set_agraph_node_properties(node)
|
||||
ag.edge_attr['len'] = 50 / 72 + 200 / 72
|
||||
ag.graph_attr['overlap'] = 'false'
|
||||
ag._layout()
|
||||
#ag.draw('/home/audrey/render.dot', prog='nop')
|
||||
|
||||
for node in ag.nodes_iter():
|
||||
idx = int(node)
|
||||
chart = qnodes[idx]
|
||||
posx, posy = node.attr['pos'].split(',')
|
||||
chart.center = QPointF(float(posx), -float(posy))
|
||||
self.scene().addItem(chart)
|
||||
|
||||
for u, v in ug.edges:
|
||||
lu = lookup[u]
|
||||
lv = lookup[v]
|
||||
if isinstance(lu, ExternNode):
|
||||
towards_start = lu.goes_out
|
||||
towards_end = lu.goes_in
|
||||
elif isinstance(lv, ExternNode):
|
||||
towards_start = lv.goes_in
|
||||
towards_end = lv.goes_out
|
||||
else:
|
||||
towards_end = lv in ng.succ[lu]
|
||||
towards_start = lu in ng.succ[lv]
|
||||
arrow = PropArrow(None, qnodes[u], qnodes[v], towards_start, towards_end)
|
||||
self.scene().addItem(arrow)
|
||||
|
||||
self.nodes = {chart.nav: chart for chart in qnodes if isinstance(chart, PropChartHG)}
|
||||
|
||||
rect = self.scene().itemsBoundingRect()
|
||||
self.resetTransform()
|
||||
self.setSceneRect(rect)
|
||||
self.centerOn(rect.center())
|
||||
|
||||
def label(self, node: RelativeAtomOrGroup) -> str:
|
||||
if node not in self._labels:
|
||||
|
@ -180,6 +129,163 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
self._labels[node] = 'Unknown group'
|
||||
return self._labels[node]
|
||||
|
||||
# private interfaces
|
||||
|
||||
def _animate_layout(self, duration=500):
|
||||
self._layout_animation_controller.setInterval(duration)
|
||||
self._layout_animation_controller.start()
|
||||
self._layout_animation.start()
|
||||
|
||||
def _layout(self, maxiter=1):
|
||||
old_nodes = dict(zip(self.ug_lookup, self.qnodes))
|
||||
|
||||
word_height = 40
|
||||
byte_height = word_height / self.hg.kp.kb._project.arch.bytes
|
||||
|
||||
current_group = self.current_group.am_obj
|
||||
self.ng = self.hg.local_graph(current_group)
|
||||
self.ug = networkx.Graph(self.ng)
|
||||
for edge in self.ug.edges:
|
||||
del self.ug.edges[edge]['prev']
|
||||
del self.ug.edges[edge]['next']
|
||||
if current_group in self.ug.nodes:
|
||||
for adj in list(self.ug.adj[current_group]):
|
||||
enode = ExternNode(adj)
|
||||
self.ug.remove_edge(current_group, adj)
|
||||
self.ug.add_edge(adj, enode)
|
||||
assert len(self.ug.adj[current_group]) == 0
|
||||
self.ug.remove_node(current_group)
|
||||
|
||||
self.ug_lookup = list(self.ug.nodes())
|
||||
self.ug_reverse = {x: i for i, x in enumerate(self.ug_lookup)}
|
||||
networkx.relabel_nodes(self.ug, self.ug_reverse, copy=False)
|
||||
|
||||
self.qnodes = []
|
||||
entering: Set[HGNode] = set()
|
||||
ibr = self.scene().itemsBoundingRect()
|
||||
if ibr.isEmpty():
|
||||
ibr = QRectF(0, -1000, 1000, 1000)
|
||||
for i, node in enumerate(self.ug_lookup):
|
||||
if isinstance(node, ExternNode):
|
||||
qnode = old_nodes.pop(node.adj_to, None)
|
||||
if qnode is None:
|
||||
qnode = HGExtern(None, model=node, hgw=self)
|
||||
entering.add(qnode)
|
||||
qnode.center = rand_point(ibr, i)
|
||||
self.qnodes.append(qnode)
|
||||
else:
|
||||
qnode = old_nodes.pop(node, None)
|
||||
if qnode is None:
|
||||
qnode = PropChartHG(
|
||||
None,
|
||||
prop=self.hg.prop(node),
|
||||
byte_height=byte_height,
|
||||
model=node,
|
||||
hgw=self,
|
||||
label=self.label(node)
|
||||
)
|
||||
entering.add(qnode)
|
||||
pos = self.adding_positions.get(node, None)
|
||||
if pos is None:
|
||||
pos = rand_point(self.scene().itemsBoundingRect(), i)
|
||||
qnode.center = pos
|
||||
self.qnodes.append(qnode)
|
||||
|
||||
for u, v in self.ug.edges:
|
||||
lu = self.ug_lookup[u]
|
||||
lv = self.ug_lookup[v]
|
||||
qu = self.qnodes[u]
|
||||
qv = self.qnodes[v]
|
||||
if lu in qv.edges:
|
||||
assert lv in qu.edges
|
||||
else:
|
||||
assert lv not in qu.edges
|
||||
if isinstance(lu, ExternNode):
|
||||
towards_start = current_group in self.ng.succ[lv]
|
||||
towards_end = lv in self.ng.succ[current_group]
|
||||
elif isinstance(lv, ExternNode):
|
||||
towards_start = lu in self.ng.succ[current_group]
|
||||
towards_end = current_group in self.ng.succ[lu]
|
||||
else:
|
||||
towards_end = lv in self.ng.succ[lu]
|
||||
towards_start = lu in self.ng.succ[lv]
|
||||
arrow = HGArrow(None, self.qnodes[u], self.qnodes[v], towards_start, towards_end)
|
||||
qu.edges[lv] = arrow
|
||||
qv.edges[lu] = arrow
|
||||
self.scene().addItem(arrow)
|
||||
arrow.enter()
|
||||
self.qedges.append(arrow)
|
||||
# the distinction here that we call enter on an arrow manually but the node calls exit on it is weird.
|
||||
# but I think it works.
|
||||
# it is predicated on the idea that edges are never removed unless their nodes are removed. take note!
|
||||
|
||||
for qnode in old_nodes.values():
|
||||
qnode.exit()
|
||||
for qnode in entering:
|
||||
self.scene().addItem(qnode)
|
||||
qnode.enter()
|
||||
|
||||
self.ag = networkx.drawing.nx_agraph.to_agraph(self.ug)
|
||||
self._iterate_layout(maxiter)
|
||||
|
||||
def _iterate_layout(self, maxiter=1):
|
||||
if self._layout_lock.acquire(blocking=False):
|
||||
self._layout_event_occurring = True
|
||||
try:
|
||||
for node in self.ag.nodes_iter():
|
||||
idx = int(node)
|
||||
qnode = self.qnodes[idx]
|
||||
qnode.set_agraph_node_properties(node)
|
||||
node.attr['pos'] = f'{qnode.x()},{-qnode.y()}'
|
||||
node.attr['pin'] = str(qnode.pinned).lower()
|
||||
for edge in self.ag.edges_iter():
|
||||
edge.attr['len'] = self.layout_social_distancing
|
||||
self.ag.graph_attr['overlap'] = self.layout_overlap
|
||||
self.ag.graph_attr['maxiter'] = maxiter
|
||||
self.ag.graph_attr['normalize'] = 'false'
|
||||
self.ag.graph_attr['inputscale'] = '0'
|
||||
self.ag._layout(self.layout_prog)
|
||||
|
||||
for node in self.ag.nodes_iter():
|
||||
idx = int(node)
|
||||
chart = self.qnodes[idx]
|
||||
posx, posy = node.attr['pos'].split(',')
|
||||
newpos = QPointF(float(posx), -float(posy))
|
||||
if not chart.pinned:
|
||||
chart.center = newpos
|
||||
|
||||
for edge in self.qedges:
|
||||
edge.layout()
|
||||
|
||||
rect = None
|
||||
for qnode in self.qnodes:
|
||||
br = qnode.boundingRect()
|
||||
br.translate(qnode.x(), qnode.y())
|
||||
rect = rect.united(br) if rect is not None else br
|
||||
if rect is None:
|
||||
rect = QRectF()
|
||||
rect = rect.marginsAdded(QMarginsF(SCENE_MARGIN, SCENE_MARGIN, SCENE_MARGIN, SCENE_MARGIN))
|
||||
self.setSceneRect(rect)
|
||||
finally:
|
||||
self._layout_event_occurring = False
|
||||
self._layout_lock.release()
|
||||
else:
|
||||
l.warning("Layout lock conflict. Is the application lagging?")
|
||||
|
||||
# qt overrides
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Z:
|
||||
self._layout_animation.start()
|
||||
event.accept()
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
if event.key() == Qt.Key_Z:
|
||||
self._layout_animation.stop()
|
||||
event.accept()
|
||||
super().keyReleaseEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super().mouseDoubleClickEvent(event)
|
||||
if not event.isAccepted():
|
||||
|
@ -189,13 +295,75 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
self.current_group.am_event()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self._mouse_is_down = True
|
||||
super().mousePressEvent(event)
|
||||
item = self.itemAt(event.pos())
|
||||
if item is None:
|
||||
event.accept()
|
||||
self.select(None)
|
||||
|
||||
class PropNode(QGraphicsItem):
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._mouse_is_down = False
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
# objectcontainer event handlers
|
||||
|
||||
def _on_change_group(self, **kwargs):
|
||||
self._layout(600)
|
||||
self.resetTransform()
|
||||
self.centerOn(self.sceneRect().center())
|
||||
|
||||
def _on_selection_changed(self, src=None, **kwargs):
|
||||
if src == 'qt':
|
||||
return
|
||||
self._selection_event_occurring = True
|
||||
self.scene().clearSelection()
|
||||
for node in self.selected_nodes:
|
||||
qnode = self.qnodes[self.ug_reverse[node]]
|
||||
qnode.setSelected(True)
|
||||
self._selection_event_occurring = False
|
||||
|
||||
class AnimatableItem(QGraphicsItem):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.timeline: Optional[QTimeLine] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _start_animation(self, animation, duration, finished=None, ease=QEasingCurve.Linear):
|
||||
if self.timeline is not None:
|
||||
self.timeline.stop()
|
||||
self.timeline = QTimeLine(duration, None)
|
||||
self.timeline.setEasingCurve(ease)
|
||||
self.timeline.valueChanged.connect(animation)
|
||||
if finished is not None:
|
||||
self.timeline.finished.connect(finished)
|
||||
self.timeline.start()
|
||||
animation(0.)
|
||||
|
||||
class HGNode(AnimatableItem):
|
||||
def __init__(self, *args, model: RelativeAtomOrGroupOrExtern, hgw: HierarchicalGraphWidget, **kwargs):
|
||||
self.model = model
|
||||
self.edges: Dict[RelativeAtomOrGroupOrExtern, HGArrow] = {}
|
||||
self.hgw = hgw
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setFlag(QGraphicsItem.ItemIsSelectable)
|
||||
self.setFlag(QGraphicsItem.ItemIsMovable)
|
||||
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
|
||||
|
||||
# public interfaces
|
||||
|
||||
def exit(self, duration=250):
|
||||
for edge in self.edges.values():
|
||||
edge.orient(self, away=True)
|
||||
edge.exit(duration)
|
||||
edge.start.edges.pop(self.model)
|
||||
self._start_animation(self._animate_exit, duration, self._finish_exit)
|
||||
|
||||
def enter(self, duration=250):
|
||||
for edge in self.edges.values():
|
||||
edge.orient(self, away=True)
|
||||
self._start_animation(self._animate_enter, duration)
|
||||
|
||||
@property
|
||||
def pinned(self):
|
||||
return self.isSelected() and self.hgw._mouse_is_down
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
raise NotImplementedError
|
||||
|
@ -210,9 +378,45 @@ class PropNode(QGraphicsItem):
|
|||
def set_agraph_node_properties(self, node: pygraphviz.Node):
|
||||
raise NotImplementedError
|
||||
|
||||
# private interfaces
|
||||
|
||||
class PropChart(PropNode):
|
||||
def __init__(self, parent, prop: Prop, max_width=200., default_unit=10., byte_height=10., margin_x=15., margin_y=5., padding_left=5.):
|
||||
def _animate_enter(self, value):
|
||||
self.setOpacity(value)
|
||||
self.setScale(value)
|
||||
|
||||
def _animate_exit(self, value):
|
||||
self.setOpacity(1 - value)
|
||||
self.setScale(1 - value)
|
||||
|
||||
def _finish_exit(self):
|
||||
self.scene().removeItem(self)
|
||||
|
||||
# qt overrides
|
||||
|
||||
def itemChange(self, change, value):
|
||||
if change == QGraphicsItem.ItemSelectedHasChanged:
|
||||
if not self.hgw._selection_event_occurring:
|
||||
if value:
|
||||
self.hgw.selected_nodes.add(self.model)
|
||||
else:
|
||||
self.hgw.selected_nodes.discard(self.model)
|
||||
|
||||
self.hgw.selected_nodes.am_event(src='qt')
|
||||
elif change == QGraphicsItem.ItemPositionHasChanged:
|
||||
if not self.hgw._layout_event_occurring:
|
||||
for edge in self.edges.values():
|
||||
edge.layout()
|
||||
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
event.accept()
|
||||
self.hgw.navigate(self.model)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
|
||||
class PropChart(QGraphicsItem):
|
||||
def __init__(self, *args, prop: Prop, max_width=200., default_unit=10., byte_height=10., margin_x=15., margin_y=5., padding_left=5., **kwargs):
|
||||
self.prop = prop
|
||||
self.max_width = max_width
|
||||
self.default_unit = default_unit
|
||||
|
@ -226,60 +430,16 @@ class PropChart(PropNode):
|
|||
self.objects = []
|
||||
self.object_bg: Optional[QGraphicsRectItem] = None
|
||||
|
||||
super().__init__(parent)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout_marks()
|
||||
|
||||
def set_agraph_node_properties(self, node: pygraphviz.Node):
|
||||
node.attr['width'] = self.width / 72
|
||||
node.attr['height'] = self.height / 72
|
||||
node.attr['shape'] = 'box'
|
||||
|
||||
def clip_edge(self, extent: QPointF) -> QPointF:
|
||||
center_start = self.center
|
||||
center_end = extent
|
||||
delta_x = center_end.x() - center_start.x()
|
||||
delta_y = center_end.y() - center_start.y()
|
||||
slope = delta_y / delta_x if delta_x else float('inf')
|
||||
|
||||
start_corner_slope = self.height / self.width
|
||||
start_horiz = start_corner_slope > slope > -start_corner_slope
|
||||
|
||||
if start_horiz:
|
||||
start_right = center_end.x() > center_start.x()
|
||||
if start_right:
|
||||
start_ex = self.width / 2
|
||||
else:
|
||||
start_ex = -self.width / 2
|
||||
start_ey = start_ex * slope
|
||||
else:
|
||||
start_bottom = center_end.y() > center_start.y()
|
||||
if start_bottom:
|
||||
start_ey = self.height / 2
|
||||
else:
|
||||
start_ey = -self.height / 2
|
||||
start_ex = start_ey / slope
|
||||
|
||||
return QPointF(center_start.x() + start_ex, center_start.y() + start_ey)
|
||||
|
||||
def boundingRect(self):
|
||||
return QRectF(0, 0, self.width, self.height)
|
||||
|
||||
def paint(self, painter, option, widget=...):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center(self) -> QPointF:
|
||||
pos = self.pos()
|
||||
pos.setX(pos.x() + self.width / 2)
|
||||
pos.setY(pos.y() + self.height / 2)
|
||||
return pos
|
||||
|
||||
@center.setter
|
||||
def center(self, v: QPointF):
|
||||
self.setX(v.x() - self.width / 2)
|
||||
self.setY(v.y() - self.height / 2)
|
||||
|
||||
def _layout_marks(self):
|
||||
data = [(offset, kind, size, count) for offset, v1 in self.prop.struct_data.items() for size, v2 in v1.items() for kind, count in v2.items()]
|
||||
data.sort()
|
||||
|
@ -332,21 +492,23 @@ class PropChart(PropNode):
|
|||
tick.setPos(QPointF(self.margin_x - tick.boundingRect().width(), self.margin_y + ypos * self.byte_height))
|
||||
self.objects.append(tick)
|
||||
|
||||
class PropChartHG(PropChart):
|
||||
def __init__(self, *args, nav: RelativeAtomOrGroup, hgw: HierarchicalGraphWidget, label: str, **kwargs):
|
||||
self.nav = nav
|
||||
self.hgw = hgw
|
||||
class PropChartHG(HGNode, PropChart):
|
||||
def __init__(self, *args, label: str, **kwargs):
|
||||
self.label = label
|
||||
self.object_label = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
event.accept()
|
||||
self.hgw.navigate(self.nav)
|
||||
@property
|
||||
def center(self) -> QPointF:
|
||||
pos = self.pos()
|
||||
pos.setX(pos.x() + self.width / 2)
|
||||
pos.setY(pos.y() + self.height / 2)
|
||||
return pos
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
event.accept()
|
||||
self.hgw.select(self.nav)
|
||||
@center.setter
|
||||
def center(self, v: QPointF):
|
||||
self.setX(v.x() - self.width / 2)
|
||||
self.setY(v.y() - self.height / 2)
|
||||
|
||||
def _layout_marks(self):
|
||||
lbl_height = 20
|
||||
|
@ -366,21 +528,55 @@ class PropChartHG(PropChart):
|
|||
self.object_label.setFont(TEXT_FONT)
|
||||
self.objects.append(self.object_label)
|
||||
|
||||
self.setTransformOriginPoint(self.center)
|
||||
|
||||
def paint(self, painter, option, widget=...):
|
||||
if self.nav is self.hgw.current_node.am_obj:
|
||||
if self.isSelected():
|
||||
self.object_bg.setPen(QPen(BOX_BORDER_SELECTED_COLOR, BOX_BORDER_SELECTED_WIDTH))
|
||||
else:
|
||||
self.object_bg.setPen(QPen(BOX_BORDER_COLOR, BOX_BORDER_WIDTH))
|
||||
|
||||
super().paint(painter, option, widget)
|
||||
|
||||
class PropExtern(PropNode):
|
||||
def __init__(self, parent, radius=28.):
|
||||
def clip_edge(self, extent: QPointF) -> QPointF:
|
||||
center_start = self.center
|
||||
center_end = extent
|
||||
delta_x = center_end.x() - center_start.x()
|
||||
delta_y = center_end.y() - center_start.y()
|
||||
slope = delta_y / delta_x if delta_x else float('inf')
|
||||
|
||||
start_corner_slope = self.height / self.width
|
||||
start_horiz = start_corner_slope > slope > -start_corner_slope
|
||||
|
||||
if start_horiz:
|
||||
start_right = center_end.x() > center_start.x()
|
||||
if start_right:
|
||||
start_ex = self.width / 2
|
||||
else:
|
||||
start_ex = -self.width / 2
|
||||
start_ey = start_ex * slope
|
||||
else:
|
||||
start_bottom = center_end.y() > center_start.y()
|
||||
if start_bottom:
|
||||
start_ey = self.height / 2
|
||||
else:
|
||||
start_ey = -self.height / 2
|
||||
start_ex = start_ey / slope
|
||||
|
||||
return QPointF(center_start.x() + start_ex, center_start.y() + start_ey)
|
||||
|
||||
def set_agraph_node_properties(self, node: pygraphviz.Node):
|
||||
node.attr['width'] = self.width / DPI
|
||||
node.attr['height'] = self.height / DPI
|
||||
node.attr['shape'] = 'box'
|
||||
|
||||
class HGExtern(HGNode):
|
||||
def __init__(self, *args, radius=28., **kwargs):
|
||||
self.radius = radius
|
||||
self.objects = []
|
||||
self.object_bg: Optional[QGraphicsEllipseItem] = None
|
||||
self.object_text = None
|
||||
super().__init__(parent)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._layout()
|
||||
|
||||
|
@ -409,7 +605,7 @@ class PropExtern(PropNode):
|
|||
return self.center + edge_vector
|
||||
|
||||
def set_agraph_node_properties(self, node: pygraphviz.Node):
|
||||
node.attr['width'] = self.radius * 2 / 72
|
||||
node.attr['width'] = self.radius * 2 / DPI
|
||||
node.attr['shape'] = 'circle'
|
||||
|
||||
def boundingRect(self) -> QRectF:
|
||||
|
@ -421,8 +617,8 @@ class PropExtern(PropNode):
|
|||
def paint(self, painter, option, widget=...):
|
||||
pass
|
||||
|
||||
class PropArrow(QGraphicsItem):
|
||||
def __init__(self, parent, start: PropNode, end: PropNode, toward_start: bool, toward_end: bool, stroke=3., arrow_size=8.):
|
||||
class HGArrow(AnimatableItem):
|
||||
def __init__(self, parent, start: HGNode, end: HGNode, toward_start: bool, toward_end: bool, stroke=3., arrow_size=8.):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.toward_start = toward_start
|
||||
|
@ -432,7 +628,23 @@ class PropArrow(QGraphicsItem):
|
|||
self._percentage = 1.
|
||||
|
||||
super().__init__(parent)
|
||||
self.layout()
|
||||
|
||||
def enter(self, duration=250):
|
||||
self._start_animation(self._animate_enter, duration)
|
||||
|
||||
def _animate_enter(self, value):
|
||||
self.setOpacity(value)
|
||||
self.percentage = value
|
||||
|
||||
def exit(self, duration=250):
|
||||
self._start_animation(self._animate_exit, duration, self._finish_exit)
|
||||
|
||||
def _animate_exit(self, value):
|
||||
self.setOpacity(1 - value)
|
||||
self.percentage = 1 - value
|
||||
|
||||
def _finish_exit(self):
|
||||
self.scene().removeItem(self)
|
||||
|
||||
@property
|
||||
def percentage(self):
|
||||
|
@ -455,15 +667,18 @@ class PropArrow(QGraphicsItem):
|
|||
self.rel_end = end_pt - start_pt
|
||||
|
||||
|
||||
def orient(self, start: PropChart):
|
||||
if start is self.start:
|
||||
pass
|
||||
elif start is self.end:
|
||||
def orient(self, anchor: HGNode, away=False):
|
||||
if anchor is self.start:
|
||||
anchor_start = True
|
||||
elif anchor is self.end:
|
||||
anchor_start = False
|
||||
else:
|
||||
raise ValueError("Orienting from node which is not associated with edge")
|
||||
|
||||
if away ^ (not anchor_start):
|
||||
self.start, self.end = self.end, self.start
|
||||
self.toward_start, self.toward_end = self.toward_end, self.toward_start
|
||||
self.layout()
|
||||
else:
|
||||
raise ValueError("Orienting from node which is not associated with edge")
|
||||
|
||||
def boundingRect(self):
|
||||
x = 0
|
||||
|
@ -543,3 +758,9 @@ def rotate(pt: QPointF, angle: float) -> QPointF:
|
|||
x = pt.x()
|
||||
y = pt.y()
|
||||
return QPointF(x * cost - y * sint, x * sint + y * cost)
|
||||
|
||||
def rand_point(range: QRectF, seed=None) -> QPointF:
|
||||
r = random.Random(seed)
|
||||
x = r.random()
|
||||
y = r.random()
|
||||
return QPointF(range.width() * x + range.x(), range.height() * y + range.y())
|
||||
|
|
Loading…
Reference in New Issue