scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.2__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 (104) hide show
  1. pyworkflow/apps/__init__.py +29 -0
  2. pyworkflow/apps/pw_manager.py +37 -0
  3. pyworkflow/apps/pw_plot.py +51 -0
  4. pyworkflow/apps/pw_project.py +130 -0
  5. pyworkflow/apps/pw_protocol_list.py +143 -0
  6. pyworkflow/apps/pw_protocol_run.py +51 -0
  7. pyworkflow/apps/pw_run_tests.py +268 -0
  8. pyworkflow/apps/pw_schedule_run.py +322 -0
  9. pyworkflow/apps/pw_sleep.py +37 -0
  10. pyworkflow/apps/pw_sync_data.py +440 -0
  11. pyworkflow/apps/pw_viewer.py +78 -0
  12. pyworkflow/constants.py +1 -1
  13. pyworkflow/gui/__init__.py +36 -0
  14. pyworkflow/gui/browser.py +768 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +981 -0
  17. pyworkflow/gui/form.py +2727 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +571 -0
  21. pyworkflow/gui/matplotlib_image.py +233 -0
  22. pyworkflow/gui/plotter.py +247 -0
  23. pyworkflow/gui/project/__init__.py +25 -0
  24. pyworkflow/gui/project/base.py +193 -0
  25. pyworkflow/gui/project/constants.py +139 -0
  26. pyworkflow/gui/project/labels.py +205 -0
  27. pyworkflow/gui/project/project.py +491 -0
  28. pyworkflow/gui/project/searchprotocol.py +240 -0
  29. pyworkflow/gui/project/searchrun.py +181 -0
  30. pyworkflow/gui/project/steps.py +171 -0
  31. pyworkflow/gui/project/utils.py +332 -0
  32. pyworkflow/gui/project/variables.py +179 -0
  33. pyworkflow/gui/project/viewdata.py +472 -0
  34. pyworkflow/gui/project/viewprojects.py +519 -0
  35. pyworkflow/gui/project/viewprotocols.py +2141 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +774 -0
  38. pyworkflow/gui/tooltip.py +185 -0
  39. pyworkflow/gui/tree.py +684 -0
  40. pyworkflow/gui/widgets.py +307 -0
  41. pyworkflow/mapper/__init__.py +26 -0
  42. pyworkflow/mapper/mapper.py +226 -0
  43. pyworkflow/mapper/sqlite.py +1583 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/object.py +1 -0
  46. pyworkflow/plugin.py +4 -4
  47. pyworkflow/project/__init__.py +31 -0
  48. pyworkflow/project/config.py +454 -0
  49. pyworkflow/project/manager.py +180 -0
  50. pyworkflow/project/project.py +2095 -0
  51. pyworkflow/project/usage.py +165 -0
  52. pyworkflow/protocol/__init__.py +38 -0
  53. pyworkflow/protocol/bibtex.py +48 -0
  54. pyworkflow/protocol/constants.py +87 -0
  55. pyworkflow/protocol/executor.py +515 -0
  56. pyworkflow/protocol/hosts.py +318 -0
  57. pyworkflow/protocol/launch.py +277 -0
  58. pyworkflow/protocol/package.py +42 -0
  59. pyworkflow/protocol/params.py +781 -0
  60. pyworkflow/protocol/protocol.py +2712 -0
  61. pyworkflow/resources/protlabels.xcf +0 -0
  62. pyworkflow/resources/sprites.png +0 -0
  63. pyworkflow/resources/sprites.xcf +0 -0
  64. pyworkflow/template.py +1 -1
  65. pyworkflow/tests/__init__.py +29 -0
  66. pyworkflow/tests/test_utils.py +25 -0
  67. pyworkflow/tests/tests.py +342 -0
  68. pyworkflow/utils/__init__.py +38 -0
  69. pyworkflow/utils/dataset.py +414 -0
  70. pyworkflow/utils/echo.py +104 -0
  71. pyworkflow/utils/graph.py +169 -0
  72. pyworkflow/utils/log.py +293 -0
  73. pyworkflow/utils/path.py +528 -0
  74. pyworkflow/utils/process.py +154 -0
  75. pyworkflow/utils/profiler.py +92 -0
  76. pyworkflow/utils/progressbar.py +154 -0
  77. pyworkflow/utils/properties.py +618 -0
  78. pyworkflow/utils/reflection.py +129 -0
  79. pyworkflow/utils/utils.py +880 -0
  80. pyworkflow/utils/which.py +229 -0
  81. pyworkflow/webservices/__init__.py +8 -0
  82. pyworkflow/webservices/config.py +8 -0
  83. pyworkflow/webservices/notifier.py +152 -0
  84. pyworkflow/webservices/repository.py +59 -0
  85. pyworkflow/webservices/workflowhub.py +86 -0
  86. pyworkflowtests/tests/__init__.py +0 -0
  87. pyworkflowtests/tests/test_canvas.py +72 -0
  88. pyworkflowtests/tests/test_domain.py +45 -0
  89. pyworkflowtests/tests/test_logs.py +74 -0
  90. pyworkflowtests/tests/test_mappers.py +392 -0
  91. pyworkflowtests/tests/test_object.py +507 -0
  92. pyworkflowtests/tests/test_project.py +42 -0
  93. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  94. pyworkflowtests/tests/test_protocol_export.py +78 -0
  95. pyworkflowtests/tests/test_protocol_output.py +158 -0
  96. pyworkflowtests/tests/test_streaming.py +47 -0
  97. pyworkflowtests/tests/test_utils.py +210 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
  99. scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
  100. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  101. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
  102. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
  103. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
  104. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/top_level.txt +0 -0
