#!/usr/bin/env python
#
# $Id: ConnectedBox.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $
#
# Copyright 2001 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""A HandledBox which can be connected to another HandledBox.
A canvas object subclassed from HandledBox which provides
connection points for connecting edges on each Handle.
"""
__rcs_info__ = {
#
# Creation Information
#
'module_name' : '$RCSfile: ConnectedBox.py,v $',
'rcs_id' : '$Id: ConnectedBox.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $',
'creator' : 'Doug Hellmann <doug@hellfly.net>',
'project' : 'PmwContribD',
'created' : 'Sun, 01-Apr-2001 15:05:00 EDT',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.3 $',
'date' : '$Date: 2001/11/03 11:05:22 $',
}
#
# Import system modules
#
import Tkinter
import Pmw
import sys, os, string
import Canvas
import math
#
# Import Local modules
#
import colormath
import ShadowBox
import HandledBox
import AnimatedFileIcon
import AnimatedFolder
#
# Module
#
class Connection:
"""An edge between two ConnectedBox objects.
"""
def __init__(self, connectingCanvas, from_handle, to_handle):
"""Create a Connection
Arguments
'connectingCanvas' -- Canvas on which everything will be
drawn.
'from_handle' -- Handle object which is origin of this
Connection.
'to_handle' -- Handle object which is destination of this
Connection.
"""
self.from_handle = from_handle
self.to_handle = to_handle
self.connectingCanvas = connectingCanvas
self.canvas = connectingCanvas.component('canvas')
self.line = Canvas.Line( self.canvas,
self.get_coords(),
#fill='blue',
fill='red',
width=2,
#arrow='last',
capstyle='round',
)
from_handle.parent.add_connection(self)
to_handle.parent.add_connection(self)
return
def get_coords(self):
"Return the coordinates of both points of this Connection."
return (self.from_handle.parent.connection_point(self.from_handle),
self.to_handle.parent.connection_point(self.to_handle))
def update(self):
"Redraw."
self.line.coords( self.get_coords() )
return
class ConnectedBox(HandledBox.HandledBox):
"""Subclass of HandledBox which allows connections between handles.
"""
def __init__(self, canvas, connectingCanvas, **kw):
apply(HandledBox.HandledBox.__init__, (self, canvas,), kw)
self.connectingCanvas = connectingCanvas
self.connectingCanvas.register_node(self)
self.connections = []
def __str__(self):
return '%s,(%d,%d)' % (self.name, self.ulx, self.uly)
def add_connection(self, conn):
"Add a new connection."
self.connections.append(conn)
return
def move(self, delta_x, delta_y):
"Move the object on the canvas by the specified x,y offsets."
HandledBox.HandledBox.move(self, delta_x, delta_y)
map( lambda x: x.update(), self.connections )
return
def center_of_line(self, p1, p2):
"""Return the center point of the line connecting the two points.
Arguments
'p1' -- Point one, tuple with (x,y)
'p2' -- Point two, tuple with (x,y)
"""
return ( (p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2 )
def add_handle(self, handle_name=None, edge=Tkinter.LEFT,
helpMessage=None, relief=Tkinter.RAISED,
color='red', highlight_color='green'):
"""Add a new handle to the edge of the box.
Arguments
'handle_name' -- The name of the handle.
'edge' -- A side specification. Defaults to Tkinter.LEFT.
'helpMessage' -- Balloon help to be displayed when the
mouse pointer is over the handle.
'relief' -- 3-D relief specification. Defaults to
Tkinter.RAISED.
'color' -- Usual color of the handle target spot.
'highlight_color' -- Color of the handle target spot when
the mouse pointer is over it.
"""
h = HandledBox.HandledBox.add_handle(self, handle_name=handle_name,
edge=edge,
helpMessage=helpMessage,
relief=relief
)
if h.edge in [ Tkinter.TOP, Tkinter.LEFT ]:
input = 0
direction = h.shadow_edge()
else:
input = 1
direction = h.edge
ibbox = h.ibbox()
top_left = ibbox[0]
bottom_right = ibbox[1]
bottom_left = (ibbox[0][0], ibbox[1][1])
top_right = (ibbox[1][0], ibbox[0][1])
if direction == Tkinter.LEFT:
handle_coords = ( top_right, bottom_right,
self.center_of_line( top_left, bottom_left )
)
elif direction == Tkinter.RIGHT:
handle_coords = ( top_left, bottom_left,
self.center_of_line( top_right, bottom_right )
)
elif direction == Tkinter.TOP:
handle_coords = ( bottom_left, bottom_right,
self.center_of_line( top_left, top_right )
)
elif direction == Tkinter.BOTTOM:
handle_coords = ( top_left, top_right,
self.center_of_line( bottom_left, bottom_right )
)
h.socket = Canvas.Polygon(self.canvas,
handle_coords,
fill=color,
outline=color,
)
h.socket.highlight_color = highlight_color
h.socket.normal_color = color
h.parent.add_object(h.socket)
h.socket.bind(
'<ButtonPress-1>',
lambda e, s=self, h=h: s.connectingCanvas.toggle_rubber_band(e, h)
)
if not input:
h.socket.bind(
'<Enter>',
lambda e, s=self, h=h: s.connectingCanvas.enable_rubber_band_end(e, h)
)
else:
h.socket.bind(
'<Enter>',
lambda e, s=self, h=h: s.connectingCanvas.enable_rubber_band_start(e, h)
)
h.socket.bind('<Leave>', self.connectingCanvas.disable_rubber_band)
h.socket.bind(
'<Leave>',
lambda e, h=h: h.socket.config(fill=h.socket.normal_color, outline=h.socket.normal_color)
)
return h
def highlight_handle(self, h):
"Called when handle should be highlighted as feedback to the user."
h.socket.config(fill=h.socket.highlight_color, outline=h.socket.highlight_color)
return
def dehighlight_handle(self, h):
"""Called when handle should no longer be highlighted.
This method is the opposite of 'highlight_handle'.
"""
h.socket.config(fill=h.socket.normal_color, outline=h.socket.normal_color)
return
def connection_point(self, h):
"Returns the point on the handle to which Connections should be drawn."
if h.edge in [ Tkinter.TOP, Tkinter.LEFT ]:
input = 0
direction = h.shadow_edge()
else:
input = 1
direction = h.edge
ibbox = h.ibbox()
top_left = ibbox[0]
bottom_right = ibbox[1]
bottom_left = (ibbox[0][0], ibbox[1][1])
top_right = (ibbox[1][0], ibbox[0][1])
if input:
if direction == Tkinter.RIGHT:
return self.center_of_line( top_right, bottom_right )
elif direction == Tkinter.BOTTOM:
return self.center_of_line( bottom_left, bottom_right )
else:
if direction == Tkinter.RIGHT:
return self.center_of_line( top_left, bottom_left )
elif direction == Tkinter.BOTTOM:
return self.center_of_line( top_left, top_right )
return self.center_of_line( top_left, bottom_right )
class ConnectedBoxGraph:
"A graph represented using ConnectedBox and Connection objects."
def __init__(self):
self.edges = []
self.nodes = []
return
def add_node(self, node):
"Add a node to the graph."
if node in self.nodes:
raise ValueError('Duplicate value: %s' % node)
else:
self.nodes.append(node)
return
def add_edge(self, edge):
"Add an edge to the graph."
if edge in self.edges:
raise ValueError('Duplicate edge: %s' % edge)
else:
self.edges.append(edge)
return
def __str__(self):
s = ''
for n in self.nodes:
s = s + 'NODE:%s\n' % n
for e in self.edges:
s = s + 'EDGE:%s -> %s\n' % (e.from_handle.name, e.to_handle.name)
return s
class ConnectingCanvas(Pmw.ScrolledCanvas):
"""A special type of canvas which knows how to handle ConnectedBox objects.
"""
def __init__(self,
parent=None,
enabled_color='green',
rubber_band_color='black',
**kw):
"""Create ConnectingCanvas.
Arguments
'parent' -- The parent widget for the canvas.
'enabled_color' -- Color for rubber band when the
Connection can be made.
'rubber_band_color' -- Normal color for the rubber band.
"""
apply(Pmw.ScrolledCanvas.__init__, (self, parent,), kw)
#Pmw.ScrolledCanvas.__init__(self, parent, kw)
self.canvas = self.component('canvas')
#print 'creating ConnectingCanvas'
self.rubber_band = None
self.enabled_color = enabled_color
self.rubber_band_color = rubber_band_color
self.disable_rubber_band()
self.canvas.bind('<Motion>', self.move_rubber_band)
self.canvas.bind(
'<Key-Escape>',
lambda e, s=self, h=None: s.toggle_rubber_band(e, h)
)
self.graph = ConnectedBoxGraph()
return
def register_node(self, node):
"Register a node on the canvas."
self.graph.add_node(node)
self.print_graph()
return
def print_graph(self):
print 'CURRENT GRAPH:\n%s\n' % self.graph
return
def toggle_rubber_band(self, event, h=None):
"Toggles the rubber band state."
#print 'in toggle_rubber_band with h=%s' % h
self.canvas.focus_set()
if not self.rubber_band:
#
# The user clicked on a socket and there was no
# existing rubber band.
#
#print 'starting rubber band at %d, %d' % (event.x, event.y)
if self.rubber_band_start_enabled:
self.rubber_band_start_enabled = 0
self.rubber_band_start = h.parent.connection_point(h)
self.rubber_band_end = h.parent.connection_point(h)
self.rubber_band_start_handle = h
self.rubber_band = Canvas.Line(self.canvas,
( self.rubber_band_start, self.rubber_band_end ),
fill=self.rubber_band_color,
width=2,
)
self.rubber_band.bind('<ButtonPress-1>', self.toggle_rubber_band)
#print 'starting rubber band at %d, %d' % (event.x, event.y)
#print 'starting rubber band at %s' % self.rubber_band_start_handle.name
else:
if self.rubber_band_end_enabled:
#
# The user clicked on another socket, close out
# the rubber band.
#
#print 'ending rubber band at %d, %d' % (event.x, event.y)
#print 'ending rubber band at %s' % h.name
self.rubber_band_end = h.parent.connection_point(h)
self.rubber_band.unbind('<ButtonPress-1>')
self.rubber_band.delete()
self.rubber_band = None
con = Connection(self, self.rubber_band_start_handle, h)
self.graph.add_edge(con)
self.print_graph()
elif not h:
#
# The user pressed Escape, cancel the rubber band
#
self.canvas.delete(self.rubber_band.id)
del self.rubber_band
self.rubber_band_start = None
self.rubber_band_end = None
self.rubber_band = None
return
def move_rubber_band(self, event):
"Move the points of the rubber band to be consistent with the motion event."
if self.rubber_band:
if event.x < self.rubber_band_start[0]:
# moving left
x_offset = 2
else:
x_offset = -2
if event.y < self.rubber_band_start[1]:
# moving up
y_offset = 2
else:
y_offset = -2
self.rubber_band_end = (event.x + x_offset, event.y + y_offset)
self.rubber_band.coords( (self.rubber_band_start, self.rubber_band_end) )
#print 'moving to %d, %d' % (event.x, event.y)
return
def enable_rubber_band_end(self, event=None, h=None):
"Stop the enable-rubber-band "
#print 'in enable_rubber_band_end '
if self.rubber_band:
#print 'enabling rubber band end'
if h.parent != self.rubber_band_start_handle.parent:
h.parent.highlight_handle(h)
self.rubber_band_end_enabled = 1
self.rubber_band.config(fill=self.enabled_color)
else:
h.parent.dehighlight_handle(h)
self.rubber_band.config(fill=self.rubber_band_color)
return
def enable_rubber_band_start(self, event=None, h=None):
"Start enabling the rubber band."
#print 'in enable_rubber_band_start '
if not self.rubber_band:
#print 'enabling rubber band start'
self.rubber_band_start_enabled = 1
h.parent.highlight_handle(h)
return
def disable_rubber_band(self, event=None):
"Turn off the rubber band."
#print 'disabling rubber band'
self.rubber_band_end_enabled = 0
self.rubber_band_start_enabled = 0
if self.rubber_band:
self.rubber_band.config(fill=self.rubber_band_color)
return
class ConnectedIcon(ConnectedBox):
"Combine the ConnectedBox with the interface of the AnimatedIcon."
def __init__(self, fileName=None, **kw):
apply(ConnectedBox.__init__, (self,), kw)
((ulx, uly), (lrx, lry)) = self.ibbox()
icon = self.create_icon(ulx, uly, lrx, lry)
if icon:
self.add_object(icon)
self.icon = icon
self.add_handles()
def create_icon(self, ulx, uly, lrx, lry):
pass
def add_handles(self):
pass
class ConnectedFile(ConnectedIcon):
"File icon which allows connections to be made to and from it."
def __init__(self, fileName, **kw):
self.fileName = fileName
apply(ConnectedIcon.__init__, (self,), kw)
return
def create_icon(self, ulx, uly, lrx, lry):
icon = AnimatedFileIcon.AnimatedFileIcon(self.canvas,
ulx=ulx + 1,
uly=uly + 1,
width = lrx - ulx - 2,
height = lry - uly - 2,
)
return icon
class InputFile(ConnectedFile):
"A ConnectedFile which accepts one incoming connection."
def add_handles(self):
self.add_handle(self.fileName,
helpMessage='Input file "%s"' % self.fileName, edge=Tkinter.RIGHT)
return
class OutputFile(ConnectedFile):
"A ConnectedFile which provides one outgoing connection."
def add_handles(self):
self.add_handle(self.fileName,
helpMessage='Output file "%s"' % self.fileName, edge=Tkinter.LEFT)
return
class ConnectedDirectory(ConnectedIcon):
"Folder icon which allows connections to be made from and to it."
def __init__(self, directoryName, globPattern, **kw):
self.directoryName=directoryName
self.globPattern=globPattern
apply(ConnectedIcon.__init__, (self,), kw)
return
def create_icon(self, ulx, uly, lrx, lry):
icon = AnimatedFolder.DoubleClickFolder(self.canvas,
ulx=ulx + 1,
uly=uly + 1,
width = lrx - ulx - 2,
height = lry - uly - 2,
name = 'the folder',
)
return icon
class InputDirectory(ConnectedDirectory):
"A ConnectedDirectory which supports one incoming connection."
def add_handles(self):
self.add_handle(self.directoryName,
helpMessage='Input directory "%s"(%s)' % (self.directoryName, self.globPattern),
edge=Tkinter.RIGHT)
return
class OutputDirectory(ConnectedDirectory):
"A ConnectedDirectory which provides one outgoing connection."
def add_handles(self):
self.add_handle(self.directoryName,
helpMessage='Output directory "%s"(%s)' % (self.directoryName, self.globPattern),
edge=Tkinter.LEFT)
return
if __name__ == '__main__':
import TestCases.CanvasTestApp
class GreenCircleBox(ConnectedBox):
def __init__(self, **kw):
apply(ConnectedBox.__init__, (self,), kw)
for n in range(0, 2):
for side in [ Tkinter.LEFT, Tkinter.RIGHT, Tkinter.TOP, Tkinter.BOTTOM ]:
name = '%s_%d' % (side, n)
h = self.add_handle(name,
helpMessage = 'help for %s' % name, edge = side)
#
# Put a circle in the middle of the box
#
((ulx, uly), (lrx, lry)) = self.ibbox()
circle = Canvas.Oval(self.canvas, ulx + 1, uly + 1, lrx - 2, lry - 2,
fill='green',
outline='black',
width=2)
self.add_object(circle)
self.circle = circle
class ConnectBoxTest(TestCases.CanvasTestApp.CanvasTestApp):
appname = 'Test shadow box'
handle_message_type = 'userevent'
def showHandleInfo(self, event, handle=None):
self.showMessage(self.handle_message_type, 'Over %s' % handle.name)
def hideHandleInfo(self, event):
##print 'in hideHandleInfo (%s)' % event
self.showMessage(self.handle_message_type)
def createCanvasObjects(self):
box = GreenCircleBox(canvas=self.canvas, connectingCanvas=self.scrolledCanvas,
name='green circle',
ulx=50, uly=50,
width=50,
height=50,
relief=Tkinter.RIDGE,
bd=10,
ridgeWidth=6,
allowMotion=1,
#background='lightblue',
balloon = self.balloon(),
helpMessage = 'Process?',
handleWidth=15,
handleHeight=15,
)
box2 = InputFile(fileName='blah',
canvas=self.canvas,
connectingCanvas=self.scrolledCanvas,
name='file icon',
ulx=200, uly=50,
width=85,
height=110,
relief=Tkinter.RIDGE,
bd=10,
ridgeWidth=6,
allowMotion=1,
#background='lightblue',
balloon = self.balloon(),
helpMessage = 'InputFile',
handleWidth=15,
handleHeight=15,
)
box2a = OutputFile(fileName='blah',
canvas=self.canvas,
connectingCanvas=self.scrolledCanvas,
name='file icon',
ulx=350, uly=50,
width=85,
height=110,
relief=Tkinter.RIDGE,
bd=10,
ridgeWidth=6,
allowMotion=1,
#background='lightblue',
balloon = self.balloon(),
helpMessage = 'OutputFile',
handleWidth=15,
handleHeight=15,
)
#
# Put a circle in the middle of the box
#
box3 = InputDirectory('Data:', '*',
canvas=self.canvas,
connectingCanvas=self.scrolledCanvas,
name='folder icon',
ulx=200, uly=200,
width=110,
height=85,
relief=Tkinter.RIDGE,
bd=10,
ridgeWidth=6,
allowMotion=1,
#background='lightblue',
balloon = self.balloon(),
helpMessage = 'help for 2nd handled box',
handleWidth=15,
handleHeight=15,
)
box4 = OutputDirectory('Data:', '*.out',
canvas=self.canvas,
connectingCanvas=self.scrolledCanvas,
name='folder icon',
ulx=350, uly=200,
width=110,
height=85,
relief=Tkinter.RIDGE,
bd=10,
ridgeWidth=6,
allowMotion=1,
#background='lightblue',
balloon = self.balloon(),
helpMessage = 'help for 2nd handled box',
handleWidth=15,
handleHeight=15,
)
def folderClickCB(self, event, icon):
#print 'clicked on folder %s (%s)' % (icon, icon.name)
#print 'icon is now in %s state' % (icon.get_state())
##print 'closing'
icon.set_state('closed')
#print ''
ConnectBoxTest(canvas_pyclass=ConnectingCanvas).run()
|