# -*- coding: utf-8 -*-
## Filename : chart_controler.py
## Author(s) : Michel Le Borgne
## Created : 4/3/2010
## Revision :
## Source :
##
## Copyright 2012 - 2020 IRISA/IRSET
##
## This library is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published
## by the Free Software Foundation; either version 2.1 of the License, or
## any later version.
##
## This library 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. The software and
## documentation provided hereunder is on an "as is" basis, and IRISA has
## no obligations to provide maintenance, support, updates, enhancements
## or modifications.
## In no event shall IRISA be liable to any party for direct, indirect,
## special, incidental or consequential damages, including lost profits,
## arising out of the use of this software and its documentation, even if
## IRISA have been advised of the possibility of such damage. See
## the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this library; if not, write to the Free Software Foundation,
## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
##
## The original code contained here was initially developed by:
##
## Michel Le Borgne.
## IRISA
## Symbiose team
## IRISA Campus de Beaulieu
## 35042 RENNES Cedex, FRANCE
##
##
## http:
## mailto:
##
## Contributor(s): Geoffroy Andrieux, Nolwenn Le Meur
##
"""Main GUI controllers + auxiliary class (ChartClipboard)
- :class:`ChartClipboard`: A clipboard to handle the copy of nodes through models
- :class:`ChartControler`: A controler for graphical views
- :class:`NavControler`: A controler for navigation view (overview section in the GUI)
"""
# Standard imports
from __future__ import print_function
import itertools as it
from string import ascii_uppercase
from math import sqrt
import re
# Custom imports
import gtk
from gtk.gdk import (
Cursor,
ARROW,
BOTTOM_LEFT_CORNER,
BOTTOM_RIGHT_CORNER,
TOP_LEFT_CORNER,
TOP_RIGHT_CORNER,
LINE_ON_OFF_DASH,
)
# Cadbiom imports
from cadbiom_gui.gt_gui.graphics.drawing_style import Arrow
from cadbiom.models.guard_transitions.chart_model import CMacroNode, CNode, CSimpleNode
from cadbiom import commons as cm
LOGGER = cm.logger()
[docs]class ChartClipboard(object):
"""A clipboard used by ChartControler to handle the copy of nodes through models.
"""
def __init__(self):
self.clip_node = None
[docs] def put_node(self, node):
"""register a chart model node in the clipboard"""
self.clip_node = node
[docs] def get_node(self):
"""retrieve the current node in the clipboard"""
node = self.clip_node
self.clip_node = None
return node
[docs] def has_element(self):
"""test if the clipboard has a registered node"""
return self.clip_node is not None
[docs] def has_macro(self):
"""test if the clipboard has a registered macro node"""
return self.clip_node.is_macro() if self.clip_node else False
[docs]class ChartControler(object):
"""Implement a controler for graphical views
Used by::
- the main graph editor widget as this
- NavControler for the overview widget through inheritance
Signals::
- current_change: Inform CharterInfo that the currently selected transition,
model or node has changed. Used to dispatch related information in the GUI.
- edit_node: Inform Charter that a MacroNode will be edited.
TODO: just show metadata on double click on another item
=> add a signal to inform CharterInfo objects
:param model: Current chart model
:param mouse_role: Mouse action "select", "resizing", "moving"
:param current_node: Currently selected node
:param current_node_center: Tuple of coordinates (x, y) of the current node
:param current_handle: Id of the node corner currently selected for resizing
(only for MacroNodes)
:param current_transition: Currently selected transition
:param m_vscreen_coord: Mouse virtual screen coordinates (xloc, yloc)
:param lastx: coordinates of last click in view
:param lasty: coordinates of last click in view
:param clipboard: Clipboard object used to copy nodes through models
:param drawing_style:
:param signal_dict: Structure to assign observers to each signal of ChartControler
:param gen_name: Generator of unique names for new nodes
:param node_copy_count: Generator of ints used in the naming of copied nodes
:type model: <ChartModel>
:type mouse_role: <str>
:type current_node: <CNode>
:type current_node_center: <tuple <float>,<float>>
:type current_handle: <int>
:type current_transition: <CTransition>
:type m_vscreen_coord: <tuple <float>,<float>>
:type lastx: <float>
:type lasty: <float>
:type clipboard: <ChartClipboard>
:type drawing_style:
:type signal_dict: <dict <str>:<list>>
:type gen_name: <generator>
:type node_copy_count: <generator>
"""
cursors = []
cursors.append(Cursor(ARROW))
cursors.append(Cursor(TOP_LEFT_CORNER))
cursors.append(Cursor(TOP_RIGHT_CORNER))
cursors.append(Cursor(BOTTOM_RIGHT_CORNER))
cursors.append(Cursor(BOTTOM_LEFT_CORNER))
LSIGNALS = ["current_change", "edit_node"]
def __init__(self, model, clipboard):
self.model = model
# mouse role
self.mouse_role = "select"
# Set default node
self.current_node = self.model.get_root()
# Note: current_node_center should be initialized to None
# only if current_node is also None...
self.current_node_center = None
self.current_handle = 0
self.current_transition = None
# coordinate of last click in virtual screen 1x1
self.m_vscreen_coord = None
# coordinates of last click in view
self.lastx = 0
self.lasty = 0
self.clipboard = clipboard
self.drawing_style = None
self.signal_dict = {signal: list() for signal in self.LSIGNALS}
# Generator for the auto-naming of new nodes
self.gen_name = self.nodes_names_generator()
# Generator for the auto-naming of copied nodes
self.node_copy_count = it.count(1)
[docs] def attach(self, signal, obs):
"""Register an observer for the given signal
:param signal: Name of the signal ("current_change" or "edit_node")
:param obs: The observer
:type signal: <str>
:type obs: <CharterInfo> or <Charter>
"""
lobs = self.signal_dict[signal]
if not obs in lobs:
lobs.append(obs)
[docs] def detach(self, signal, obs):
"""Remove an observer for the given signal
:param signal: Name of the signal ("current_change" or "edit_node")
:param obs: The observer
:type signal: <str>
:type obs: <CharterInfo> or <Charter>
"""
self.signal_dict[signal].remove(obs)
[docs] def notify(self, signal):
"""Emit a signal subsequently to a mouse action
- current_change: Tell CharterInfo that the current selection has changed
- edit_node: Tell Charter that the current MacroNode will be edited
:param signal: name of the signal
:type signal: <str>
"""
LOGGER.debug("ChartControler notify signal: %s to %s", signal, self.signal_dict[signal])
lobs = self.signal_dict[signal]
if signal == "current_change":
# Tell CharterInfo that the selection has changed
for obs in lobs:
obs.update(self.current_node, self.current_transition)
elif signal == "edit_node":
# Tell Charter that the MacroNode will be edited
for obs in lobs:
obs.update(self.current_node)
else:
raise Exception("ChartControler: Unknown signal")
[docs] def set_view(self, view):
"""
As it says
"""
self.view = view
[docs] def set_mouse_role(self, role):
"""
As it says
"""
self.mouse_role = role
[docs] def remove_node_or_transition(self, item):
"""Remove the given transition or the given node
Called by Charter object when Delete key is pressed, or when the Remove
option is selected on the the context-menu hover an item on the DrawingArea.
:param item:
:type item: <CTransition> or <CNode> but not <CTopNode>
"""
# Remove the item
item.remove()
if not isinstance(item, CNode):
# Transition
self.current_transition = None
# TODO: Don't know why, but the deletion of transitions requires
# a notification to the model (this is not the case for nodes)
self.model.notify()
# Reset selection
self.current_node = None
self.current_node_center = None
# Current handle is now the TopNode
self.current_handle = 0
# Refresh ChartInfo in order to display actualized data about the model
self.notify("current_change")
[docs] def new_node(self, node_type):
"""Creation of a new node
Called during the :meth:`on_button_release` callback.
@param node_type: type of the node (string)
"""
xnode = self.m_vscreen_coord[0]
ynode = self.m_vscreen_coord[1]
if node_type == "simple":
self.current_node = self.current_node.add_simple_node(
next(self.gen_name), xnode, ynode
)
elif node_type == "macro":
self.current_node = self.current_node.add_macro_subnode(
next(self.gen_name), xnode, ynode, 0.25, 0.25
)
elif node_type == "start":
self.current_node = self.current_node.add_start_node(xnode, ynode)
elif node_type == "trap":
self.current_node = self.current_node.add_trap_node(xnode, ynode)
elif node_type == "perm":
self.current_node = self.current_node.add_perm_node(
next(self.gen_name), xnode, ynode
)
elif node_type == "input":
self.current_node = self.current_node.add_input_node(
next(self.gen_name), xnode, ynode
)
elif node_type == "env":
self.current_node = self.current_node.add_env_node(
next(self.gen_name), xnode, ynode, 0.25, 0.25
)
else: # bug!
raise TypeError("new_node: UNKNOWN TYPE: %s" % node_type)
self.current_transition = None
self.current_handle = 0
self.m_vscreen_coord = (0.0, 0.0)
self.notify("current_change")
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
[docs] def nodes_names_generator(self):
"""Return a generator of names for new nodes.
Names are generated in lexicographic order and we try to avoid names
that are already in the model (this is important in order to avoid
overwriting of nodes).
"""
for size in it.count(start=1):
for tpl in it.combinations(ascii_uppercase, size):
name = "".join(tpl)
if name in self.model.node_dict:
continue
yield name
[docs] def new_transition(self, xmo, ymo):
"""Create a new transition
Called during the :meth:`on_button_release` callback.
@param xmo, ymo: int mouse screen coordinates
"""
# are we in a node?
xloc = xmo / self.view.draw_width
yloc = ymo / self.view.draw_height
# w_coef = 1.0/self.view.draw_width
# h_coef = 1.0/self.view.draw_height
(node, handle, center, trans) = self.model.find_element(
(xloc, yloc), self.drawing_style
)
if not node: # no extremity (never happens)
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
self.model.notify() # rubout the draft transition
return
# are we in same container
if self.current_node.father != node.father:
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
self.model.notify() # rubout the draft transition
return
# we are in extremity node - current node is origin node
# is origin node correct and extremity node correct
if self.current_node.is_for_origin() and node.is_for_extremity():
trans = self.current_node.father.add_transition(self.current_node, node)
if trans:
self.current_node.selected = False
self.current_node = None
self.m_vscreen_coord = (0.0, 0.0)
self.current_transition = trans
trans.selected = True
self.notify("current_change")
self.model.notify()
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
else:
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
self.model.notify() # rubout the draft transition
else:
self.view.window.set_cursor(ChartControler.cursors[0])
self.mouse_role = "select"
if self.current_node:
self.current_node.selected = False
self.model.notify() # rubout the draft transition
return
[docs] def on_motion_notify(self, widget, event):
"""Callback moving the cursor in the DrawingArea
(in the ChartView object)
:param widget: gtk DrawingArea that emit the event
:param event: gdk mouse event with attributes (x, y)
:type widget: <NavView> or <ChartView>
:type event: <gtk.gdk.Event>
"""
swi = self.view.draw_width
she = self.view.draw_height
# transform mouse coordinates to virtual screen 1.0 x 1.0
xvirt = event.x / swi
yvirt = event.y / she
if self.mouse_role == "select":
(node, handle, center, trans) = self.model.find_element(
(xvirt, yvirt), self.drawing_style
)
if node == self.model.get_root():
handle = 0
# detect a new handle
if handle != self.current_handle:
self.current_handle = handle
self.view.window.set_cursor(ChartControler.cursors[handle])
elif self.mouse_role == "resizing":
self.current_node.resize(
xvirt, yvirt, self.current_handle, swi, she, self.model.get_root()
)
elif self.mouse_role == "moving":
v_dx = xvirt - self.m_vscreen_coord[0]
v_dy = yvirt - self.m_vscreen_coord[1]
v_size = self.current_node.accept(self.drawing_style)
self.current_node.move(v_dx, v_dy, v_size, self.model.get_root())
self.m_vscreen_coord = (xvirt, yvirt)
elif self.mouse_role == "new_trans":
if self.current_node:
self.draft_transition(event.x, event.y)
[docs] def draft_transition(self, xmo, ymo):
"""
draw a transition arrow in dotted line
@param xmo,ymo: mouse coordinates
"""
view = self.view
self.model.notify()
xr1 = int(self.current_node_center[0] * self.view.draw_width)
yr1 = int(self.current_node_center[1] * self.view.draw_height)
# graphic context
pixmap = view.pixmap
grc = view.window.new_gc()
grc.set_line_attributes(1, LINE_ON_OFF_DASH, 0, 0)
# line
pixmap.draw_line(grc, xr1, yr1, int(xmo), int(ymo))
# arrow
unx = xmo - xr1
uny = ymo - yr1
norm = sqrt(unx ** 2 + uny ** 2)
if norm != 0:
unx = unx / norm
uny = uny / norm
arr = Arrow()
arr.draw(view, (unx, uny), (xmo, ymo))
grc = view.window.new_gc()
view.window.draw_drawable(
grc, view.pixmap, 0, 0, 0, 0, view.draw_width, view.draw_height
)
## API for communication with charter ######################################
[docs] def set_action_select(self, widget):
"""
As it says
"""
self.mouse_role = "select"
[docs] def set_action_new_simple_node(self, widget):
"""
As it says
"""
self.mouse_role = "simple"
[docs] def set_action_new_macro_node(self, widget):
"""
As it says
"""
self.mouse_role = "macro"
[docs] def set_action_new_start_node(self, widget):
"""
As it says
"""
self.mouse_role = "start"
[docs] def set_action_new_trap_node(self, widget):
"""
As it says
"""
self.mouse_role = "trap"
[docs] def set_action_new_transition(self, widget):
"""
As it says
"""
if self.current_node:
self.current_node.selected = False
self.current_node = None
self.current_handle = 0
self.m_vscreen_coord = None
if self.current_transition:
self.current_transition.selected = False
self.current_transition = None
self.mouse_role = "new_trans"
[docs] def set_action_new_perm_node(self, widget):
"""
As it says
"""
self.mouse_role = "perm"
[docs]class NavControler(object):
"""Controler for navigation view (overview section in the GUI)
Implement a controler for navigation views
"""
def __init__(self):
self.lastx = 0 # coordinates of last click in view
self.lasty = 0
self.in_ret = False
self.obs = []
[docs] def set_view(self, view):
"""
As it says
"""
# attribute view is assigned when a view is created
self.view = view
[docs] def attach(self, obs):
"""
observer management
"""
if not obs in self.obs:
self.obs.append(obs)
[docs] def detach(self, obs):
"""
observer management
"""
self.obs.remove(obs)
[docs] def notify(self, depx, depy):
"""
observer management
"""
for obs in self.obs:
obs.update(depx, depy)
[docs] def on_motion_notify(self, widget, event):
"""
callback
"""
if self.in_ret:
depx = event.x - self.lastx
depy = event.y - self.lasty
alloc = widget.get_allocation()
# limits
vv1 = self.view.x_ret + depx + self.view.w_ret
vv2 = self.view.x_ret + depx
if (vv1 > alloc.width) or (vv2 < 0):
depx = 0
vv1 = self.view.y_ret + depy + self.view.h_ret
vv2 = self.view.y_ret + depy
if (vv1 > alloc.height) or (vv2 < 0):
depy = 0
self.lastx = self.lastx + depx
self.lasty = self.lasty + depy
# reticule move
if (not depx == 0) or (not depy == 0):
depx = float(depx) / alloc.width
depy = float(depy) / alloc.height
self.notify(depx, depy)