pyworkflow/gui/gui.py ADDED
@@ -0,0 +1,571 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
4
+ # *
5
+ # * [1] SciLifeLab, Stockholm University
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, see <https://www.gnu.org/licenses/>.
19
+ # *
20
+ # * All comments concerning this program package may be sent to the
21
+ # * e-mail address 'scipion@cnb.csic.es'
22
+ # *
23
+ # **************************************************************************
24
+ import os
25
+ import tkinter as tk
26
+ import tkinter.font as tkFont
27
+ import queue
28
+ from functools import partial
29
+ from tkinter.ttk import Style
30
+
31
+ import pyworkflow
32
+ import pyworkflow as pw
33
+ from pyworkflow.object import Object
34
+ from pyworkflow.utils import Message, Icon
35
+ from PIL import Image, ImageTk
36
+
37
+ from pyworkflow.utils import SpriteImage, Sprite
38
+ from .widgets import Button
39
+ import numpy as np
40
+
41
+ DEFAULT_WINDOW_CLASS = "Scipion Framework"
42
+
43
+ # --------------- GUI CONFIGURATION parameters -----------------------
44
+ # TODO: read font size and name from config file
45
+ FONT_ITALIC = 'fontItalic'
46
+ FONT_NORMAL = 'fontNormal'
47
+ FONT_BOLD = 'fontBold'
48
+ FONT_BIG = 'fontBig'
49
+ # TextColor
50
+ # cfgCitationTextColor = "dark olive green"
51
+ # cfgLabelTextColor = "black"
52
+ # cfgSectionTextColor = "blue4"
53
+ # Background Color
54
+ # cfgBgColor = "light grey"
55
+ # cfgLabelBgColor = "white"
56
+ # cfgHighlightBgColor = cfgBgColor
57
+ #This with trigger the validation of the color falling back the firebrick if fails
58
+ cfgButtonActiveBgColor = pw.Config.getActiveColor()
59
+ cfgButtonFgColor = pw.Config.SCIPION_BG_COLOR
60
+ cfgButtonActiveFgColor = pw.Config.SCIPION_BG_COLOR
61
+ cfgButtonBgColor = pw.Config.SCIPION_MAIN_COLOR
62
+ cfgEntryBgColor = "lemon chiffon"
63
+ # cfgExpertLabelBgColor = "light salmon"
64
+ # cfgSectionBgColor = cfgButtonBgColor
65
+ # Color
66
+ # cfgListSelectColor = "DeepSkyBlue4"
67
+ # cfgBooleanSelectColor = "white"
68
+ # cfgButtonSelectColor = "DeepSkyBlue2"
69
+ # Dimensions limits
70
+ # cfgMaxHeight = 650
71
+ cfgMaxWidth = 800
72
+ # cfgMaxFontSize = 14
73
+ # cfgMinFontSize = 6
74
+ cfgWrapLenght = cfgMaxWidth - 50
75
+
76
+ # Style of treeviews where row height is variable based on the font size
77
+ LIST_TREEVIEW = 'List.Treeview'
78
+ BORDERLESS_TREEVIEW = 'Borderless.Treeview'
79
+
80
+ image_cache = dict()
81
+
82
+ class Config(Object):
83
+ pass
84
+
85
+
86
+ def saveConfig(filename):
87
+ from pyworkflow.mapper import SqliteMapper
88
+ from pyworkflow.object import String, Integer
89
+
90
+ mapper = SqliteMapper(filename)
91
+ o = Config()
92
+ for k, v in globals().items():
93
+ if k.startswith('cfg'):
94
+ if type(v) is str:
95
+ value = String(v)
96
+ else:
97
+ value = Integer(v)
98
+ setattr(o, k, value)
99
+ mapper.insert(o)
100
+ mapper.commit()
101
+
102
+
103
+ # --------------- FONT related variables and functions -----------------------
104
+ def setFont(fontKey, update=False, **opts):
105
+ """Register a tkFont and store it in a globals of this module
106
+ this method should be called only after a tk.Tk() windows has been
107
+ created."""
108
+ if not hasFont(fontKey) or update:
109
+ globals()[fontKey] = tkFont.Font(**opts)
110
+
111
+ return globals()[fontKey]
112
+
113
+
114
+ def hasFont(fontKey):
115
+ return fontKey in globals()
116
+
117
+
118
+ def aliasFont(fontAlias, fontKey):
119
+ """Set a fontAlias as another alias name of fontKey"""
120
+ g = globals()
121
+ g[fontAlias] = g[fontKey]
122
+
123
+
124
+ def getDefaultFont():
125
+ return tk.font.nametofont("TkDefaultFont")
126
+
127
+
128
+ def getNamedFont(fontName):
129
+ return globals()[fontName]
130
+
131
+
132
+ def getBigFont():
133
+ return getNamedFont(FONT_BIG)
134
+
135
+
136
+ def setCommonFonts(window=None):
137
+ """Set some predefined common fonts.
138
+ Same conditions of setFont applies here."""
139
+ f = setFont(FONT_NORMAL, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE)
140
+ aliasFont('fontButton', FONT_NORMAL)
141
+
142
+ # Set default font size
143
+ default_font = getDefaultFont()
144
+ default_font.configure(size=pw.Config.SCIPION_FONT_SIZE, family=pw.Config.SCIPION_FONT_NAME)
145
+
146
+ fb = setFont(FONT_BOLD, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE,
147
+ weight='bold')
148
+ fi = setFont(FONT_ITALIC, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE,
149
+ slant='italic')
150
+
151
+ setFont(FONT_BIG, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE+8)
152
+
153
+ if window:
154
+ window.fontBig = tkFont.Font(size=pw.Config.SCIPION_FONT_SIZE + 2, family=pw.Config.SCIPION_FONT_NAME,
155
+ weight='bold')
156
+ window.font = f
157
+ window.fontBold = fb
158
+ window.fontItalic = fi
159
+
160
+ # This adds the default value for the listbox inside a combo box
161
+ # Which seems to not react to default font!!
162
+ window.root.option_add("*TCombobox*Listbox*Font", default_font)
163
+ window.root.option_add("*TCombobox*Font", default_font)
164
+
165
+
166
+ def changeFontSizeByDeltha(font, deltha, minSize=-999, maxSize=999):
167
+ size = font['size']
168
+ new_size = size + deltha
169
+ if minSize <= new_size <= maxSize:
170
+ font.configure(size=new_size)
171
+
172
+
173
+ def changeFontSize(font, event, minSize=-999, maxSize=999):
174
+ deltha = 2
175
+ if event.char == '-':
176
+ deltha = -2
177
+ changeFontSizeByDeltha(font, deltha, minSize, maxSize)
178
+
179
+
180
+ # --------------- IMAGE related variables and functions -----------------------
181
+ def getImage(imageName, imgDict=None, tkImage=True, percent=100,
182
+ maxheight=None):
183
+ """ Search for the image in the RESOURCES path list. """
184
+
185
+ global image_cache
186
+
187
+ if imageName is None:
188
+ return None
189
+
190
+ # Rename .gif by .png. In Linux with pillow 9.2.0 gif transparency is broken so
191
+ # we need to go for png. But in the past, in Macs png didn't work and made us go from png to gif
192
+ # We are now providing the 2 formats, prioritising pngs. If png work in MAC and windows then gif
193
+ # could be deleted. Otherwise, we may need to do this replacement based on the OS.
194
+ # NOTE: "convert my-image.gif PNG32:my-image.png" has converted gifs to pngs RGBA (32 bits) it seems pillow
195
+ # needs RGBA format to deal with transparencies.
196
+
197
+ # Most protocols.conf uses .gif extension. We need to use png!.
198
+
199
+ # ImageName could be either a file name (bookmark.gif) a full path image or a SpriteImage
200
+ if isinstance(imageName, SpriteImage):
201
+ fromSprite = True
202
+ imageStr = str(imageName)
203
+ else:
204
+ fromSprite=False
205
+ imageStr = imageName
206
+
207
+ if not os.path.isabs(imageStr) and imageStr not in [Icon.WAITING]:
208
+ imageStr = imageStr.replace(".gif", ".png")
209
+
210
+ if imageStr in image_cache:
211
+ return image_cache[imageStr]
212
+
213
+ # If it is a definition of a sprite image
214
+ if fromSprite:
215
+ image = Sprite.getImage(imageName)
216
+ else:
217
+ imagePath = pw.findResource(imageStr) if not os.path.isabs(imageStr) else imageStr
218
+ image = Image.open(imagePath) if imagePath else None
219
+
220
+ if image:
221
+ # For a future dark mode we might need to invert the image but it requires some extra work to make it look nice:
222
+ # image = invertImage(image)
223
+ w, h = image.size
224
+ newSize = None
225
+ if percent != 100: # Display image with other dimensions
226
+ fp = float(percent) / 100.0
227
+ newSize = int(fp * w), int(fp * h)
228
+ elif maxheight and h > maxheight:
229
+ newSize = int(w * float(maxheight) / h), maxheight
230
+ if newSize:
231
+ image.thumbnail(newSize, Image.LANCZOS)
232
+ if tkImage:
233
+ image = ImageTk.PhotoImage(image)
234
+
235
+ image_cache[imageStr] = image
236
+ return image
237
+
238
+ def invertImage(img):
239
+ # Creating a numpy array out of the image object
240
+ img_arry = np.array(img)
241
+
242
+ # Maximum intensity value of the color mode
243
+ I_max = 255
244
+
245
+ # Subtracting 255 (max value possible in a given image
246
+ # channel) from each pixel values and storing the result
247
+ img_arry = I_max - img_arry
248
+
249
+ # Creating an image object from the resultant numpy array
250
+ return Image.fromarray(img_arry)
251
+ # ---------------- Windows geometry utilities -----------------------
252
+ def getGeometry(win):
253
+ """ Return the geometry information of the windows
254
+ It will be a tuple (width, height, x, y)
255
+ """
256
+ return (win.winfo_reqwidth(), win.winfo_reqheight(),
257
+ win.winfo_x(), win.winfo_y())
258
+
259
+
260
+ def centerWindows(root, dim=None, refWindows=None):
261
+ """Center a windows in the middle of the screen
262
+ or in the middle of other windows(refWindows param)"""
263
+ root.update_idletasks()
264
+ if dim is None:
265
+ gw, gh, _, _ = getGeometry(root)
266
+ else:
267
+ gw, gh = dim
268
+ if refWindows:
269
+ rw, rh, rx, ry = getGeometry(refWindows)
270
+ x = rx + (rw - gw) / 2
271
+ y = ry + (rh - gh) / 2
272
+ else:
273
+ w = root.winfo_screenwidth()
274
+ h = root.winfo_screenheight()
275
+ x = (w - gw) / 2
276
+ y = (h - gh) / 2
277
+
278
+ root.geometry("%dx%d+%d+%d" % (gw, gh, x, y))
279
+
280
+
281
+ def configureWeigths(widget, row=0, column=0):
282
+ """This function is a shortcut to a common
283
+ used pair of calls: rowconfigure and columnconfigure
284
+ for making childs widgets take the space available"""
285
+ widget.columnconfigure(column, weight=1)
286
+ widget.rowconfigure(row, weight=1)
287
+
288
+
289
+ def defineStyle():
290
+ """
291
+ Defines some specific behaviour of the style.
292
+ """
293
+
294
+ # To specify the height of the rows based on the font size.
295
+ # Should be centralized somewhere.
296
+ style = Style()
297
+ defaultFont = getDefaultFont()
298
+
299
+ iconsSizePx = int((pyworkflow.Config.SCIPION_ICON_ZOOM/100 * 32))
300
+ fontHeight = defaultFont.metrics()['linespace']
301
+ rowheight = max(iconsSizePx, fontHeight)
302
+
303
+ style.configure(LIST_TREEVIEW, rowheight=rowheight,
304
+ background=pw.Config.SCIPION_BG_COLOR,
305
+ fieldbackground=pw.Config.SCIPION_BG_COLOR)
306
+ style.configure(LIST_TREEVIEW+".Heading", font=(defaultFont["family"],defaultFont["size"]))
307
+
308
+ style.configure(BORDERLESS_TREEVIEW, rowheight=rowheight,
309
+ background=pw.Config.SCIPION_BG_COLOR,
310
+ fieldbackground=pw.Config.SCIPION_BG_COLOR,
311
+ borderwidth=0, font=(defaultFont["family"],defaultFont["size"]))
312
+
313
+
314
+ class Window:
315
+ """Class to manage a Tk windows.
316
+ It will encapsulate some basic creation and
317
+ setup functions. """
318
+ # To allow plugins to add their own menus
319
+ _pluginMenus = dict()
320
+
321
+ def __init__(self, title='', masterWindow=None, weight=True,
322
+ minsize=(500, 300), icon=Icon.SCIPION_ICON, **kwargs):
323
+ """Create a Tk window.
324
+ title: string to use as title for the windows.
325
+ master: if not provided, the windows create will be the principal one
326
+ weight: if true, the first col and row will be configured with weight=1
327
+ minsize: a minimum size for height and width
328
+ icon: if not None, set the windows icon
329
+ """
330
+ # Init gui plugins
331
+ pw.Config.getDomain()._discoverGUIPlugins()
332
+
333
+ if masterWindow is None:
334
+ # Unused?? Window._root = self
335
+ self._images = {}
336
+ # If a window which isn't the main Scipion window is generated from another main window, e. g. with Scipion
337
+ # template after the refactoring of the kickoff, in which a dialog is launched and then a form, being it
338
+ # called from the command line, so there's no Scipion main window. In that case, a tk.Tk() exists because if
339
+ # a tk.TopLevel(), as the dialog, is directly launched, it automatically generates a main tk.Tk(). Thus,
340
+ # after that first auto-tk.Tk(), another tk.Tk() was created here, and so the previous information was lost.
341
+ # Solution proposed is to generate the root as an invisible window if it doesn't exist previously, and make
342
+ # he first window generated a tk.Toplevel. After that, all steps executed later will go through the else
343
+ # statement, being that way each new tk.Toplevel() correctly referenced.
344
+ root = tk.Tk()
345
+ root.withdraw() # Main window, invisible
346
+ # invoke the button on the return key
347
+ root.bind_class("Button", "<Key-Return>", lambda event: event.widget.invoke())
348
+
349
+ self._class = kwargs.get("_class", DEFAULT_WINDOW_CLASS)
350
+ self.root = tk.Toplevel(class_=self._class) # Toplevel of main window
351
+ else:
352
+ class_ = masterWindow._class if hasattr(masterWindow, "_class") else DEFAULT_WINDOW_CLASS
353
+ self.root = tk.Toplevel(masterWindow.getRoot(), class_=class_)
354
+ self.root.group(masterWindow.getRoot())
355
+ self._images = masterWindow._images
356
+
357
+ self.root.withdraw()
358
+ self.root.title(title)
359
+
360
+ if weight:
361
+ configureWeigths(self.root)
362
+ if minsize is not None:
363
+ self.root.minsize(minsize[0], minsize[1])
364
+
365
+ # Set the icon
366
+ self._setIcon(icon)
367
+
368
+ self.root.protocol("WM_DELETE_WINDOW", self._onClosing)
369
+ self._w, self._h, self._x, self._y = 0, 0, 0, 0
370
+ self.root.bind("<Configure>", self._configure)
371
+ self.master = masterWindow
372
+ setCommonFonts(self)
373
+
374
+ self.initial_focus = None
375
+
376
+ if kwargs.get('enableQueue', False):
377
+ self.queue = queue.Queue(maxsize=0)
378
+ else:
379
+ self.queue = None
380
+
381
+ def _setIcon(self, icon):
382
+
383
+ if icon is not None:
384
+ try:
385
+ path = pw.findResource(icon)
386
+ # If path is None --> Icon not found
387
+ if path is None:
388
+ # By default, if icon is not found use default scipion one.
389
+ path = pw.findResource(Icon.SCIPION_ICON)
390
+
391
+ abspath = os.path.abspath(path)
392
+
393
+ img = tk.Image("photo", file=abspath)
394
+ self.root.tk.call('wm', 'iconphoto', self.root._w, img)
395
+ except Exception as e:
396
+ # Do nothing if icon could not be loaded
397
+ pass
398
+
399
+ def __processQueue(self): # called from main frame
400
+ if not self.queue.empty():
401
+ func = self.queue.get(block=False)
402
+ # executes graphic interface function
403
+ func()
404
+ self._queueTimer = self.root.after(500, self.__processQueue)
405
+
406
+ def enqueue(self, func):
407
+ """ Put some function to be executed in the GUI main thread. """
408
+ self.queue.put(func)
409
+
410
+ def getRoot(self):
411
+ return self.root
412
+
413
+ def desiredDimensions(self):
414
+ """Override this method to calculate desired dimensions."""
415
+ return None
416
+
417
+ def _configure(self, e):
418
+ """ Filter event and call appropriate handler. """
419
+ if self.root != e.widget:
420
+ return
421
+
422
+ _, _, x, y = getGeometry(self.root)
423
+ w, h = e.width, e.height
424
+
425
+ if w != self._w or h != self._h:
426
+ self._w, self._h = w, h
427
+ self.handleResize()
428
+
429
+ if x != self._x or y != self._y:
430
+ self._x, self._y = x, y
431
+ self.handleMove()
432
+
433
+ def handleResize(self):
434
+ """Override this method to respond to resize events."""
435
+ pass
436
+
437
+ def handleMove(self):
438
+ """Override this method to respond to move events."""
439
+ pass
440
+
441
+ def show(self, center=True, modal=False):
442
+ """This function will enter in the Tk mainloop"""
443
+ if center:
444
+ if self.master is None:
445
+ refw = None
446
+ else:
447
+ refw = self.master.getRoot()
448
+ centerWindows(self.root, dim=self.desiredDimensions(),
449
+ refWindows=refw)
450
+
451
+ self.root.deiconify()
452
+ self.root.focus_set()
453
+ if self.queue is not None:
454
+ self._queueTimer = self.root.after(1000, self.__processQueue)
455
+
456
+ if self.initial_focus is not None:
457
+ self.initial_focus.focus_set()
458
+
459
+ if modal:
460
+ self.root.wait_window(self.root)
461
+ else:
462
+ self.root.mainloop()
463
+
464
+ def close(self, e=None):
465
+ self.root.destroy()
466
+ # JMRT: For some reason when Tkinter has an exception
467
+ # it does not exit the application as expected and
468
+ # remains in the mainloop, so here we are forcing
469
+ # to exit the whole system (only applies for the main window)
470
+ if self.master is None:
471
+ import sys
472
+ sys.exit()
473
+
474
+ def _onClosing(self):
475
+ """Do some cleaning before closing."""
476
+ if self.master is None:
477
+ pass
478
+ else:
479
+ self.master.getRoot().focus_set()
480
+ if self.queue is not None:
481
+ self.root.after_cancel(self._queueTimer)
482
+ self.close()
483
+
484
+ def getImage(self, imgName, percent=100, maxheight=None):
485
+ return getImage(imgName, percent=percent,
486
+ maxheight=maxheight)
487
+
488
+ def createMainMenu(self, menuConfig):
489
+ """Create Main menu from the given MenuConfig object."""
490
+ menu = tk.Menu(self.root, font=self.font)
491
+ self._addMenuChilds(menu, menuConfig)
492
+ self._addPluginMenus(menu)
493
+ self.root.config(menu=menu)
494
+ return menu
495
+
496
+ def _addMenuChilds(self, menu, menuConfig):
497
+ """Add entries of menuConfig in menu
498
+ (using add_cascade or add_command for sub-menus and final options)."""
499
+ # Helper function to create the main menu.
500
+ for sub in menuConfig:
501
+ menuLabel = sub.text
502
+ if not menuLabel: # empty or None label means a separator
503
+ menu.add_separator()
504
+ elif len(sub) > 0: # sub-menu
505
+ submenu = tk.Menu(self.root, tearoff=0, font=self.font)
506
+ menu.add_cascade(label=menuLabel, menu=submenu)
507
+ self._addMenuChilds(submenu, sub) # recursive filling
508
+ else: # menu option
509
+ # If there is an entry called "Browse files", when clicked it
510
+ # will call the method onBrowseFiles() (it has to be defined!)
511
+ def callback(name):
512
+ """Return a callback function named "on<Name>"."""
513
+ f = "on%s" % "".join(x.capitalize() for x in name.split())
514
+ return lambda: getattr(self, f)()
515
+
516
+ if sub.shortCut is not None:
517
+ menuLabel += ' (' + sub.shortCut + ')'
518
+
519
+ menu.add_command(label=menuLabel, compound=tk.LEFT,
520
+ image=self.getImage(sub.icon),
521
+ command=callback(name=sub.text))
522
+
523
+ def _addPluginMenus(self, menu):
524
+
525
+ if self._pluginMenus:
526
+ submenu = tk.Menu(self.root, tearoff=0, font=self.font)
527
+ menu.add_cascade(label="Others", menu=submenu)
528
+
529
+ # For each plugin menu
530
+ for label in self._pluginMenus:
531
+ submenu.add_command(label=label, compound=tk.LEFT,
532
+ image=self.getImage(self._pluginMenus.get(label)[1]),
533
+ command=partial(self.plugin_callback, label))
534
+
535
+ def plugin_callback(self, label):
536
+ return self._pluginMenus.get(label)[0](self)
537
+
538
+ @classmethod
539
+ def registerPluginMenu(cls, label, callback, icon=None):
540
+ # TODO: have a proper model instead of a tuple?
541
+ cls._pluginMenus[label] = (callback, icon)
542
+
543
+ def showError(self, msg, header="Error", exception=None):
544
+ """Pops up a dialog with the error message
545
+ :param msg Message to display
546
+ :param header Title of the dialog
547
+ :param exception: Optional. exception associated"""
548
+ from .dialog import showError
549
+ showError(header, msg, self.root, exception=exception)
550
+
551
+ def showInfo(self, msg, header="Info"):
552
+ from .dialog import showInfo
553
+ showInfo(header, msg, self.root)
554
+
555
+ def showWarning(self, msg, header='Warning'):
556
+ from .dialog import showWarning
557
+ showWarning(header, msg, self.root)
558
+
559
+ def askYesNo(self, title, msg):
560
+ from .dialog import askYesNo
561
+ return askYesNo(title, msg, self.root)
562
+
563
+ def createCloseButton(self, parent):
564
+ """ Create a button for closing the window, setting
565
+ the proper label and icon.
566
+ """
567
+ return Button(parent, Message.LABEL_BUTTON_CLOSE, Icon.ACTION_CLOSE,
568
+ command=self.close)
569
+
570
+ def configureWeights(self, row=0, column=0):
571
+ configureWeigths(self.root, row, column)