scipion-pyworkflow 3.7.0__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.
Files changed (140) hide show
  1. pyworkflow/__init__.py +33 -0
  2. pyworkflow/apps/__init__.py +29 -0
  3. pyworkflow/apps/pw_manager.py +37 -0
  4. pyworkflow/apps/pw_plot.py +51 -0
  5. pyworkflow/apps/pw_project.py +113 -0
  6. pyworkflow/apps/pw_protocol_list.py +143 -0
  7. pyworkflow/apps/pw_protocol_run.py +51 -0
  8. pyworkflow/apps/pw_run_tests.py +267 -0
  9. pyworkflow/apps/pw_schedule_run.py +322 -0
  10. pyworkflow/apps/pw_sleep.py +37 -0
  11. pyworkflow/apps/pw_sync_data.py +439 -0
  12. pyworkflow/apps/pw_viewer.py +78 -0
  13. pyworkflow/config.py +536 -0
  14. pyworkflow/constants.py +212 -0
  15. pyworkflow/exceptions.py +18 -0
  16. pyworkflow/gui/__init__.py +36 -0
  17. pyworkflow/gui/browser.py +726 -0
  18. pyworkflow/gui/canvas.py +1190 -0
  19. pyworkflow/gui/dialog.py +976 -0
  20. pyworkflow/gui/form.py +2627 -0
  21. pyworkflow/gui/graph.py +247 -0
  22. pyworkflow/gui/graph_layout.py +271 -0
  23. pyworkflow/gui/gui.py +566 -0
  24. pyworkflow/gui/matplotlib_image.py +233 -0
  25. pyworkflow/gui/plotter.py +247 -0
  26. pyworkflow/gui/project/__init__.py +25 -0
  27. pyworkflow/gui/project/base.py +192 -0
  28. pyworkflow/gui/project/constants.py +139 -0
  29. pyworkflow/gui/project/labels.py +205 -0
  30. pyworkflow/gui/project/project.py +484 -0
  31. pyworkflow/gui/project/searchprotocol.py +154 -0
  32. pyworkflow/gui/project/searchrun.py +181 -0
  33. pyworkflow/gui/project/steps.py +166 -0
  34. pyworkflow/gui/project/utils.py +332 -0
  35. pyworkflow/gui/project/variables.py +179 -0
  36. pyworkflow/gui/project/viewdata.py +472 -0
  37. pyworkflow/gui/project/viewprojects.py +510 -0
  38. pyworkflow/gui/project/viewprotocols.py +2093 -0
  39. pyworkflow/gui/project/viewprotocols_extra.py +560 -0
  40. pyworkflow/gui/text.py +771 -0
  41. pyworkflow/gui/tooltip.py +185 -0
  42. pyworkflow/gui/tree.py +684 -0
  43. pyworkflow/gui/widgets.py +307 -0
  44. pyworkflow/mapper/__init__.py +26 -0
  45. pyworkflow/mapper/mapper.py +222 -0
  46. pyworkflow/mapper/sqlite.py +1578 -0
  47. pyworkflow/mapper/sqlite_db.py +145 -0
  48. pyworkflow/object.py +1512 -0
  49. pyworkflow/plugin.py +712 -0
  50. pyworkflow/project/__init__.py +31 -0
  51. pyworkflow/project/config.py +451 -0
  52. pyworkflow/project/manager.py +179 -0
  53. pyworkflow/project/project.py +1990 -0
  54. pyworkflow/project/scripts/clean_projects.py +77 -0
  55. pyworkflow/project/scripts/config.py +92 -0
  56. pyworkflow/project/scripts/create.py +77 -0
  57. pyworkflow/project/scripts/edit_workflow.py +90 -0
  58. pyworkflow/project/scripts/fix_links.py +39 -0
  59. pyworkflow/project/scripts/load.py +87 -0
  60. pyworkflow/project/scripts/refresh.py +83 -0
  61. pyworkflow/project/scripts/schedule.py +111 -0
  62. pyworkflow/project/scripts/stack2volume.py +41 -0
  63. pyworkflow/project/scripts/stop.py +81 -0
  64. pyworkflow/protocol/__init__.py +38 -0
  65. pyworkflow/protocol/bibtex.py +48 -0
  66. pyworkflow/protocol/constants.py +86 -0
  67. pyworkflow/protocol/executor.py +334 -0
  68. pyworkflow/protocol/hosts.py +313 -0
  69. pyworkflow/protocol/launch.py +270 -0
  70. pyworkflow/protocol/package.py +42 -0
  71. pyworkflow/protocol/params.py +744 -0
  72. pyworkflow/protocol/protocol.py +2554 -0
  73. pyworkflow/resources/Imagej.png +0 -0
  74. pyworkflow/resources/chimera.png +0 -0
  75. pyworkflow/resources/fa-exclamation-triangle_alert.png +0 -0
  76. pyworkflow/resources/fa-info-circle_alert.png +0 -0
  77. pyworkflow/resources/fa-search.png +0 -0
  78. pyworkflow/resources/fa-times-circle_alert.png +0 -0
  79. pyworkflow/resources/file_vol.png +0 -0
  80. pyworkflow/resources/loading.gif +0 -0
  81. pyworkflow/resources/no-image128.png +0 -0
  82. pyworkflow/resources/scipion_bn.png +0 -0
  83. pyworkflow/resources/scipion_icon.png +0 -0
  84. pyworkflow/resources/scipion_icon.svg +397 -0
  85. pyworkflow/resources/scipion_icon_proj.png +0 -0
  86. pyworkflow/resources/scipion_icon_projs.png +0 -0
  87. pyworkflow/resources/scipion_icon_prot.png +0 -0
  88. pyworkflow/resources/scipion_logo.png +0 -0
  89. pyworkflow/resources/scipion_logo_normal.png +0 -0
  90. pyworkflow/resources/scipion_logo_small.png +0 -0
  91. pyworkflow/resources/sprites.png +0 -0
  92. pyworkflow/resources/sprites.xcf +0 -0
  93. pyworkflow/resources/wait.gif +0 -0
  94. pyworkflow/template.py +322 -0
  95. pyworkflow/tests/__init__.py +29 -0
  96. pyworkflow/tests/test_utils.py +25 -0
  97. pyworkflow/tests/tests.py +341 -0
  98. pyworkflow/utils/__init__.py +38 -0
  99. pyworkflow/utils/dataset.py +414 -0
  100. pyworkflow/utils/echo.py +104 -0
  101. pyworkflow/utils/graph.py +196 -0
  102. pyworkflow/utils/log.py +284 -0
  103. pyworkflow/utils/path.py +527 -0
  104. pyworkflow/utils/process.py +132 -0
  105. pyworkflow/utils/profiler.py +92 -0
  106. pyworkflow/utils/progressbar.py +154 -0
  107. pyworkflow/utils/properties.py +627 -0
  108. pyworkflow/utils/reflection.py +129 -0
  109. pyworkflow/utils/utils.py +877 -0
  110. pyworkflow/utils/which.py +229 -0
  111. pyworkflow/viewer.py +328 -0
  112. pyworkflow/webservices/__init__.py +8 -0
  113. pyworkflow/webservices/config.py +11 -0
  114. pyworkflow/webservices/notifier.py +162 -0
  115. pyworkflow/webservices/repository.py +59 -0
  116. pyworkflow/webservices/workflowhub.py +74 -0
  117. pyworkflow/wizard.py +64 -0
  118. pyworkflowtests/__init__.py +51 -0
  119. pyworkflowtests/bibtex.py +51 -0
  120. pyworkflowtests/objects.py +830 -0
  121. pyworkflowtests/protocols.py +154 -0
  122. pyworkflowtests/tests/__init__.py +0 -0
  123. pyworkflowtests/tests/test_canvas.py +72 -0
  124. pyworkflowtests/tests/test_domain.py +45 -0
  125. pyworkflowtests/tests/test_logs.py +74 -0
  126. pyworkflowtests/tests/test_mappers.py +392 -0
  127. pyworkflowtests/tests/test_object.py +507 -0
  128. pyworkflowtests/tests/test_project.py +42 -0
  129. pyworkflowtests/tests/test_protocol_execution.py +72 -0
  130. pyworkflowtests/tests/test_protocol_export.py +78 -0
  131. pyworkflowtests/tests/test_protocol_output.py +158 -0
  132. pyworkflowtests/tests/test_streaming.py +47 -0
  133. pyworkflowtests/tests/test_utils.py +210 -0
  134. scipion_pyworkflow-3.7.0.dist-info/LICENSE.txt +674 -0
  135. scipion_pyworkflow-3.7.0.dist-info/METADATA +107 -0
  136. scipion_pyworkflow-3.7.0.dist-info/RECORD +140 -0
  137. scipion_pyworkflow-3.7.0.dist-info/WHEEL +5 -0
  138. scipion_pyworkflow-3.7.0.dist-info/dependency_links.txt +1 -0
  139. scipion_pyworkflow-3.7.0.dist-info/entry_points.txt +5 -0
  140. scipion_pyworkflow-3.7.0.dist-info/top_level.txt +2 -0
@@ -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("<Shift-Button-4>", self.zoomerP)
86
+ self.bind("<Shift-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.getChilds():
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.getChilds():
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.getChilds():
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.getChilds():
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()