TO THE PLAAAAAAAAACE WHERE I BELONGGGGGGGGGGGGGGGG
(frontier expansion works)
This commit is contained in:
parent
a3ccf68845
commit
e146aaf746
|
@ -63,7 +63,7 @@ class HierarchicalGraph(RelativeAtomGraph):
|
|||
if adj in self.frontier and adj not in queue and predicate(adj):
|
||||
queue.add(adj)
|
||||
|
||||
def expand_by_key(self, seed: RelativeAtomOrGroup, group: RelativeAtomGroup, key: Callable[[RelativeAtom], Any]):
|
||||
def expand_by_key(self, seed: RelativeAtomOrGroup, group: RelativeAtomGroup, key: Callable[[RelativeAtom], Any]) -> Dict[Any, RelativeAtomGroup]:
|
||||
queues = defaultdict(set)
|
||||
for atom in self.atoms(seed):
|
||||
if atom in self.frontier:
|
||||
|
@ -73,10 +73,14 @@ class HierarchicalGraph(RelativeAtomGraph):
|
|||
if adj in self.frontier:
|
||||
queues[key(adj)].add(adj)
|
||||
|
||||
result = {}
|
||||
for keyed, queue in queues.items():
|
||||
subgroup = self.create_group([], group)
|
||||
result[keyed] = subgroup
|
||||
self.expand_while(queue, subgroup, lambda atom: key(atom) == keyed)
|
||||
|
||||
return result
|
||||
|
||||
def atoms(self, node: RelativeAtomOrGroup) -> Iterable[RelativeAtom]:
|
||||
if isinstance(node, RelativeAtomGroup):
|
||||
for child in node.children:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from itertools import chain
|
||||
from typing import Tuple, Optional, Union, Dict, List, Set
|
||||
from collections import defaultdict
|
||||
from math import sqrt, sin, cos, pi
|
||||
|
@ -34,6 +35,11 @@ BOX_BORDER_COLOR = QColor(0x60, 0x60, 0x60)
|
|||
BOX_BORDER_SELECTED_COLOR = QColor(0x60, 0x60, 0xff)
|
||||
BOX_BORDER_WIDTH = 1
|
||||
BOX_BORDER_SELECTED_WIDTH = 2
|
||||
PLUS_COLOR = QColor(0x40, 0xc0, 0xb0)
|
||||
PLUS_ICON_COLOR = QColor(0x50, 0xd0, 0xf0)
|
||||
PLUS_HOVERED_COLOR = QColor(0x40, 0xc0, 0xc0)
|
||||
PLUS_BORDER_COLOR = QColor(0x30, 0xc0, 0x50)
|
||||
PLUS_ICON_FONT = QFont("default", 12)
|
||||
|
||||
SCENE_MARGIN = 200
|
||||
DPI = 72
|
||||
|
@ -49,12 +55,12 @@ def kind_to_color(kind: DataKind) -> QColor:
|
|||
raise ValueError(kind)
|
||||
|
||||
class HierarchicalGraphView(BaseView):
|
||||
def __init__(self, instance, default_docking_position, hg: HierarchicalGraph, *args, current_group: Optional[RelativeAtomGroup]=None, **kwargs):
|
||||
def __init__(self, instance, default_docking_position, hg: HierarchicalGraph, faux_frontier: Set[RelativeAtomOrGroup], *args, current_group: Optional[RelativeAtomGroup]=None, **kwargs):
|
||||
super().__init__("typetapper", instance, default_docking_position, *args, **kwargs)
|
||||
self.base_caption = "TypeTapper"
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
self.graph = HierarchicalGraphWidget(self, instance, hg, current_group)
|
||||
self.graph = HierarchicalGraphWidget(self, instance, hg, current_group, faux_frontier)
|
||||
layout.addWidget(self.graph)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
@ -65,34 +71,40 @@ class ExternNode:
|
|||
RelativeAtomOrGroupOrExtern = Union[RelativeAtomOrGroup, ExternNode]
|
||||
|
||||
class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
||||
def __init__(self, parent, instance: Instance, hg: HierarchicalGraph, current_group: Optional[RelativeAtomGroup]=None):
|
||||
def __init__(self, parent, instance: Instance, hg: HierarchicalGraph, current_group: Optional[RelativeAtomGroup]=None, faux_frontier: Optional[Set[RelativeAtomOrGroup]]=None):
|
||||
self.hg = hg
|
||||
self.current_group: Union[ObjectContainer, RelativeAtomGroup] = ObjectContainer(hg.root_group if current_group is None else current_group)
|
||||
self.selected_nodes: Union[ObjectContainer, Set[RelativeAtomOrGroupOrExtern]] = ObjectContainer(set())
|
||||
self.instance = instance
|
||||
self._labels = {}
|
||||
|
||||
self.qnodes: List[HGNode] = []
|
||||
self.qedges: List[HGArrow] = []
|
||||
self.expansion_qnodes: List[HGNode] = []
|
||||
self.expansion_qedges: List[HGArrow] = []
|
||||
self.expansion_models: List[RelativeAtomOrGroup] = []
|
||||
|
||||
self.layout_prog = 'neato'
|
||||
self.layout_social_distancing = 200 / DPI
|
||||
self.layout_overlap = 'vpsc'
|
||||
|
||||
self._labels = {}
|
||||
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
|
||||
self.faux_frontier = faux_frontier or set()
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setScene(QGraphicsScene())
|
||||
self.scene().selectionChanged.connect(self._on_selection_changed_internal)
|
||||
self.current_group.am_subscribe(self._on_change_group)
|
||||
self.selected_nodes.am_subscribe(self._on_selection_changed)
|
||||
self._layout_animation.setInterval(16)
|
||||
|
@ -121,16 +133,118 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
if isinstance(node, RelativeAtom):
|
||||
self._labels[node] = str(node.atom)
|
||||
else:
|
||||
self._labels[node] = 'Unknown group'
|
||||
funcs = {self.hg.kp.atom_to_function(atom) for atom in self.hg.atoms(node) if atom not in self.hg.frontier}
|
||||
if len(funcs) == 1:
|
||||
func = funcs.pop()
|
||||
self._labels[node] = self.instance.kb.functions[func].name
|
||||
else:
|
||||
self._labels[node] = 'Unknown group'
|
||||
if func in self.instance.kb.functions:
|
||||
self._labels[node] = self.instance.kb.functions[func].name
|
||||
return self._labels[node]
|
||||
|
||||
def can_expand(self, model: RelativeAtomOrGroupOrExtern):
|
||||
if model not in self.ng.nodes:
|
||||
return False
|
||||
return any(adj in self.faux_frontier for adj in chain(self.ng.succ[model], self.ng.pred[model]))
|
||||
|
||||
def begin_expand(self, model: RelativeAtomOrGroup):
|
||||
qnode = self.qnodes[self.ug_reverse[model]]
|
||||
self.scene().clearSelection()
|
||||
qnode.setSelected(True)
|
||||
for other_qnode in self.qnodes:
|
||||
other_qnode.setAcceptHoverEvents(False)
|
||||
other_qnode.setAcceptedMouseButtons(Qt.MouseButton.NoButton | 0)
|
||||
if qnode is other_qnode:
|
||||
other_qnode.setOpacity(0.8)
|
||||
else:
|
||||
other_qnode.setOpacity(0.4)
|
||||
for edge in self.qedges:
|
||||
edge.setOpacity(0.4)
|
||||
|
||||
byte_height = self._byte_height
|
||||
self.expansion_models = [adj for adj in chain(self.ng.succ[model], self.ng.pred[model]) if adj in self.faux_frontier]
|
||||
self.expansion_qnodes = [PropChartHG(
|
||||
None,
|
||||
prop=self.hg.prop(other_model),
|
||||
byte_height=byte_height,
|
||||
model=other_model,
|
||||
hgw=self,
|
||||
label=self.label(other_model)
|
||||
) for other_model in self.expansion_models]
|
||||
self.expansion_qedges = [HGArrow(
|
||||
None,
|
||||
qnode,
|
||||
other_qnode,
|
||||
other_model in self.ng.succ[model],
|
||||
other_model in self.ng.pred[model]
|
||||
) for other_qnode, other_model in zip(self.expansion_qnodes, self.expansion_models)]
|
||||
|
||||
tmp_ag = pygraphviz.AGraph()
|
||||
tmp_ag.add_node('x')
|
||||
qnode.set_agraph_node_properties(tmp_ag.get_node('x'))
|
||||
for idx, other_qnode in enumerate(self.expansion_qnodes):
|
||||
tmp_ag.add_node(idx)
|
||||
other_qnode.set_agraph_node_properties(tmp_ag.get_node(idx))
|
||||
tmp_ag.add_edge(idx, 'x', len=self.layout_social_distancing)
|
||||
tmp_ag.graph_attr['overlap'] = 'false'
|
||||
|
||||
tmp_ag._layout()
|
||||
origin = parse_pos(tmp_ag.get_node('x').attr['pos'])
|
||||
for idx, other_qnode in enumerate(self.expansion_qnodes):
|
||||
self.scene().addItem(other_qnode)
|
||||
other_qnode.center = parse_pos(tmp_ag.get_node(idx).attr['pos']) - origin + qnode.center
|
||||
other_qnode.enter()
|
||||
other_qnode.set_expandable(False)
|
||||
|
||||
for qedge in self.expansion_qedges:
|
||||
self.scene().addItem(qedge)
|
||||
qedge.layout()
|
||||
qedge.enter()
|
||||
|
||||
@property
|
||||
def is_expanding(self):
|
||||
return bool(self.expansion_qnodes)
|
||||
|
||||
def end_expand(self, accepted_qnode: Optional['PropChartHG']=None):
|
||||
for qnode in self.qnodes:
|
||||
qnode.setAcceptHoverEvents(True)
|
||||
qnode.setAcceptedMouseButtons(Qt.MouseButton.AllButtons | 0)
|
||||
qnode.setOpacity(1)
|
||||
for edge in self.qedges:
|
||||
edge.setOpacity(1)
|
||||
|
||||
for qnode in self.expansion_qnodes:
|
||||
if qnode is accepted_qnode:
|
||||
model = accepted_qnode.model
|
||||
self.ug_reverse[model] = len(self.ug_lookup)
|
||||
self.qnodes.append(accepted_qnode)
|
||||
self.ug_lookup.append(model)
|
||||
else:
|
||||
qnode.exit()
|
||||
|
||||
for qedge in self.expansion_qedges:
|
||||
if qedge.start is accepted_qnode or qedge.end is accepted_qnode:
|
||||
self.qedges.append(qedge)
|
||||
qedge.start.edges[qedge.end.model] = qedge
|
||||
qedge.end.edges[qedge.start.model] = qedge
|
||||
else:
|
||||
qedge.exit()
|
||||
self.expansion_qedges.clear()
|
||||
self.expansion_qnodes.clear()
|
||||
self.expansion_models.clear()
|
||||
|
||||
if accepted_qnode:
|
||||
self.faux_frontier.remove(accepted_qnode.model)
|
||||
newgroups = self.hg.expand_by_key(accepted_qnode.model, self.current_group.am_obj, self.hg.kp.atom_to_function)
|
||||
self.faux_frontier.update(newgroups.values())
|
||||
self._layout(0)
|
||||
|
||||
# private interfaces
|
||||
|
||||
@property
|
||||
def _byte_height(self):
|
||||
word_height = 40
|
||||
return word_height / self.hg.kp.kb._project.arch.bytes
|
||||
|
||||
def _animate_layout(self, duration=500):
|
||||
self._layout_animation_controller.setInterval(duration)
|
||||
self._layout_animation_controller.start()
|
||||
|
@ -138,13 +252,14 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
|
||||
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
|
||||
byte_height = self._byte_height
|
||||
|
||||
current_group = self.current_group.am_obj
|
||||
self.ng = self.hg.local_graph(current_group)
|
||||
self.ug = networkx.Graph(self.ng)
|
||||
for model in list(self.ug.nodes):
|
||||
if model in self.faux_frontier:
|
||||
self.ug.remove_node(model)
|
||||
for edge in self.ug.edges:
|
||||
del self.ug.edges[edge]['prev']
|
||||
del self.ug.edges[edge]['next']
|
||||
|
@ -185,12 +300,12 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
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
|
||||
qnode.center = rand_point(ibr, i)
|
||||
self.qnodes.append(qnode)
|
||||
|
||||
for model, qnode in zip(self.ug_lookup, self.qnodes):
|
||||
qnode.set_expandable(self.can_expand(model))
|
||||
|
||||
for u, v in self.ug.edges:
|
||||
lu = self.ug_lookup[u]
|
||||
lv = self.ug_lookup[v]
|
||||
|
@ -229,6 +344,8 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
self._iterate_layout(maxiter)
|
||||
|
||||
def _iterate_layout(self, maxiter=1):
|
||||
if not maxiter:
|
||||
return
|
||||
if self._layout_lock.acquire(blocking=False):
|
||||
self._layout_event_occurring = True
|
||||
try:
|
||||
|
@ -236,7 +353,7 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
idx = int(node)
|
||||
qnode = self.qnodes[idx]
|
||||
qnode.set_agraph_node_properties(node)
|
||||
node.attr['pos'] = f'{qnode.x()},{-qnode.y()}'
|
||||
node.attr['pos'] = render_pos(qnode.center)
|
||||
node.attr['pin'] = str(qnode.pinned).lower()
|
||||
for edge in self.ag.edges_iter():
|
||||
edge.attr['len'] = self.layout_social_distancing
|
||||
|
@ -249,8 +366,7 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
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))
|
||||
newpos = parse_pos(node.attr['pos'])
|
||||
if not chart.pinned:
|
||||
chart.center = newpos
|
||||
|
||||
|
@ -275,7 +391,7 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
# qt overrides
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == Qt.Key_Z:
|
||||
if event.key() == Qt.Key_Z and not self.is_expanding:
|
||||
self._layout_animation.start()
|
||||
event.accept()
|
||||
super().keyPressEvent(event)
|
||||
|
@ -319,6 +435,23 @@ class HierarchicalGraphWidget(QZoomableDraggableGraphicsView):
|
|||
qnode.setSelected(True)
|
||||
self._selection_event_occurring = False
|
||||
|
||||
def _on_selection_changed_internal(self):
|
||||
if self.is_expanding:
|
||||
items = self.scene().selectedItems()
|
||||
if not items:
|
||||
self.end_expand()
|
||||
elif len(items) == 1:
|
||||
qnode = items[0]
|
||||
assert isinstance(qnode, PropChartHG)
|
||||
if qnode.model not in self.ug_reverse:
|
||||
self.end_expand(qnode)
|
||||
else:
|
||||
l.error("How'd you select more than one item during expansion")
|
||||
elif not self._selection_event_occurring:
|
||||
self.selected_nodes.am_obj = {model for model, qnode in zip(self.ug_lookup, self.qnodes) if qnode.isSelected()}
|
||||
self.selected_nodes.am_event(src='qt')
|
||||
|
||||
|
||||
class AnimatableItem(QGraphicsItem):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.timeline: Optional[QTimeLine] = None
|
||||
|
@ -352,7 +485,7 @@ class HGNode(AnimatableItem):
|
|||
for edge in self.edges.values():
|
||||
edge.orient(self, away=True)
|
||||
edge.exit(duration)
|
||||
edge.start.edges.pop(self.model)
|
||||
edge.start.edges.pop(self.model, None)
|
||||
self._start_animation(self._animate_exit, duration, self._finish_exit)
|
||||
|
||||
def enter(self, duration=250):
|
||||
|
@ -364,6 +497,9 @@ class HGNode(AnimatableItem):
|
|||
def pinned(self):
|
||||
return self.isSelected() and self.hgw._mouse_is_down
|
||||
|
||||
def set_expandable(self, expandable: bool):
|
||||
pass
|
||||
|
||||
@property
|
||||
def center(self):
|
||||
raise NotImplementedError
|
||||
|
@ -394,15 +530,7 @@ class HGNode(AnimatableItem):
|
|||
# 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 change == QGraphicsItem.ItemPositionHasChanged:
|
||||
if not self.hgw._layout_event_occurring:
|
||||
for edge in self.edges.values():
|
||||
edge.layout()
|
||||
|
@ -496,6 +624,7 @@ class PropChartHG(HGNode, PropChart):
|
|||
def __init__(self, *args, label: str, **kwargs):
|
||||
self.label = label
|
||||
self.object_label = None
|
||||
self.object_btn: Optional[PlusButton] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -527,9 +656,20 @@ class PropChartHG(HGNode, PropChart):
|
|||
self.object_label.setPos(QPointF(self.margin_x, self.margin_y))
|
||||
self.object_label.setFont(TEXT_FONT)
|
||||
self.objects.append(self.object_label)
|
||||
self.object_btn = PlusButton(self, self.begin_expand, 16)
|
||||
self.object_btn.setPos(self.width - 18, 2)
|
||||
|
||||
self.setTransformOriginPoint(self.center)
|
||||
|
||||
def set_expandable(self, expandable: bool):
|
||||
self.object_btn.setVisible(expandable)
|
||||
|
||||
def begin_expand(self):
|
||||
if self.timeline.state() == QTimeLine.NotRunning:
|
||||
# horrifying
|
||||
QTimer.singleShot(10, lambda: self.hgw.begin_expand(self.model))
|
||||
return True
|
||||
|
||||
def paint(self, painter, option, widget=...):
|
||||
if self.isSelected():
|
||||
self.object_bg.setPen(QPen(BOX_BORDER_SELECTED_COLOR, BOX_BORDER_SELECTED_WIDTH))
|
||||
|
@ -612,10 +752,15 @@ class HGExtern(HGNode):
|
|||
return QRectF(-self.radius, -self.radius, self.radius*2, self.radius*2)
|
||||
|
||||
def shape(self) -> QPainterPath:
|
||||
return self.object_bg.shape().translated(-self.radius, -self.radius)
|
||||
result = QPainterPath()
|
||||
result.addEllipse(0, 0, self.radius*2, self.radius*2)
|
||||
return result
|
||||
|
||||
def paint(self, painter, option, widget=...):
|
||||
pass
|
||||
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))
|
||||
|
||||
class HGArrow(AnimatableItem):
|
||||
def __init__(self, parent, start: HGNode, end: HGNode, toward_start: bool, toward_end: bool, stroke=3., arrow_size=8.):
|
||||
|
@ -727,6 +872,40 @@ class HGArrow(AnimatableItem):
|
|||
path.closeSubpath()
|
||||
return path
|
||||
|
||||
class PlusButton(QGraphicsItem):
|
||||
def __init__(self, parent, callback, size):
|
||||
self.callback = callback
|
||||
self.size = size
|
||||
super().__init__(parent)
|
||||
|
||||
self.object_bg = QGraphicsRectItem(0, 0, size, size, self)
|
||||
self.object_bg.setBrush(PLUS_COLOR)
|
||||
self.object_bg.setPen(QPen(PLUS_BORDER_COLOR, BOX_BORDER_WIDTH))
|
||||
self.object_icon = QGraphicsSimpleTextItem("+", self)
|
||||
self.object_icon.setFont(PLUS_ICON_FONT)
|
||||
self.object_icon.setBrush(PLUS_ICON_COLOR)
|
||||
self.object_icon.setPos(self.object_bg.boundingRect().center() - self.object_icon.boundingRect().center())
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
def paint(self, painter, option, widget=...):
|
||||
pass
|
||||
|
||||
def boundingRect(self):
|
||||
return QRectF(0, 0, self.size, self.size)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self.callback():
|
||||
event.ignore()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def hoverEnterEvent(self, event):
|
||||
self.object_bg.setBrush(PLUS_HOVERED_COLOR)
|
||||
super().hoverEnterEvent(event)
|
||||
|
||||
def hoverLeaveEvent(self, event):
|
||||
self.object_bg.setBrush(PLUS_COLOR)
|
||||
super().hoverLeaveEvent(event)
|
||||
|
||||
def arrowhead_vectors(start: QPointF, end: QPointF, stem_width: float, flare_width: float) -> Tuple[QPointF, QPointF, QPointF, QPointF, QPointF, QPointF]:
|
||||
direction = end - start
|
||||
norm = normalized(direction)
|
||||
|
@ -764,3 +943,10 @@ def rand_point(range: QRectF, seed=None) -> QPointF:
|
|||
x = r.random()
|
||||
y = r.random()
|
||||
return QPointF(range.width() * x + range.x(), range.height() * y + range.y())
|
||||
|
||||
def parse_pos(pos: str) -> QPointF:
|
||||
posx, posy = pos.split(',')
|
||||
return QPointF(float(posx), -float(posy))
|
||||
|
||||
def render_pos(pt: QPointF) -> str:
|
||||
return f'{pt.x()},{-pt.y()}'
|
||||
|
|
|
@ -44,8 +44,8 @@ class TypeTapper(BasePlugin):
|
|||
func = self.kp.atom_to_function(node)
|
||||
func_group = hg.create_group(start_relatoms, hg.root_group)
|
||||
hg.expand_while(start_relatoms, func_group, lambda atom: self.kp.atom_to_function(atom) == func)
|
||||
hg.expand_by_key(func_group, hg.root_group, self.kp.atom_to_function)
|
||||
newgroups = hg.expand_by_key(func_group, hg.root_group, self.kp.atom_to_function)
|
||||
|
||||
view = HierarchicalGraphView(self.workspace.main_instance, "center", hg)
|
||||
view = HierarchicalGraphView(self.workspace.main_instance, "center", hg, set(newgroups.values()))
|
||||
self.workspace.add_view(view)
|
||||
self.workspace.raise_view(view)
|
||||
|
|
Loading…
Reference in New Issue