scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyworkflow/apps/__init__.py +29 -0
- pyworkflow/apps/pw_manager.py +37 -0
- pyworkflow/apps/pw_plot.py +51 -0
- pyworkflow/apps/pw_project.py +113 -0
- pyworkflow/apps/pw_protocol_list.py +143 -0
- pyworkflow/apps/pw_protocol_run.py +51 -0
- pyworkflow/apps/pw_run_tests.py +267 -0
- pyworkflow/apps/pw_schedule_run.py +322 -0
- pyworkflow/apps/pw_sleep.py +37 -0
- pyworkflow/apps/pw_sync_data.py +439 -0
- pyworkflow/apps/pw_viewer.py +78 -0
- pyworkflow/constants.py +1 -1
- pyworkflow/gui/__init__.py +36 -0
- pyworkflow/gui/browser.py +760 -0
- pyworkflow/gui/canvas.py +1190 -0
- pyworkflow/gui/dialog.py +979 -0
- pyworkflow/gui/form.py +2726 -0
- pyworkflow/gui/graph.py +247 -0
- pyworkflow/gui/graph_layout.py +271 -0
- pyworkflow/gui/gui.py +566 -0
- pyworkflow/gui/matplotlib_image.py +233 -0
- pyworkflow/gui/plotter.py +247 -0
- pyworkflow/gui/project/__init__.py +25 -0
- pyworkflow/gui/project/base.py +192 -0
- pyworkflow/gui/project/constants.py +139 -0
- pyworkflow/gui/project/labels.py +205 -0
- pyworkflow/gui/project/project.py +491 -0
- pyworkflow/gui/project/searchprotocol.py +238 -0
- pyworkflow/gui/project/searchrun.py +181 -0
- pyworkflow/gui/project/steps.py +171 -0
- pyworkflow/gui/project/utils.py +332 -0
- pyworkflow/gui/project/variables.py +179 -0
- pyworkflow/gui/project/viewdata.py +472 -0
- pyworkflow/gui/project/viewprojects.py +510 -0
- pyworkflow/gui/project/viewprotocols.py +2116 -0
- pyworkflow/gui/project/viewprotocols_extra.py +562 -0
- pyworkflow/gui/text.py +771 -0
- pyworkflow/gui/tooltip.py +185 -0
- pyworkflow/gui/tree.py +684 -0
- pyworkflow/gui/widgets.py +307 -0
- pyworkflow/mapper/__init__.py +26 -0
- pyworkflow/mapper/mapper.py +222 -0
- pyworkflow/mapper/sqlite.py +1581 -0
- pyworkflow/mapper/sqlite_db.py +145 -0
- pyworkflow/project/__init__.py +31 -0
- pyworkflow/project/config.py +454 -0
- pyworkflow/project/manager.py +180 -0
- pyworkflow/project/project.py +2095 -0
- pyworkflow/project/usage.py +165 -0
- pyworkflow/protocol/__init__.py +38 -0
- pyworkflow/protocol/bibtex.py +48 -0
- pyworkflow/protocol/constants.py +87 -0
- pyworkflow/protocol/executor.py +483 -0
- pyworkflow/protocol/hosts.py +317 -0
- pyworkflow/protocol/launch.py +277 -0
- pyworkflow/protocol/package.py +42 -0
- pyworkflow/protocol/params.py +781 -0
- pyworkflow/protocol/protocol.py +2707 -0
- pyworkflow/tests/__init__.py +29 -0
- pyworkflow/tests/test_utils.py +25 -0
- pyworkflow/tests/tests.py +341 -0
- pyworkflow/utils/__init__.py +38 -0
- pyworkflow/utils/dataset.py +414 -0
- pyworkflow/utils/echo.py +104 -0
- pyworkflow/utils/graph.py +169 -0
- pyworkflow/utils/log.py +293 -0
- pyworkflow/utils/path.py +528 -0
- pyworkflow/utils/process.py +153 -0
- pyworkflow/utils/profiler.py +92 -0
- pyworkflow/utils/progressbar.py +154 -0
- pyworkflow/utils/properties.py +617 -0
- pyworkflow/utils/reflection.py +129 -0
- pyworkflow/utils/utils.py +880 -0
- pyworkflow/utils/which.py +229 -0
- pyworkflow/webservices/__init__.py +8 -0
- pyworkflow/webservices/config.py +8 -0
- pyworkflow/webservices/notifier.py +152 -0
- pyworkflow/webservices/repository.py +59 -0
- pyworkflow/webservices/workflowhub.py +74 -0
- pyworkflowtests/tests/__init__.py +0 -0
- pyworkflowtests/tests/test_canvas.py +72 -0
- pyworkflowtests/tests/test_domain.py +45 -0
- pyworkflowtests/tests/test_logs.py +74 -0
- pyworkflowtests/tests/test_mappers.py +392 -0
- pyworkflowtests/tests/test_object.py +507 -0
- pyworkflowtests/tests/test_project.py +42 -0
- pyworkflowtests/tests/test_protocol_execution.py +146 -0
- pyworkflowtests/tests/test_protocol_export.py +78 -0
- pyworkflowtests/tests/test_protocol_output.py +158 -0
- pyworkflowtests/tests/test_streaming.py +47 -0
- pyworkflowtests/tests/test_utils.py +210 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +2 -2
- scipion_pyworkflow-3.11.1.dist-info/RECORD +161 -0
- scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
pyworkflow/gui/canvas.py
ADDED
@@ -0,0 +1,1190 @@
|
|
1
|
+
# **************************************************************************
|
2
|
+
# *
|
3
|
+
# * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
|
4
|
+
# *
|
5
|
+
# * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
|
6
|
+
# *
|
7
|
+
# * This program is free software; you can redistribute it and/or modify
|
8
|
+
# * it under the terms of the GNU General Public License as published by
|
9
|
+
# * the Free Software Foundation; either version 3 of the License, or
|
10
|
+
# * (at your option) any later version.
|
11
|
+
# *
|
12
|
+
# * This program is distributed in the hope that it will be useful,
|
13
|
+
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# * GNU General Public License for more details.
|
16
|
+
# *
|
17
|
+
# * You should have received a copy of the GNU General Public License
|
18
|
+
# * along with this program; if not, write to the Free Software
|
19
|
+
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
20
|
+
# * 02111-1307 USA
|
21
|
+
# *
|
22
|
+
# * All comments concerning this program package may be sent to the
|
23
|
+
# * e-mail address 'scipion@cnb.csic.es'
|
24
|
+
# *
|
25
|
+
# **************************************************************************
|
26
|
+
"""
|
27
|
+
This module extends the functionalities of a normal Tkinter Canvas.
|
28
|
+
The new Canvas class allows to easily display Texboxes and Edges
|
29
|
+
that can be interactively dragged and clicked.
|
30
|
+
"""
|
31
|
+
import logging
|
32
|
+
logger = logging.getLogger(__name__)
|
33
|
+
import math
|
34
|
+
import tkinter as tk
|
35
|
+
import operator
|
36
|
+
|
37
|
+
from pyworkflow import Config
|
38
|
+
from pyworkflow.gui import gui, getDefaultFont
|
39
|
+
from pyworkflow.gui.widgets import Scrollable
|
40
|
+
|
41
|
+
DEFAULT_ZOOM = 100
|
42
|
+
|
43
|
+
DEFAULT_FONT_SIZE = Config.SCIPION_FONT_SIZE
|
44
|
+
|
45
|
+
DEFAULT_CONNECTOR_FILL = "blue"
|
46
|
+
DEFAULT_CONNECTOR_OUTLINE = "black"
|
47
|
+
|
48
|
+
|
49
|
+
class Canvas(tk.Canvas, Scrollable):
|
50
|
+
"""Canvas to draw some objects.
|
51
|
+
It actually is a Frame, a Canvas and scrollbars"""
|
52
|
+
_images = {}
|
53
|
+
|
54
|
+
def __init__(self, parent, tooltipCallback=None, tooltipDelay=1500, **kwargs):
|
55
|
+
defaults = {'bg': Config.SCIPION_BG_COLOR}
|
56
|
+
defaults.update(kwargs)
|
57
|
+
Scrollable.__init__(self, parent, tk.Canvas, **defaults)
|
58
|
+
|
59
|
+
self.lastItem = None # Track last item selected
|
60
|
+
self.lastPos = (0, 0) # Track last clicked position
|
61
|
+
self.eventPos = (0, 0)
|
62
|
+
self.dragging = False # Track first clicked position (for a drag action)
|
63
|
+
self.items = {} # Keep a dictionary with high-level items
|
64
|
+
self.cleanSelected = True
|
65
|
+
|
66
|
+
self.onClickCallback = None
|
67
|
+
self.onDoubleClickCallback = None
|
68
|
+
self.onRightClickCallback = None
|
69
|
+
self.onControlClickCallback = None
|
70
|
+
self.onAreaSelected = None
|
71
|
+
|
72
|
+
# Add bindings
|
73
|
+
self.bind("<Button-1>", self.onClick)
|
74
|
+
self.bind("<ButtonRelease-1>", self.onButton1Release)
|
75
|
+
self.bind("<Button-3>", self.onRightClick)
|
76
|
+
self.bind("<Button-2>", self.onRightClick)
|
77
|
+
self.bind("<Double-Button-1>", self.onDoubleClick)
|
78
|
+
self.bind("<B1-Motion>", self.onDrag)
|
79
|
+
# Hide the right-click menu
|
80
|
+
self.bind('<FocusOut>', self._unpostMenu)
|
81
|
+
self.bind("<Key>", self._unpostMenu)
|
82
|
+
self.bind("<Control-1>", self.onControlClick)
|
83
|
+
# self.bind("<MouseWheel>", self.onScroll)
|
84
|
+
# Scroll bindings in Linux
|
85
|
+
self.bind("<Control-Button-4>", self.zoomerP)
|
86
|
+
self.bind("<Control-Button-5>", self.zoomerM)
|
87
|
+
|
88
|
+
self._tooltipId = None
|
89
|
+
self._tooltipOn = False # True if the tooltip is displayed
|
90
|
+
self._tooltipCallback = tooltipCallback
|
91
|
+
self._tooltipDelay = tooltipDelay
|
92
|
+
|
93
|
+
self._runsFont = getDefaultFont().copy()
|
94
|
+
self._zoomFactor = DEFAULT_ZOOM
|
95
|
+
self.nodeList = None
|
96
|
+
|
97
|
+
if tooltipCallback:
|
98
|
+
self.bind('<Motion>', self.onMotion)
|
99
|
+
# self.bind('<Leave>', self.onLeave)
|
100
|
+
self._createTooltip() # This should set
|
101
|
+
|
102
|
+
self._menu = tk.Menu(self, tearoff=0)
|
103
|
+
|
104
|
+
def _drawGrid(self):
|
105
|
+
""" For debugging purposes. Do not delete.
|
106
|
+
Draws a grid on the canvas to get an ide about where click is happening"""
|
107
|
+
self.update()
|
108
|
+
_, _, width, height = self.frame.bbox(self)
|
109
|
+
|
110
|
+
for line in range(0, width, 100): # range(start, stop, step)
|
111
|
+
self.create_line([(line, 0), (line, height)], fill='black', tags='grid_line_w')
|
112
|
+
|
113
|
+
for line in range(0, height, 100):
|
114
|
+
self.create_line([(0, line), (width, line)], fill='black', tags='grid_line_h')
|
115
|
+
|
116
|
+
def _createTooltip(self):
|
117
|
+
""" Create a Tooltip window to display tooltips in
|
118
|
+
the canvas.
|
119
|
+
"""
|
120
|
+
tw = tk.Toplevel(self)
|
121
|
+
tw.withdraw() # hidden by default
|
122
|
+
tw.wm_overrideredirect(1) # Remove window decorations
|
123
|
+
tw.bind("<Leave>", self.hideTooltip)
|
124
|
+
|
125
|
+
self._tooltip = tw
|
126
|
+
|
127
|
+
def _showTooltip(self, x, y, item):
|
128
|
+
# check that the mouse is still in the position
|
129
|
+
nx = self.winfo_pointerx()
|
130
|
+
ny = self.winfo_pointery()
|
131
|
+
if x == nx and y == ny:
|
132
|
+
self._tooltipOn = True
|
133
|
+
tw = self._tooltip # short notation
|
134
|
+
self._tooltipCallback(tw, item)
|
135
|
+
tw.update_idletasks()
|
136
|
+
tw.wm_geometry("+%d+%d" % (x, y))
|
137
|
+
tw.deiconify()
|
138
|
+
|
139
|
+
def hideTooltip(self, e=None):
|
140
|
+
if self._tooltipOn:
|
141
|
+
self._tooltipOn = False
|
142
|
+
tw = self._tooltip # short notation
|
143
|
+
tw.withdraw()
|
144
|
+
|
145
|
+
def getRunsFont(self):
|
146
|
+
return self._runsFont
|
147
|
+
|
148
|
+
def getImage(self, img):
|
149
|
+
return gui.getImage(img)
|
150
|
+
|
151
|
+
def _unpostMenu(self, e=None):
|
152
|
+
self._menu.unpost()
|
153
|
+
|
154
|
+
def getCoordinates(self, event):
|
155
|
+
"""Converts the events coordinates to canvas coordinates"""
|
156
|
+
# Convert screen coordinates to canvas coordinates
|
157
|
+
xc = self.canvasx(event.x)
|
158
|
+
yc = self.canvasy(event.y)
|
159
|
+
return xc, yc
|
160
|
+
|
161
|
+
def selectItem(self, item):
|
162
|
+
if self.lastItem:
|
163
|
+
self.lastItem.setSelected(False)
|
164
|
+
self.lastItem = item
|
165
|
+
item.setSelected(True)
|
166
|
+
|
167
|
+
def multipleItemsSelected(self):
|
168
|
+
""" Returns True if more than one box selected,
|
169
|
+
False otherwise.
|
170
|
+
TODO: add numItemsSelected as attribute to Canvas
|
171
|
+
class and update when selection changes
|
172
|
+
"""
|
173
|
+
selectedItemCounts = 0
|
174
|
+
for k, v in self.items.items():
|
175
|
+
if v.getSelected():
|
176
|
+
selectedItemCounts += 1
|
177
|
+
if selectedItemCounts > 1:
|
178
|
+
return True
|
179
|
+
return False
|
180
|
+
|
181
|
+
def _findItem(self, xc, yc):
|
182
|
+
""" Find if there is any item in the canvas
|
183
|
+
in the mouse event coordinates.
|
184
|
+
Return None if not Found
|
185
|
+
"""
|
186
|
+
items = self.find_overlapping(xc - 1, yc - 1, xc + 1, yc + 1)
|
187
|
+
if self.lastItem is not None and self.lastItem.id in items:
|
188
|
+
return self.lastItem
|
189
|
+
for i in items:
|
190
|
+
if i in self.items:
|
191
|
+
return self.items[i]
|
192
|
+
return None
|
193
|
+
|
194
|
+
def _handleMouseEvent(self, event, callback):
|
195
|
+
# Store last event coordinates
|
196
|
+
self.eventPos = (event.x, event.y)
|
197
|
+
# Retrieve the coordinates relative to the Canvas
|
198
|
+
xc, yc = self.getCoordinates(event)
|
199
|
+
self.lastItem = self._findItem(xc, yc)
|
200
|
+
self.callbackResults = None
|
201
|
+
|
202
|
+
if callback:
|
203
|
+
self.callbackResults = callback(self.lastItem, event)
|
204
|
+
|
205
|
+
def onClick(self, event):
|
206
|
+
# On click happens completely before onDrag
|
207
|
+
self.cleanSelected = True
|
208
|
+
self._unpostMenu()
|
209
|
+
self._handleMouseEvent(event, self.onClickCallback)
|
210
|
+
if self.lastItem is None:
|
211
|
+
# Moving the canvas as a whole
|
212
|
+
self.move_start(event)
|
213
|
+
else:
|
214
|
+
# Dragging a single box
|
215
|
+
self.captureLastPosition(event)
|
216
|
+
|
217
|
+
def onControlClick(self, event):
|
218
|
+
self.cleanSelected = False
|
219
|
+
self._unpostMenu()
|
220
|
+
self._handleMouseEvent(event, self.onControlClickCallback)
|
221
|
+
|
222
|
+
def onRightClick(self, e=None):
|
223
|
+
# RightClick callback will not work not, as it need
|
224
|
+
# the event information to know the coordinates
|
225
|
+
self._handleMouseEvent(e, self.onRightClickCallback)
|
226
|
+
unpost = True
|
227
|
+
# If the callback return a list of actions
|
228
|
+
# we will show up a menu with them
|
229
|
+
actions = self.callbackResults
|
230
|
+
|
231
|
+
if actions:
|
232
|
+
self._menu.delete(0, tk.END)
|
233
|
+
for a in actions:
|
234
|
+
if a is None:
|
235
|
+
self._menu.add_separator()
|
236
|
+
else:
|
237
|
+
img = ''
|
238
|
+
label= a[0]
|
239
|
+
size = len(a)
|
240
|
+
|
241
|
+
if size > 2: # image for the action
|
242
|
+
img = self.getImage(a[2])
|
243
|
+
|
244
|
+
# Shortcuts
|
245
|
+
if size > 3:
|
246
|
+
shortCut = a[3]
|
247
|
+
if shortCut:
|
248
|
+
label= "%s (%s)" % (label, shortCut)
|
249
|
+
|
250
|
+
def getAction(callback):
|
251
|
+
return lambda: callback(e)
|
252
|
+
|
253
|
+
self._menu.add_command(label=label, command=getAction(a[1]),
|
254
|
+
image=img, compound=tk.LEFT,
|
255
|
+
font=gui.getDefaultFont())
|
256
|
+
self._menu.post(e.x_root, e.y_root)
|
257
|
+
unpost = False
|
258
|
+
if unpost:
|
259
|
+
self._menu.unpost()
|
260
|
+
|
261
|
+
def onDoubleClick(self, event):
|
262
|
+
self._handleMouseEvent(event, self.onDoubleClickCallback)
|
263
|
+
|
264
|
+
# move
|
265
|
+
def move_start(self, event):
|
266
|
+
# If nothing was click on ButtonPress
|
267
|
+
if self.lastItem is None:
|
268
|
+
self.captureLastPosition(event)
|
269
|
+
self.config(cursor='fleur')
|
270
|
+
self.scan_mark(event.x, event.y)
|
271
|
+
|
272
|
+
def onDrag(self, event):
|
273
|
+
try:
|
274
|
+
|
275
|
+
if self.lastItem:
|
276
|
+
xc, yc = self.getCoordinates(event)
|
277
|
+
dx, dy = xc - self.lastPos[0], yc - self.lastPos[1]
|
278
|
+
# logger.info("Moving item %s, %s." % (dx,dy))
|
279
|
+
self.lastItem.move(dx, dy)
|
280
|
+
self.lastPos = (xc, yc)
|
281
|
+
else:
|
282
|
+
self.dragging = True
|
283
|
+
self.scan_dragto(event.x, event.y, gain=1)
|
284
|
+
|
285
|
+
except Exception as ex:
|
286
|
+
# JMRT: We are having a weird exception here.
|
287
|
+
# Presumably because there is concurrency between the onDrag
|
288
|
+
# event and the refresh one. For now, just ignore it.
|
289
|
+
pass
|
290
|
+
|
291
|
+
def captureLastPosition(self, event):
|
292
|
+
""" Captures the last position the mouse were located upon the event
|
293
|
+
"""
|
294
|
+
self.lastPos = self.getCoordinates(event)
|
295
|
+
|
296
|
+
|
297
|
+
def onButton1Release(self, event):
|
298
|
+
if self.dragging:
|
299
|
+
# Failing in "data" view.
|
300
|
+
# TODO: This is not fully implemented
|
301
|
+
# self.onAreaSelected(self.firstPos[0], self.firstPos[1], event.x, event.y)
|
302
|
+
|
303
|
+
self.dragging = False
|
304
|
+
|
305
|
+
self.config(cursor='arrow')
|
306
|
+
|
307
|
+
def onMotion(self, event):
|
308
|
+
self.onLeave(event) # Hide tooltip and cancel schedule
|
309
|
+
|
310
|
+
xc, yc = self.getCoordinates(event)
|
311
|
+
item = self._findItem(xc, yc)
|
312
|
+
if item is not None:
|
313
|
+
self._tooltipId = self.after(self._tooltipDelay,
|
314
|
+
lambda: self._showTooltip(event.x_root,
|
315
|
+
event.y_root,
|
316
|
+
item))
|
317
|
+
|
318
|
+
def onLeave(self, event):
|
319
|
+
if self._tooltipId:
|
320
|
+
self.after_cancel(self._tooltipId)
|
321
|
+
self.hideTooltip()
|
322
|
+
|
323
|
+
def createTextbox(self, text, x, y, bgColor="#99DAE8", textColor='black'):
|
324
|
+
tb = TextBox(self, text, x, y, bgColor, textColor)
|
325
|
+
self.items[tb.id] = tb
|
326
|
+
return tb
|
327
|
+
|
328
|
+
def createTextCircle(self, text, x, y, bgColor="#99DAE8", textColor='black'):
|
329
|
+
tb = TextCircle(self, text, x, y, bgColor, textColor)
|
330
|
+
self.items[tb.id] = tb
|
331
|
+
return tb
|
332
|
+
|
333
|
+
def createRoundedTextbox(self, text, x, y, bgColor="#99DAE8", textColor='black'):
|
334
|
+
tb = RoundedTextBox(self, text, x, y, bgColor, textColor)
|
335
|
+
self.items[tb.id] = tb
|
336
|
+
return tb
|
337
|
+
|
338
|
+
def addItem(self, item):
|
339
|
+
self.items[item.id] = item
|
340
|
+
|
341
|
+
def createEdge(self, srcItem, dstItem):
|
342
|
+
edge = Edge(self, srcItem, dstItem)
|
343
|
+
# self.items[edge.id] = edge
|
344
|
+
return edge
|
345
|
+
|
346
|
+
def createCable(self, src, srcSocket, dst, dstSocket):
|
347
|
+
return Cable(self, src, srcSocket, dst, dstSocket)
|
348
|
+
|
349
|
+
def clear(self):
|
350
|
+
""" Clear all items from the canvas """
|
351
|
+
self.delete(tk.ALL)
|
352
|
+
self.items.clear()
|
353
|
+
|
354
|
+
def updateScrollRegion(self):
|
355
|
+
self.update_idletasks()
|
356
|
+
self.config(scrollregion=self.bbox("all"))
|
357
|
+
|
358
|
+
# linux zoom
|
359
|
+
def __zoom(self, event, scale):
|
360
|
+
|
361
|
+
newZoomFactor = round(self._zoomFactor * scale)
|
362
|
+
|
363
|
+
if self._zoomFactor == newZoomFactor:
|
364
|
+
return
|
365
|
+
|
366
|
+
self._zoomFactor = newZoomFactor
|
367
|
+
|
368
|
+
# x, y = self.getCoordinates(event)
|
369
|
+
self.scale("all", 0, 0, scale, scale)
|
370
|
+
self.__scaleFonts()
|
371
|
+
self.configure(scrollregion=self.bbox("all"))
|
372
|
+
|
373
|
+
def __scaleFonts(self):
|
374
|
+
|
375
|
+
currentFontSize = self._runsFont['size']
|
376
|
+
newFontSize = currentFontSize
|
377
|
+
|
378
|
+
zoomPairs = [(32, 7),
|
379
|
+
(44, 6),
|
380
|
+
(53, 5),
|
381
|
+
(66, 4),
|
382
|
+
(73, 3),
|
383
|
+
(82, 2),
|
384
|
+
(90, 1),
|
385
|
+
(105, 0),
|
386
|
+
(120, -1),
|
387
|
+
(137, -2),
|
388
|
+
(999, -3),
|
389
|
+
]
|
390
|
+
|
391
|
+
for factor, sizeDecrement in zoomPairs:
|
392
|
+
if self._zoomFactor <= factor:
|
393
|
+
newFontSize = DEFAULT_FONT_SIZE - sizeDecrement
|
394
|
+
break
|
395
|
+
|
396
|
+
if currentFontSize != newFontSize:
|
397
|
+
self._runsFont['size'] = newFontSize
|
398
|
+
|
399
|
+
def zoomerP(self, event):
|
400
|
+
self.__zoom(event, 1.111111)
|
401
|
+
|
402
|
+
def zoomerM(self, event):
|
403
|
+
self.__zoom(event, 0.9)
|
404
|
+
|
405
|
+
def moveTo(self, x, y):
|
406
|
+
|
407
|
+
if x > 1 or y > 1:
|
408
|
+
x0,y0,x1,y1 = self.bbox("all")
|
409
|
+
|
410
|
+
# x dim
|
411
|
+
x = x / (x0+x1)
|
412
|
+
start, end = self.xview()
|
413
|
+
visisble_length = end - start
|
414
|
+
x = x - (visisble_length/2)
|
415
|
+
|
416
|
+
# Same with y
|
417
|
+
y = y / (y0+y1)
|
418
|
+
start, end = self.yview()
|
419
|
+
visible_length = end - start
|
420
|
+
y = y - (visible_length / 2)
|
421
|
+
|
422
|
+
self.xview("moveto", x)
|
423
|
+
self.yview("moveto", y)
|
424
|
+
|
425
|
+
def drawGraph(self, graph, layout=None, drawNode=None, nodeList=None):
|
426
|
+
""" Draw a graph in the canvas.
|
427
|
+
nodes in the graph should have x and y.
|
428
|
+
If layout is not None, it will be used to
|
429
|
+
reorganize the node positions.
|
430
|
+
Provide drawNode if you want to customize how
|
431
|
+
to create the boxes for each graph node.
|
432
|
+
"""
|
433
|
+
|
434
|
+
# Reset the zoom and font
|
435
|
+
scale = self._zoomFactor / DEFAULT_ZOOM
|
436
|
+
self._zoomFactor = DEFAULT_ZOOM
|
437
|
+
self._runsFont['size'] = DEFAULT_FONT_SIZE
|
438
|
+
self.nodeList = nodeList
|
439
|
+
|
440
|
+
if drawNode is None:
|
441
|
+
self.drawNode = self._drawNode
|
442
|
+
else:
|
443
|
+
self.drawNode = drawNode
|
444
|
+
|
445
|
+
self._drawNodes(graph.getRoot(), {})
|
446
|
+
|
447
|
+
if layout is not None:
|
448
|
+
layout.draw(graph)
|
449
|
+
# Update node positions
|
450
|
+
self._updatePositions(graph.getRoot(), {})
|
451
|
+
self.updateScrollRegion()
|
452
|
+
self.__zoom(None, scale)
|
453
|
+
|
454
|
+
def reorganizeGraph(self, graph, layout=None):
|
455
|
+
|
456
|
+
layout.draw(graph)
|
457
|
+
# Update node positions
|
458
|
+
self._updatePositions(graph.getRoot(), {}, createEdges=False)
|
459
|
+
self.updateScrollRegion()
|
460
|
+
|
461
|
+
def _drawNode(self, canvas, node):
|
462
|
+
""" Default implementation to draw nodes as textboxes. """
|
463
|
+
return TextBox(self, node.getLabel(), 0, 0,
|
464
|
+
bgColor="#99DAE8", textColor='black')
|
465
|
+
|
466
|
+
def _drawNodes(self, node, visitedDict={}):
|
467
|
+
nodeName = node.getName()
|
468
|
+
|
469
|
+
if nodeName not in visitedDict:
|
470
|
+
visitedDict[nodeName] = True
|
471
|
+
item = self.drawNode(self, node)
|
472
|
+
node.width, node.height = item.getDimensions()
|
473
|
+
node.item = item
|
474
|
+
item.node = node
|
475
|
+
self.addItem(item)
|
476
|
+
|
477
|
+
if getattr(node, 'expanded', True):
|
478
|
+
for child in node.getChildren():
|
479
|
+
if self.nodeList is None:
|
480
|
+
self._drawNodes(child, visitedDict)
|
481
|
+
elif self.nodeList.getNode(child.run.getObjId()).isVisible():
|
482
|
+
self._drawNodes(child, visitedDict)
|
483
|
+
else:
|
484
|
+
self._setupParentProperties(node, visitedDict)
|
485
|
+
else:
|
486
|
+
self._setupParentProperties(node, visitedDict)
|
487
|
+
|
488
|
+
def _connectParents(self, item):
|
489
|
+
"""
|
490
|
+
Establishes a connection between the visible parents of node's children
|
491
|
+
with node
|
492
|
+
"""
|
493
|
+
logger.debug("Connecting item %s with parents:" % item)
|
494
|
+
visibleParents = self._visibleParents(item, [])
|
495
|
+
for visibleParent in visibleParents:
|
496
|
+
if visibleParent != item:
|
497
|
+
try:
|
498
|
+
logger.debug("Visible parent: %s" % visibleParent)
|
499
|
+
dest = self.items[item.item.id]
|
500
|
+
source = self.items[visibleParent.item.id]
|
501
|
+
visibleParentNode = self.nodeList.getNode(visibleParent.run.getObjId())
|
502
|
+
itemNode = self.nodeList.getNode(item.run.getObjId())
|
503
|
+
|
504
|
+
if visibleParent not in item.getParents() and visibleParentNode.isExpanded():
|
505
|
+
self.createEdge(source, dest)
|
506
|
+
if not itemNode.isExpanded():
|
507
|
+
self.createEdge(source, dest)
|
508
|
+
except:
|
509
|
+
logger.warning("Can't connect node %s to parent %s" % (item, visibleParent))
|
510
|
+
|
511
|
+
def _visibleParents(self, node, parentlist):
|
512
|
+
"""
|
513
|
+
Return a list with the visible parents of the node's children
|
514
|
+
"""
|
515
|
+
for child in node.getChildren():
|
516
|
+
parents = child.getParents()
|
517
|
+
for parent in parents:
|
518
|
+
parentNode = self.nodeList.getNode(parent.run.getObjId())
|
519
|
+
if parentNode.isVisible() and parent != node and parent not in parentlist:
|
520
|
+
parentlist.append(parent)
|
521
|
+
return parentlist
|
522
|
+
|
523
|
+
def _setupParentProperties(self, node, visitedDict):
|
524
|
+
""" This methods is used for collapsed nodes, in which
|
525
|
+
the properties (width, height, x and y) is propagated
|
526
|
+
to the hidden childs.
|
527
|
+
"""
|
528
|
+
for child in node.getChildren():
|
529
|
+
if child.getName() not in visitedDict:
|
530
|
+
child.width = node.width
|
531
|
+
child.height = node.height
|
532
|
+
child.x = node.x
|
533
|
+
child.y = node.y
|
534
|
+
self._setupParentProperties(child, visitedDict)
|
535
|
+
|
536
|
+
def _updatePositions(self, node, visitedDict=None, createEdges=True):
|
537
|
+
""" Update position of nodes and create the edges. """
|
538
|
+
nodeName = node.getName()
|
539
|
+
|
540
|
+
if nodeName not in visitedDict:
|
541
|
+
visitedDict[nodeName] = True
|
542
|
+
item = node.item
|
543
|
+
logger.debug("Updating position for node: %s, item: %s" % (node, item))
|
544
|
+
item.moveTo(node.x, node.y)
|
545
|
+
|
546
|
+
if getattr(node, 'expanded', True):
|
547
|
+
for child in node.getChildren():
|
548
|
+
if self.nodeList is None:
|
549
|
+
self.createEdge(item, child.item)
|
550
|
+
self._updatePositions(child, visitedDict, createEdges)
|
551
|
+
elif self.nodeList.getNode(child.run.getObjId()).isVisible():
|
552
|
+
if createEdges:
|
553
|
+
self.createEdge(item, child.item)
|
554
|
+
self._updatePositions(child, visitedDict, createEdges)
|
555
|
+
else:
|
556
|
+
if createEdges:
|
557
|
+
self._connectParents(node)
|
558
|
+
self._updatePositions(node, visitedDict, createEdges)
|
559
|
+
|
560
|
+
|
561
|
+
def findClosestPoints(list1, list2):
|
562
|
+
candidates = []
|
563
|
+
for c1 in list1:
|
564
|
+
for c2 in list2:
|
565
|
+
candidates.append([c1, c2, math.hypot(c2[0] - c1[0], c2[1] - c1[1])])
|
566
|
+
closestTuple = min(candidates, key=operator.itemgetter(2))
|
567
|
+
return closestTuple[0], closestTuple[1]
|
568
|
+
|
569
|
+
|
570
|
+
def findClosestConnectors(item1, item2):
|
571
|
+
return findUpDownClosestConnectors(item1, item2)
|
572
|
+
|
573
|
+
|
574
|
+
def findUpDownClosestConnectors(item1, item2):
|
575
|
+
srcConnectors = item1.getUpDownConnectorsCoordinates()
|
576
|
+
dstConnectors = item2.getUpDownConnectorsCoordinates()
|
577
|
+
if srcConnectors and dstConnectors:
|
578
|
+
c1Coords, c2Coords = findClosestPoints(srcConnectors, dstConnectors)
|
579
|
+
return c1Coords, c2Coords
|
580
|
+
return None
|
581
|
+
|
582
|
+
|
583
|
+
def findStrictClosestConnectors(item1, item2):
|
584
|
+
srcConnectors = item1.getConnectorsCoordinates()
|
585
|
+
dstConnectors = item2.getConnectorsCoordinates()
|
586
|
+
c1Coords, c2Coords = findClosestPoints(srcConnectors, dstConnectors)
|
587
|
+
return c1Coords, c2Coords
|
588
|
+
|
589
|
+
|
590
|
+
def getConnectors(itemSource, itemDest):
|
591
|
+
srcConnector = itemSource.getOutputConnectorCoordinates()
|
592
|
+
dstConnector = itemDest.getInputConnectorCoordinates()
|
593
|
+
|
594
|
+
return srcConnector, dstConnector
|
595
|
+
|
596
|
+
|
597
|
+
class Item(object):
|
598
|
+
socketSeparation = 12
|
599
|
+
verticalFlow = True
|
600
|
+
|
601
|
+
TOP = 0
|
602
|
+
RIGHT = 1
|
603
|
+
BOTTOM = 2
|
604
|
+
LEFT = 3
|
605
|
+
|
606
|
+
def __init__(self, canvas, x, y):
|
607
|
+
self.activeConnector = None
|
608
|
+
self.canvas = canvas
|
609
|
+
self.x = x
|
610
|
+
self.y = y
|
611
|
+
self.sockets = {}
|
612
|
+
self.listeners = []
|
613
|
+
self.selectionListeners = []
|
614
|
+
self._selected = False
|
615
|
+
|
616
|
+
def getCenter(self, x1, y1, x2, y2):
|
617
|
+
xc = (x2 + x1) / 2.0
|
618
|
+
yc = (y2 + y1) / 2.0
|
619
|
+
return xc, yc
|
620
|
+
|
621
|
+
def getConnectorsCoordinates(self):
|
622
|
+
x1, y1, x2, y2 = self.getCorners()
|
623
|
+
xc, yc = self.getCenter(x1, y1, x2, y2)
|
624
|
+
return [(xc, y1), (x2, yc), (xc, y2), (x1, yc)]
|
625
|
+
|
626
|
+
def getTopConnectorCoordinates(self):
|
627
|
+
|
628
|
+
return self._getConnectorCoordinates(self.TOP)
|
629
|
+
|
630
|
+
def getBottomConnectorCoordinates(self):
|
631
|
+
|
632
|
+
return self._getConnectorCoordinates(self.BOTTOM)
|
633
|
+
|
634
|
+
def getLeftConnectorCoordinates(self):
|
635
|
+
|
636
|
+
return self._getConnectorCoordinates(self.LEFT)
|
637
|
+
|
638
|
+
def getRightConnectorCoordinates(self):
|
639
|
+
|
640
|
+
return self._getConnectorCoordinates(self.RIGHT)
|
641
|
+
|
642
|
+
def _getConnectorCoordinates(self, side):
|
643
|
+
fourConnectors = self.getConnectorsCoordinates()
|
644
|
+
return fourConnectors[side]
|
645
|
+
|
646
|
+
def getInputConnectorCoordinates(self):
|
647
|
+
|
648
|
+
if self.verticalFlow:
|
649
|
+
return self.getTopConnectorCoordinates()
|
650
|
+
else:
|
651
|
+
return self.getLeftConnectorCoordinates()
|
652
|
+
|
653
|
+
def getOutputConnectorCoordinates(self):
|
654
|
+
|
655
|
+
if self.verticalFlow:
|
656
|
+
return self.getBottomConnectorCoordinates()
|
657
|
+
else:
|
658
|
+
return self.getRightConnectorCoordinates()
|
659
|
+
|
660
|
+
def getUpDownConnectorsCoordinates(self):
|
661
|
+
corners = self.getCorners()
|
662
|
+
if corners:
|
663
|
+
x1, y1, x2, y2 = self.getCorners()
|
664
|
+
xc, yc = self.getCenter(x1, y1, x2, y2)
|
665
|
+
return [(xc, y1), (xc, y2)]
|
666
|
+
return None
|
667
|
+
|
668
|
+
def getCorners(self):
|
669
|
+
return self.canvas.bbox(self.id)
|
670
|
+
|
671
|
+
def countSockets(self, verticalLocation):
|
672
|
+
return len(list(self.getSocketsAt(verticalLocation)))
|
673
|
+
|
674
|
+
def addSocket(self, name, socketClass, verticalLocation,
|
675
|
+
fillColor=DEFAULT_CONNECTOR_FILL,
|
676
|
+
outline=DEFAULT_CONNECTOR_OUTLINE,
|
677
|
+
position=None):
|
678
|
+
count = self.countSockets(verticalLocation) + 1
|
679
|
+
if position is None:
|
680
|
+
position = count
|
681
|
+
self.relocateSockets(verticalLocation, count)
|
682
|
+
x, y = self.getSocketCoordsAt(verticalLocation, count, count)
|
683
|
+
self.sockets[name] = {"object": socketClass(self.canvas, x, y, name,
|
684
|
+
fillColor=fillColor,
|
685
|
+
outline=outline),
|
686
|
+
"verticalLocation": verticalLocation,
|
687
|
+
"position": position}
|
688
|
+
self.paintSocket(self.getSocket(name))
|
689
|
+
|
690
|
+
def getSocket(self, name):
|
691
|
+
return self.sockets[name]["object"]
|
692
|
+
|
693
|
+
def getSocketsAt(self, verticalLocation):
|
694
|
+
return filter(lambda s: s["verticalLocation"] == verticalLocation, self.sockets.values())
|
695
|
+
|
696
|
+
def getSocketCoords(self, name):
|
697
|
+
socket = self.sockets[name]
|
698
|
+
return self.getSocketCoordsAt(socket["verticalLocation"], socket["position"],
|
699
|
+
self.countSockets(socket["verticalLocation"]))
|
700
|
+
|
701
|
+
def getSocketCoordsAt(self, verticalLocation, position=1, socketsCount=1):
|
702
|
+
x1, y1, x2, y2 = self.getCorners()
|
703
|
+
xc = (x2 + x1) / 2.0
|
704
|
+
yc = (y1 + y2) / 2.0
|
705
|
+
|
706
|
+
socketsGroupSize = (socketsCount - 1) * self.socketSeparation
|
707
|
+
socketsGroupStart = xc - (socketsGroupSize / 2)
|
708
|
+
x = socketsGroupStart + (position - 1) * self.socketSeparation
|
709
|
+
if verticalLocation == "top":
|
710
|
+
y = y1
|
711
|
+
elif verticalLocation == 'bottom':
|
712
|
+
y = y2
|
713
|
+
elif verticalLocation == 'left':
|
714
|
+
y = yc
|
715
|
+
x = x1
|
716
|
+
else:
|
717
|
+
y = yc
|
718
|
+
x = x2
|
719
|
+
return x, y
|
720
|
+
|
721
|
+
def relocateSockets(self, verticalLocation, count):
|
722
|
+
sockets = self.getSocketsAt(verticalLocation)
|
723
|
+
for socket in sockets:
|
724
|
+
o = socket["object"]
|
725
|
+
x, y = self.getSocketCoordsAt(verticalLocation, socket["position"], count)
|
726
|
+
o.moveTo(x, y)
|
727
|
+
|
728
|
+
def paintSocket(self, socket):
|
729
|
+
# x,y=self.getSocketCoords(socket["name"])
|
730
|
+
socket.paintSocket()
|
731
|
+
|
732
|
+
def paintSockets(self):
|
733
|
+
for name in self.sockets.keys():
|
734
|
+
self.paintSocket(self.getSocket(name))
|
735
|
+
|
736
|
+
def getDimensions(self):
|
737
|
+
x, y, x2, y2 = self.canvas.bbox(self.id)
|
738
|
+
return x2 - x, y2 - y
|
739
|
+
|
740
|
+
def move(self, dx, dy):
|
741
|
+
if hasattr(self, "id"):
|
742
|
+
self.canvas.move(self.id, dx, dy)
|
743
|
+
self.x += dx
|
744
|
+
self.y += dy
|
745
|
+
for name in self.sockets.keys():
|
746
|
+
socket = self.sockets[name]
|
747
|
+
socket["object"].move(dx, dy)
|
748
|
+
for listenerFunc in self.listeners:
|
749
|
+
listenerFunc(dx, dy)
|
750
|
+
|
751
|
+
def moveTo(self, x, y):
|
752
|
+
"""Move TextBox to a new position (x,y)"""
|
753
|
+
self.move(x - self.x, y - self.y)
|
754
|
+
|
755
|
+
def addPositionListener(self, listenerFunc):
|
756
|
+
self.listeners.append(listenerFunc)
|
757
|
+
|
758
|
+
def addSelectionListener(self, listenerFunc):
|
759
|
+
self.selectionListeners.append(listenerFunc)
|
760
|
+
|
761
|
+
def _notifySelectionListeners(self, value):
|
762
|
+
|
763
|
+
for listenerFunc in self.selectionListeners:
|
764
|
+
listenerFunc(value)
|
765
|
+
|
766
|
+
def setSelected(self, value):
|
767
|
+
self._selected = value
|
768
|
+
bw = 0
|
769
|
+
bc = 'black'
|
770
|
+
if value:
|
771
|
+
bw = 2
|
772
|
+
self.lift()
|
773
|
+
# bc = 'Firebrick'
|
774
|
+
else:
|
775
|
+
self.lower()
|
776
|
+
|
777
|
+
self.canvas.itemconfig(self.id, width=bw, outline=bc)
|
778
|
+
self._notifySelectionListeners(value)
|
779
|
+
|
780
|
+
def getSelected(self):
|
781
|
+
return self._selected
|
782
|
+
|
783
|
+
def lift(self):
|
784
|
+
self.canvas.lift(self.id)
|
785
|
+
|
786
|
+
def lower(self):
|
787
|
+
self.canvas.lower(self.id)
|
788
|
+
|
789
|
+
|
790
|
+
class TextItem(Item):
|
791
|
+
"""This class will serve to paint and store rectangle boxes with some text.
|
792
|
+
x and y are the coordinates of the center of this item"""
|
793
|
+
|
794
|
+
def __init__(self, canvas, text, x, y, bgColor, textColor='black'):
|
795
|
+
super(TextItem, self).__init__(canvas, x, y)
|
796
|
+
self.bgColor = bgColor
|
797
|
+
self.textColor = textColor
|
798
|
+
self.text = text
|
799
|
+
self.margin = 8
|
800
|
+
self.paint()
|
801
|
+
|
802
|
+
def _paintBounds(self, x, y, w, h, fillColor):
|
803
|
+
""" Subclasses should implement this method
|
804
|
+
to paint the bounds to the text.
|
805
|
+
Normally the bound are: rectangle or circles.
|
806
|
+
Params:
|
807
|
+
x, y: top left corner of the bounding box
|
808
|
+
w, h: width and height of the box
|
809
|
+
fillColor: color to fill the background
|
810
|
+
Returns:
|
811
|
+
should return the id of the created shape
|
812
|
+
"""
|
813
|
+
pass
|
814
|
+
|
815
|
+
def paint(self):
|
816
|
+
"""Paint the object in a specific position."""
|
817
|
+
|
818
|
+
self.id_text = self.canvas.create_text(self.x, self.y, text=self.text,
|
819
|
+
justify=tk.CENTER, fill=self.textColor,
|
820
|
+
font=self.canvas.getRunsFont())
|
821
|
+
|
822
|
+
xr, yr, w, h = self.canvas.bbox(self.id_text)
|
823
|
+
m = self.margin
|
824
|
+
|
825
|
+
self.id = self._paintBounds(xr - m, yr - m, w + m, h + m, fillColor=self.bgColor)
|
826
|
+
self.canvas.lift(self.id_text)
|
827
|
+
|
828
|
+
def move(self, dx, dy):
|
829
|
+
super(TextItem, self).move(dx, dy)
|
830
|
+
self.canvas.move(self.id_text, dx, dy)
|
831
|
+
|
832
|
+
def lift(self):
|
833
|
+
super(TextItem, self).lift()
|
834
|
+
self.canvas.lift(self.id_text)
|
835
|
+
|
836
|
+
def lower(self):
|
837
|
+
self.canvas.lower(self.id_text)
|
838
|
+
super(TextItem, self).lower()
|
839
|
+
|
840
|
+
|
841
|
+
|
842
|
+
class TextBox(TextItem):
|
843
|
+
def __init__(self, canvas, text, x, y, bgColor, textColor='black'):
|
844
|
+
super(TextBox, self).__init__(canvas, text, x, y, bgColor, textColor)
|
845
|
+
|
846
|
+
def _paintBounds(self, x, y, w, h, fillColor):
|
847
|
+
return self.canvas.create_rectangle(x, y, w, h, fill=fillColor, outline=fillColor)
|
848
|
+
|
849
|
+
|
850
|
+
class RoundedTextBox(TextItem):
|
851
|
+
def __init__(self, canvas, text, x, y, bgColor, textColor='black'):
|
852
|
+
super(RoundedTextBox, self).__init__(canvas, text, x, y, bgColor, textColor)
|
853
|
+
|
854
|
+
def _paintBounds(self, upperLeftX, upperLeftY, bottomRightX, bottomRightY, fillColor):
|
855
|
+
d = 5
|
856
|
+
# When smooth=1, you define a straight segment by including its ends twice
|
857
|
+
return self.canvas.create_polygon(upperLeftX + d + 1, upperLeftY, # 1
|
858
|
+
upperLeftX + d, upperLeftY, # 1
|
859
|
+
bottomRightX - d, upperLeftY, # 2
|
860
|
+
bottomRightX - d, upperLeftY, # 2
|
861
|
+
# bottomRightX-d+1,upperLeftY, #2b
|
862
|
+
bottomRightX, upperLeftY + d - 1, # 3b
|
863
|
+
bottomRightX, upperLeftY + d, # 3
|
864
|
+
bottomRightX, upperLeftY + d, # 3
|
865
|
+
bottomRightX, bottomRightY - d, # 4
|
866
|
+
bottomRightX, bottomRightY - d, # 4
|
867
|
+
bottomRightX - d, bottomRightY, # 5
|
868
|
+
bottomRightX - d, bottomRightY, # 5
|
869
|
+
upperLeftX + d, bottomRightY, # 6
|
870
|
+
upperLeftX + d, bottomRightY, # 6
|
871
|
+
# upperLeftX+d-1,bottomRightY, #6b
|
872
|
+
upperLeftX, bottomRightY - d + 1, # 7b
|
873
|
+
upperLeftX, bottomRightY - d, # 7
|
874
|
+
upperLeftX, bottomRightY - d, # 7
|
875
|
+
upperLeftX, upperLeftY + d, # 8
|
876
|
+
upperLeftX, upperLeftY + d, # 8
|
877
|
+
# upperLeftX, upperLeftY+d-1, #8b
|
878
|
+
upperLeftX + d - 1, upperLeftY, # 1b
|
879
|
+
fill=fillColor, outline='black', smooth=1)
|
880
|
+
|
881
|
+
def getDimensions(self):
|
882
|
+
return self.canvas.bbox(self.id)
|
883
|
+
|
884
|
+
|
885
|
+
class TextCircle(TextItem):
|
886
|
+
def __init__(self, canvas, text, x, y, bgColor, textColor='black'):
|
887
|
+
super(TextCircle, self).__init__(canvas, text, x, y, bgColor, textColor)
|
888
|
+
|
889
|
+
def _paintBounds(self, x, y, w, h, fillColor):
|
890
|
+
return self.canvas.create_oval(x, y, w, h, fill=fillColor)
|
891
|
+
|
892
|
+
|
893
|
+
# This are not used and depends on xmippLib
|
894
|
+
# class ImageBox(Item):
|
895
|
+
# def __init__(self, canvas, imgPath, x=0, y=0, text=None):
|
896
|
+
# Item.__init__(self, canvas, x, y)
|
897
|
+
# # Create the image
|
898
|
+
# from pyworkflow.gui import getImage, getImageFromPath
|
899
|
+
#
|
900
|
+
# if imgPath is None:
|
901
|
+
# self.image = getImage('no-image.gif')
|
902
|
+
# else:
|
903
|
+
# self.image = getImageFromPath(imgPath)
|
904
|
+
#
|
905
|
+
# if text is not None:
|
906
|
+
# self.label = tk.Label(canvas, image=self.image, text=text,
|
907
|
+
# compound=tk.TOP, bg='gray')
|
908
|
+
# self.id = self.canvas.create_window(x, y, window=self.label)
|
909
|
+
# self.label.bind('<Button-1>', self._onClick)
|
910
|
+
# else:
|
911
|
+
# self.id = self.canvas.create_image(x, y, image=self.image)
|
912
|
+
#
|
913
|
+
#
|
914
|
+
# def setSelected(self, value): #Ignore selection highlight
|
915
|
+
# pass
|
916
|
+
#
|
917
|
+
# def _onClick(self, e=None):
|
918
|
+
# pass
|
919
|
+
|
920
|
+
|
921
|
+
class Connector(Item):
|
922
|
+
""" Default connector has no graphical representation (hence, it'ss invisible). Subclasses offer different looks"""
|
923
|
+
|
924
|
+
def __init__(self, canvas, x, y, name):
|
925
|
+
super(Connector, self).__init__(canvas, x, y)
|
926
|
+
self.name = name
|
927
|
+
|
928
|
+
def paintSocket(self):
|
929
|
+
"""Should be implemented by the subclasses"""
|
930
|
+
pass
|
931
|
+
|
932
|
+
def paintPlug(self, canvas, x, y):
|
933
|
+
"""Should be implemented by the subclasses"""
|
934
|
+
pass
|
935
|
+
|
936
|
+
def move(self, dx, dy):
|
937
|
+
super(Connector, self).move(dx, dy)
|
938
|
+
if hasattr(self, "socketId"):
|
939
|
+
self.canvas.move(self.socketId, dx, dy)
|
940
|
+
if hasattr(self, "plugId"):
|
941
|
+
self.canvas.move(self.plugId, dx, dy)
|
942
|
+
|
943
|
+
|
944
|
+
class ColoredConnector(Connector):
|
945
|
+
def __init__(self, canvas, x, y, name, fillColor=DEFAULT_CONNECTOR_FILL,
|
946
|
+
outline=DEFAULT_CONNECTOR_OUTLINE):
|
947
|
+
super(ColoredConnector, self).__init__(canvas, x, y, name)
|
948
|
+
self.fillColor = fillColor
|
949
|
+
self.outline = outline
|
950
|
+
|
951
|
+
|
952
|
+
class RoundConnector(ColoredConnector):
|
953
|
+
radius = 3
|
954
|
+
|
955
|
+
def paintSocket(self):
|
956
|
+
self.socketId = self.canvas.create_oval(self.x - self.radius,
|
957
|
+
self.y - self.radius,
|
958
|
+
self.x + self.radius,
|
959
|
+
self.y + self.radius,
|
960
|
+
outline=self.outline)
|
961
|
+
|
962
|
+
def paintPlug(self):
|
963
|
+
self.plugId = self.canvas.create_oval(self.x - self.radius,
|
964
|
+
self.y - self.radius,
|
965
|
+
self.x + self.radius,
|
966
|
+
self.y + self.radius,
|
967
|
+
fill=self.fillColor, width=0)
|
968
|
+
|
969
|
+
|
970
|
+
class SquareConnector(ColoredConnector):
|
971
|
+
halfside = 3
|
972
|
+
|
973
|
+
def paintSocket(self):
|
974
|
+
self.socketId = self.canvas.create_rectangle(self.x - self.halfside,
|
975
|
+
self.y - self.halfside,
|
976
|
+
self.x + self.halfside,
|
977
|
+
self.y + self.halfside,
|
978
|
+
outline=self.outline)
|
979
|
+
|
980
|
+
def paintPlug(self):
|
981
|
+
self.plugId = self.canvas.create_rectangle(self.x - self.halfside,
|
982
|
+
self.y - self.halfside,
|
983
|
+
self.x + self.halfside,
|
984
|
+
self.y + self.halfside,
|
985
|
+
fill=self.fillColor, width=0)
|
986
|
+
|
987
|
+
|
988
|
+
# !!!! other figures: half circle, diamond...
|
989
|
+
class Oval:
|
990
|
+
"""Oval or circle"""
|
991
|
+
|
992
|
+
def __init__(self, canvas, x, y, radio, color='green', anchor=None):
|
993
|
+
self.anchor = anchor
|
994
|
+
self.X, self.Y = x, y
|
995
|
+
self.radio = radio
|
996
|
+
self.color = color
|
997
|
+
self.canvas = canvas
|
998
|
+
anchor.addPositionListener(self.updateSrc)
|
999
|
+
anchor.addSelectionListener(self.selectionListener)
|
1000
|
+
self.id = None
|
1001
|
+
self.paint()
|
1002
|
+
|
1003
|
+
def paint(self):
|
1004
|
+
|
1005
|
+
if self.id:
|
1006
|
+
self.canvas.delete(self.id)
|
1007
|
+
|
1008
|
+
self.id = self.canvas.create_oval(self.X, self.Y, self.X + self.radio,
|
1009
|
+
self.Y + self.radio, fill=self.color,
|
1010
|
+
outline='black')
|
1011
|
+
# self.canvas.tag_raise(self.id)
|
1012
|
+
|
1013
|
+
def updateSrc(self, dx, dy):
|
1014
|
+
self.X += dx
|
1015
|
+
self.Y += dy
|
1016
|
+
self.paint()
|
1017
|
+
|
1018
|
+
def selectionListener(self, value):
|
1019
|
+
if value:
|
1020
|
+
self.canvas.lift(self.id)
|
1021
|
+
|
1022
|
+
|
1023
|
+
class Rectangle:
|
1024
|
+
def __init__(self, canvas, x, y, width, height=None, color='green', anchor=None):
|
1025
|
+
|
1026
|
+
self.anchor = anchor
|
1027
|
+
self.X, self.Y = x, y
|
1028
|
+
self.width = width
|
1029
|
+
self.height = height or width
|
1030
|
+
self.color = color
|
1031
|
+
self.canvas = canvas
|
1032
|
+
anchor.addPositionListener(self.updateSrc)
|
1033
|
+
anchor.addSelectionListener(self.selectionListener)
|
1034
|
+
self.id = None
|
1035
|
+
self.paint()
|
1036
|
+
|
1037
|
+
def paint(self):
|
1038
|
+
|
1039
|
+
if self.id:
|
1040
|
+
self.canvas.delete(self.id)
|
1041
|
+
|
1042
|
+
self.id = self.canvas.create_rectangle(self.X, self.Y, self.X + self.width,
|
1043
|
+
self.Y + self.height, fill=self.color,
|
1044
|
+
outline=self.color)
|
1045
|
+
# self.canvas.tag_raise(self.id)
|
1046
|
+
|
1047
|
+
def updateSrc(self, dx, dy):
|
1048
|
+
self.X += dx
|
1049
|
+
self.Y += dy
|
1050
|
+
self.paint()
|
1051
|
+
|
1052
|
+
def selectionListener(self, value):
|
1053
|
+
if value:
|
1054
|
+
self.canvas.lift(self.id)
|
1055
|
+
|
1056
|
+
|
1057
|
+
class Edge:
|
1058
|
+
"""Edge between two objects"""
|
1059
|
+
|
1060
|
+
def __init__(self, canvas, source, dest):
|
1061
|
+
self.source = source
|
1062
|
+
self.dest = dest
|
1063
|
+
self.srcX, self.srcY = source.x, source.y
|
1064
|
+
self.dstX, self.dstY = dest.x, dest.y
|
1065
|
+
self.canvas = canvas
|
1066
|
+
source.addPositionListener(self.updateSrc)
|
1067
|
+
dest.addPositionListener(self.updateDst)
|
1068
|
+
|
1069
|
+
source.addSelectionListener(self.updateColor)
|
1070
|
+
dest.addSelectionListener(self.updateColor)
|
1071
|
+
self.id = None
|
1072
|
+
self.paint()
|
1073
|
+
|
1074
|
+
def paint(self):
|
1075
|
+
# coords = findClosestConnectors(self.source,self.dest)
|
1076
|
+
coords = getConnectors(self.source, self.dest)
|
1077
|
+
|
1078
|
+
if coords:
|
1079
|
+
c1Coords, c2Coords = coords
|
1080
|
+
|
1081
|
+
if self.id:
|
1082
|
+
self.canvas.delete(self.id)
|
1083
|
+
|
1084
|
+
lineColor = '#ccc'
|
1085
|
+
lineWidth = 1
|
1086
|
+
if not self.canvas.multipleItemsSelected():
|
1087
|
+
if self.dest.getSelected():
|
1088
|
+
lineColor = '#000'
|
1089
|
+
lineWidth = 2
|
1090
|
+
elif self.source.getSelected():
|
1091
|
+
lineColor = '#b22222'
|
1092
|
+
lineWidth = 2
|
1093
|
+
else:
|
1094
|
+
if self.dest.getSelected() and self.source.getSelected():
|
1095
|
+
lineColor = '#000'
|
1096
|
+
lineWidth = 2
|
1097
|
+
|
1098
|
+
self.id = self.canvas.create_line(c1Coords[0], c1Coords[1],
|
1099
|
+
c2Coords[0], c2Coords[1],
|
1100
|
+
width=lineWidth, fill=lineColor)
|
1101
|
+
self.canvas.tag_lower(self.id)
|
1102
|
+
|
1103
|
+
def updateSrc(self, dx, dy):
|
1104
|
+
self.srcX += dx
|
1105
|
+
self.srcY += dy
|
1106
|
+
self.paint()
|
1107
|
+
|
1108
|
+
def updateDst(self, dx, dy):
|
1109
|
+
self.dstX += dx
|
1110
|
+
self.dstY += dy
|
1111
|
+
self.paint()
|
1112
|
+
|
1113
|
+
def updateColor(self, value):
|
1114
|
+
self.paint()
|
1115
|
+
|
1116
|
+
|
1117
|
+
# !!!! Interaction: allow to reconnect cables dynamically
|
1118
|
+
|
1119
|
+
# !!!! Antialiasing for the line - it seems TkInter does not support antialiasing...
|
1120
|
+
# Although Tk 8.5 supports anti-aliasing if the Cairo library is installed:
|
1121
|
+
# @see http://wiki.tcl.tk/10630
|
1122
|
+
|
1123
|
+
class Cable:
|
1124
|
+
def __init__(self, canvas, src, srcConnector, dst, dstConnector):
|
1125
|
+
self.id = None
|
1126
|
+
self.canvas = canvas
|
1127
|
+
self.srcPlug = src.getSocket(srcConnector)
|
1128
|
+
self.dstPlug = dst.getSocket(dstConnector)
|
1129
|
+
self.srcX = self.srcPlug.x
|
1130
|
+
self.srcY = self.srcPlug.y
|
1131
|
+
self.dstX, self.dstY = dst.getSocketCoords(dstConnector)
|
1132
|
+
src.addPositionListener(self.srcMoved)
|
1133
|
+
dst.addPositionListener(self.dstMoved)
|
1134
|
+
self.paint()
|
1135
|
+
|
1136
|
+
def srcMoved(self, dx, dy):
|
1137
|
+
self.srcX = self.srcX + dx
|
1138
|
+
self.srcY = self.srcY + dy
|
1139
|
+
self.updateCoords()
|
1140
|
+
|
1141
|
+
def updateCoords(self):
|
1142
|
+
self.canvas.coords(self.id, self.srcX, self.srcY, self.dstX, self.dstY)
|
1143
|
+
|
1144
|
+
def dstMoved(self, dx, dy):
|
1145
|
+
self.dstX = self.dstX + dx
|
1146
|
+
self.dstY = self.dstY + dy
|
1147
|
+
self.updateCoords()
|
1148
|
+
|
1149
|
+
def paint(self):
|
1150
|
+
self.id = self.canvas.create_line(self.srcX, self.srcY, self.dstX,
|
1151
|
+
self.dstY, width=2)
|
1152
|
+
self.canvas.tag_lower(self.id)
|
1153
|
+
self.paintPlugs()
|
1154
|
+
|
1155
|
+
def paintPlugs(self):
|
1156
|
+
self.srcPlug.paintPlug()
|
1157
|
+
self.dstPlug.paintPlug()
|
1158
|
+
|
1159
|
+
|
1160
|
+
if __name__ == '__main__':
|
1161
|
+
root = tk.Tk()
|
1162
|
+
canvas = Canvas(root, width=800, height=600)
|
1163
|
+
canvas.frame.grid(row=0, column=0, sticky='nsew')
|
1164
|
+
root.grid_columnconfigure(0, weight=1)
|
1165
|
+
root.grid_rowconfigure(0, weight=1)
|
1166
|
+
|
1167
|
+
|
1168
|
+
def canvasExample1():
|
1169
|
+
tb1 = canvas.createTextCircle("Project", 100, 100, "blue")
|
1170
|
+
tb2 = canvas.createTextbox("This is an intentionally quite big, big box,"
|
1171
|
+
"\nas you may appreciate looking carefully"
|
1172
|
+
"\nat it,\nas many times\nas you might need", 300, 200)
|
1173
|
+
tb2.addSocket("output1", RoundConnector, "bottom", fillColor="green")
|
1174
|
+
tb2.addSocket("output2", SquareConnector, "bottom", fillColor="yellow")
|
1175
|
+
tb2.addSocket("output3", SquareConnector, "bottom", fillColor="blue")
|
1176
|
+
tb3 = canvas.createRoundedTextbox("another one\n", 100, 200, "red")
|
1177
|
+
tb4 = canvas.createRoundedTextbox("tb4", 300, 300, "yellow")
|
1178
|
+
tb4.addSocket("input1", SquareConnector, "top", outline="red")
|
1179
|
+
tb5 = canvas.createTextCircle("tb5", 400, 300, "grey")
|
1180
|
+
tb5.addSocket("input1", SquareConnector, "top")
|
1181
|
+
e1 = canvas.createEdge(tb1, tb2)
|
1182
|
+
e2 = canvas.createEdge(tb1, tb3)
|
1183
|
+
c1 = canvas.createCable(tb2, "output2", tb4, "input1")
|
1184
|
+
c2 = canvas.createCable(tb2, "output3", tb5, "input1")
|
1185
|
+
tb3.moveTo(100, 300)
|
1186
|
+
|
1187
|
+
|
1188
|
+
canvasExample1()
|
1189
|
+
|
1190
|
+
root.mainloop()
|