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
@@ -0,0 +1,981 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
4
+ # * Jose Gutierrez (jose.gutierrez@cnb.csic.es)
5
+ # *
6
+ # * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
7
+ # *
8
+ # * This program is free software; you can redistribute it and/or modify
9
+ # * it under the terms of the GNU General Public License as published by
10
+ # * the Free Software Foundation; either version 3 of the License, or
11
+ # * (at your option) any later version.
12
+ # *
13
+ # * This program is distributed in the hope that it will be useful,
14
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # * GNU General Public License for more details.
17
+ # *
18
+ # * You should have received a copy of the GNU General Public License
19
+ # * along with this program; if not, write to the Free Software
20
+ # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
21
+ # * 02111-1307 USA
22
+ # *
23
+ # * All comments concerning this program package may be sent to the
24
+ # * e-mail address 'scipion@cnb.csic.es'
25
+ # *
26
+ # **************************************************************************
27
+ """
28
+ Module to handling Dialogs
29
+ some code was taken from tkSimpleDialog
30
+ """
31
+ import os.path
32
+ import tkinter as tk
33
+ import traceback
34
+ from tkcolorpicker import askcolor as _askColor
35
+ from pyworkflow import Config
36
+ from pyworkflow.exceptions import PyworkflowException
37
+ from pyworkflow.utils import Message, Icon, Color
38
+ from . import gui, Window, widgets, configureWeigths, LIST_TREEVIEW, defineStyle, ToolTip, getDefaultFont
39
+ from .tree import BoundTree, Tree
40
+ from .text import Text, TaggedText
41
+
42
+ # Possible result values for a Dialog
43
+ from .. import TK
44
+
45
+ RESULT_YES = 0
46
+ RESULT_NO = 1
47
+ RESULT_CANCEL = 2
48
+ RESULT_RUN_SINGLE = 3
49
+ RESULT_RUN_ALL = 4
50
+ RESULT_CLOSE = 5
51
+
52
+
53
+ class Dialog(tk.Toplevel):
54
+ _images = {} # Images cache
55
+ """Implementation of our own dialog to display messages
56
+ It will have by default a three buttons: YES, NO and CANCEL
57
+ Subclasses can rename the labels of the buttons like: OK, CLOSE or others
58
+ The buttons(and theirs order) can be changed.
59
+ An image name can be passed to display left to the message.
60
+ """
61
+
62
+ def __init__(self, parent, title, lockGui=True, **kwargs):
63
+ """Initialize a dialog.
64
+ Arguments:
65
+ parent -- a parent window (the application window)
66
+ title -- the dialog title
67
+ **args accepts:
68
+ buttons -- list of buttons tuples containing which buttons to display
69
+ """
70
+
71
+ if parent is None:
72
+ parent = tk.Tk()
73
+ parent.withdraw()
74
+ gui.setCommonFonts()
75
+ # invoke the button on the return key
76
+ parent.bind_class("Button", "<Key-Return>", lambda event: event.widget.invoke())
77
+
78
+ tk.Toplevel.__init__(self, parent)
79
+
80
+ defineStyle()
81
+
82
+ self.withdraw() # remain invisible for now
83
+ # If the master is not viewable, don't
84
+ # make the child transient, or else it
85
+ # would be opened withdrawn
86
+ if parent.winfo_viewable() and lockGui:
87
+ self.transient(parent)
88
+
89
+ if title:
90
+ self.title(title)
91
+
92
+ self.parent = parent
93
+
94
+ # Default to CANCEL so if window is "Closed" behaves the same.
95
+ self.result = RESULT_CANCEL
96
+ self.initial_focus = None
97
+
98
+ bodyFrame = tk.Frame(self)
99
+ # Call subclass method body to create that region
100
+ self.body(bodyFrame)
101
+ bodyFrame.grid(row=0, column=0, sticky='news',
102
+ padx=5, pady=5)
103
+
104
+ # Frame for the info/message label
105
+ infoFrame = tk.Frame(self)
106
+ infoFrame.grid(row=1, column=0, sticky='sew',
107
+ padx=5, pady=(0, 5))
108
+ self.floatingMessage = tk.Label(infoFrame, text="", fg=Config.SCIPION_MAIN_COLOR)
109
+ self.floatingMessage.grid(row=0, column=0, sticky='news')
110
+
111
+ # Create buttons
112
+ self.icons = kwargs.get('icons',
113
+ {RESULT_YES: Icon.BUTTON_SELECT,
114
+ RESULT_NO: Icon.BUTTON_CLOSE,
115
+ RESULT_CANCEL: Icon.BUTTON_CANCEL,
116
+ RESULT_CLOSE: Icon.BUTTON_CLOSE})
117
+
118
+ self.buttons = kwargs.get('buttons', [('OK', RESULT_YES),
119
+ ('Cancel', RESULT_CANCEL)])
120
+ self.defaultButton = kwargs.get('default', 'OK')
121
+
122
+ # Frame for buttons
123
+ btnFrame = tk.Frame(self)
124
+ self.buttonbox(btnFrame)
125
+ btnFrame.grid(row=2, column=0, sticky='sew',
126
+ padx=5, pady=(0, 5))
127
+
128
+ gui.configureWeigths(self)
129
+
130
+ if self.initial_focus is None:
131
+ self.initial_focus = self
132
+
133
+ self.protocol("WM_DELETE_WINDOW", self.cancel)
134
+
135
+ if self.parent is not None:
136
+ position = kwargs.get('position', (parent.winfo_rootx() + 50,
137
+ parent.winfo_rooty() + 50))
138
+ self.geometry("+%d+%d" % position)
139
+
140
+ self.deiconify() # become visible now
141
+ self.initial_focus.focus_set()
142
+ # Pablo: I've commented this when migrating to python3 since I was getting and exception:
143
+ # window ".139897767953072.139897384058440" was deleted before its visibility changed
144
+ # wait for window to appear on screen before calling grab_set
145
+ self.wait_visibility()
146
+ if lockGui:
147
+ self.grab_set()
148
+ self.wait_window(self)
149
+
150
+ def getRoot(self):
151
+ return self
152
+
153
+ def destroy(self):
154
+ """Destroy the window"""
155
+ self.initial_focus = None
156
+ tk.Toplevel.destroy(self)
157
+
158
+ #
159
+ # construction hooks
160
+
161
+ def body(self, master):
162
+ """create dialog body.
163
+ return widget that should have initial focus.
164
+ This method should be overridden, and is called
165
+ by the __init__ method.
166
+ """
167
+ pass
168
+
169
+ def _createButton(self, frame, text, result):
170
+ icon = None
171
+ if result in self.icons.keys():
172
+ icon = self.getImage(self.icons[result])
173
+ return tk.Button(frame, text=text, image=icon,
174
+ compound=tk.LEFT,
175
+ command=lambda: self._handleResult(result))
176
+
177
+ def buttonbox(self, btnFrame):
178
+ frame = tk.Frame(btnFrame)
179
+ btnFrame.columnconfigure(0, weight=1)
180
+ frame.grid(row=0, column=0)
181
+ col = 0
182
+ for btnLabel, btnResult in self.buttons:
183
+ btn = self._createButton(frame, btnLabel, btnResult)
184
+ btn.grid(row=0, column=col, padx=5, pady=5)
185
+ if (btnLabel == self.defaultButton and
186
+ self.initial_focus is None):
187
+ self.initial_focus = btn
188
+ col += 1
189
+ self.bind("<Return>", self._handleReturn)
190
+ self.bind("<KP_Enter>", self._handleReturn)
191
+ self.bind("<Escape>", lambda e: self._handleResult(RESULT_CANCEL))
192
+
193
+ def _handleResult(self, resultValue):
194
+ """This method will be called when any button is pressed.
195
+ It will set the resultValue associated with the button
196
+ and close the Dialog"""
197
+ self.result = resultValue
198
+ noCancel = self.result != RESULT_CANCEL and self.result != RESULT_CLOSE
199
+
200
+ callBack = self.validate if noCancel else self.validateClose
201
+ if not callBack():
202
+ self.initial_focus.focus_set() # put focus back
203
+ return
204
+
205
+ self.withdraw()
206
+ self.update_idletasks()
207
+
208
+ try:
209
+ if noCancel:
210
+ self.apply()
211
+ finally:
212
+ self.cancel()
213
+
214
+ def _handleReturn(self, e=None):
215
+ """Handle press return key"""
216
+ # Check which of the buttons is the default
217
+ for button, result in self.buttons:
218
+ if self.defaultButton == button:
219
+ self._handleResult(result)
220
+
221
+ def cancel(self, event=None):
222
+ # put focus back to the parent window
223
+ if self.parent is not None:
224
+ self.parent.focus_set()
225
+ self.destroy()
226
+
227
+ #
228
+ # command hooks
229
+
230
+ def validate(self):
231
+ """validate the data
232
+ This method is called automatically to validate the data before the
233
+ dialog is destroyed. By default, it always validates OK.
234
+ """
235
+ return 1 # override
236
+
237
+ def validateClose(self):
238
+ return True
239
+
240
+ def apply(self):
241
+ """process the data
242
+ This method is called automatically to process the data, *after*
243
+ the dialog is destroyed. By default, it does nothing.
244
+ """
245
+ pass # override
246
+
247
+ def getImage(self, imgName):
248
+ """A shortcut to get an image from its name"""
249
+ return gui.getImage(imgName)
250
+
251
+ def getResult(self):
252
+ return self.result
253
+
254
+ def resultYes(self):
255
+ return self.result == RESULT_YES
256
+
257
+ def resultNo(self):
258
+ return self.result == RESULT_NO
259
+
260
+ def resultCancel(self):
261
+ return self.result == RESULT_CANCEL
262
+
263
+ def info(self, message):
264
+ """ Shows a info message for long running processes to inform the user GUI is not frozen"""
265
+ self.floatingMessage.config(text=message)
266
+
267
+ ### Basic GUI helper methods
268
+ def _addButton(self, frame, callback, text="", icon=None, row=0, col=0, tooltip=None, shortcut=""):
269
+ """ Adds a label button"""
270
+ btn = tk.Label(frame, text=text,
271
+ image=self.getImage(icon),
272
+ compound=tk.LEFT, cursor='hand2')
273
+ btn.grid(row=row, column=col, sticky='nw', padx=(5, 0), pady=(5, 0))
274
+ btn.bind('<Button-1>', callback)
275
+ if tooltip:
276
+ tooltip = tooltip + ' (%s)' % shortcut if shortcut else tooltip
277
+ ToolTip(btn, tooltip, delay=150)
278
+ if shortcut:
279
+ self.bind(shortcut, callback)
280
+
281
+
282
+ def fillMessageText(text, message):
283
+ # Insert lines of text
284
+ if isinstance(message, list):
285
+ lines = message
286
+ else:
287
+ lines = message.splitlines()
288
+ text.setReadOnly(False)
289
+ text.clear()
290
+ w = 0
291
+ for l in lines:
292
+ w = max(w, len(l))
293
+ text.addLine(l)
294
+ w = min(w + 5, 80)
295
+ h = min(len(lines) + 3, 30)
296
+ text.config(height=h, width=w)
297
+ text.addNewline()
298
+ text.setReadOnly(True)
299
+
300
+
301
+ def createMessageBody(bodyFrame, message, image,
302
+ frameBg=Config.SCIPION_BG_COLOR,
303
+ textBg=Config.SCIPION_BG_COLOR,
304
+ textPad=5):
305
+ """ Create a Text containing the message.
306
+ Params:
307
+ bodyFrame: tk.Frame to be filled.
308
+ msg: a str or list with the lines.
309
+ """
310
+ bodyFrame.config(bg=frameBg, bd=0)
311
+ text = TaggedText(bodyFrame, bg=textBg, bd=0, highlightthickness=0)
312
+ # Insert image
313
+ if image:
314
+ label = tk.Label(bodyFrame, image=image, bg=textBg, bd=0)
315
+ label.grid(row=0, column=0, sticky='nw')
316
+
317
+ text.frame.grid(row=0, column=1, sticky='news',
318
+ padx=textPad, pady=textPad)
319
+ fillMessageText(text, message)
320
+ bodyFrame.rowconfigure(0, weight=1)
321
+ bodyFrame.columnconfigure(1, weight=1)
322
+
323
+ return text
324
+
325
+
326
+ class MessageDialog(Dialog):
327
+ """Dialog subclasses to show message info, questions or errors.
328
+ It can display an icon with the message"""
329
+
330
+ def __init__(self, parent, title, msg, iconPath, **args):
331
+ self.msg = msg
332
+ self.iconPath = iconPath
333
+ if 'buttons' not in args:
334
+ args['buttons'] = [('OK', RESULT_YES)]
335
+ args['default'] = 'OK'
336
+ Dialog.__init__(self, parent, title, **args)
337
+
338
+ def body(self, bodyFrame):
339
+ self.image = gui.getImage(self.iconPath)
340
+ createMessageBody(bodyFrame, self.msg, self.image)
341
+
342
+
343
+ class ExceptionDialog(MessageDialog):
344
+ def __init__(self, *args, **kwargs):
345
+ self._exception = None if "exception" not in kwargs else kwargs['exception']
346
+ super().__init__(*args, **kwargs)
347
+
348
+ def body(self, bodyFrame):
349
+ super().body(bodyFrame)
350
+
351
+ def addTraceback(event):
352
+ detailsText = TaggedText(bodyFrame, bg=Config.SCIPION_BG_COLOR, bd=0, highlightthickness=0)
353
+ traceStr = traceback.format_exc()
354
+ fillMessageText(detailsText, traceStr)
355
+ detailsText.frame.grid(row=row + 1, column=0, columnspan=2, sticky='news', padx=5, pady=5)
356
+ event.widget.grid_forget()
357
+
358
+ row = 1
359
+ if self._exception:
360
+
361
+ if isinstance(self._exception, PyworkflowException):
362
+ helpUrl = self._exception.getUrl()
363
+ labelUrl = TaggedText(bodyFrame, bg=Config.SCIPION_BG_COLOR, bd=0, highlightthickness=0)
364
+ fillMessageText(labelUrl, "Please go here for more details: %s" % helpUrl)
365
+ labelUrl.grid(row=row, column=0, columnspan=2, sticky='news')
366
+ row += 1
367
+
368
+ label = tk.Label(bodyFrame, text="Show details...", bg=Config.SCIPION_BG_COLOR, bd=0)
369
+ label.grid(row=row, column=0, columnspan=2, sticky='news')
370
+ label.bind("<Button-1>", addTraceback)
371
+
372
+
373
+ class YesNoDialog(MessageDialog):
374
+ """Ask a question with YES/NO answer"""
375
+
376
+ def __init__(self, master, title, msg, **kwargs):
377
+ buttonList = [('Yes', RESULT_YES), ('No', RESULT_NO)]
378
+
379
+ if kwargs.get('showCancel', False):
380
+ buttonList.append(('Cancel', RESULT_CANCEL))
381
+
382
+ MessageDialog.__init__(self, master, title, msg,
383
+ Icon.ALERT, default='No',
384
+ buttons=buttonList)
385
+
386
+
387
+ class GenericDialog(Dialog):
388
+ """
389
+ Create a dialog with many buttons
390
+ Arguments:
391
+ parent -- a parent window (the application window)
392
+ title -- the dialog title
393
+ msg -- message to display into the dialog
394
+ iconPath -- path of the image to show into the dialog
395
+
396
+ **args accepts:
397
+ buttons -- list of buttons tuples containing which buttons to display and theirs values
398
+ icons -- list of icons for all buttons
399
+ default -- button default
400
+
401
+ Example:
402
+ buttons=[('Single', RESULT_RUN_SINGLE),
403
+ ('All', RESULT_RUN_ALL),
404
+ ('Cancel', RESULT_CANCEL)],
405
+ default='Cancel',
406
+ icons={RESULT_CANCEL: Icon.BUTTON_CANCEL,
407
+ RESULT_RUN_SINGLE: Icon.BUTTON_SELECT,
408
+ RESULT_RUN_ALL: Icon.ACTION_EXECUTE})
409
+ """
410
+
411
+ def __init__(self, master, title, msg, iconPath, **kwargs):
412
+ self.msg = msg
413
+ self.iconPath = iconPath
414
+ Dialog.__init__(self, master, title, **kwargs)
415
+
416
+ def body(self, bodyFrame):
417
+ self.image = gui.getImage(self.iconPath)
418
+ createMessageBody(bodyFrame, self.msg, self.image)
419
+
420
+
421
+ class EntryDialog(Dialog):
422
+ """Dialog to ask some entry"""
423
+
424
+ def __init__(self, parent, title, entryLabel, entryWidth=20,
425
+ defaultValue='', headerLabel=None):
426
+ self.entryLabel = entryLabel
427
+ self.entryWidth = entryWidth
428
+ self.headerLabel = headerLabel
429
+ self.tkvalue = tk.StringVar()
430
+ self.tkvalue.set(defaultValue)
431
+ self.value = None
432
+ Dialog.__init__(self, parent, title)
433
+
434
+ def body(self, bodyFrame):
435
+ bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
436
+ frame = tk.Frame(bodyFrame, bg=Config.SCIPION_BG_COLOR)
437
+ frame.grid(row=0, column=0, padx=20, pady=20)
438
+ row = 0
439
+ if self.headerLabel:
440
+ label = tk.Label(bodyFrame, text=self.headerLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
441
+ label.grid(row=row, column=0, columnspan=2, sticky='nw', padx=(15, 10), pady=15)
442
+ row += 1
443
+ label = tk.Label(bodyFrame, text=self.entryLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
444
+ label.grid(row=row, column=0, sticky='nw', padx=(15, 10), pady=15)
445
+ self.entry = tk.Entry(bodyFrame, bg=gui.cfgEntryBgColor,
446
+ width=self.entryWidth, textvariable=self.tkvalue,
447
+ font=getDefaultFont())
448
+ self.entry.grid(row=row, column=1, sticky='new', padx=(0, 15), pady=15)
449
+ self.initial_focus = self.entry
450
+
451
+ def apply(self):
452
+ self.value = self.entry.get()
453
+
454
+ def validate(self):
455
+ if len(self.entry.get().strip()) == 0:
456
+ showError("Validation error", "Value is empty", self)
457
+ return False
458
+ return True
459
+
460
+
461
+ class EditObjectDialog(Dialog):
462
+ """Dialog to edit some text"""
463
+
464
+ def __init__(self, parent, title, obj, mapper, **kwargs):
465
+ self.obj = obj
466
+ self.mapper = mapper
467
+
468
+ self.textWidth = 5
469
+ self.textHeight = 1
470
+ self.labelText = kwargs.get('labelText', Message.TITLE_LABEL)
471
+ self.valueText = self.obj.getObjLabel()
472
+
473
+ self.commentLabel = Message.TITLE_COMMENT
474
+ self.commentWidth = 50
475
+ self.commentHeight = 15
476
+ self.valueComment = self.obj.getObjComment()
477
+
478
+ Dialog.__init__(self, parent, title, **kwargs)
479
+
480
+ def body(self, bodyFrame):
481
+ bodyFrame.config(bg=Config.SCIPION_BG_COLOR)
482
+ frame = tk.Frame(bodyFrame, bg=Config.SCIPION_BG_COLOR)
483
+ frame.grid(row=0, column=0, padx=20, pady=20)
484
+
485
+ # Label
486
+ label_text = tk.Label(bodyFrame, text=self.labelText, bg=Config.SCIPION_BG_COLOR, bd=0)
487
+ label_text.grid(row=0, column=0, sticky='nw', padx=(15, 10), pady=15)
488
+ # Label box
489
+ var = tk.StringVar()
490
+ var.set(self.valueText)
491
+ self.textLabel = tk.Entry(bodyFrame, width=self.textWidth, textvariable=var, font=gui.getDefaultFont())
492
+ self.textLabel.grid(row=0, column=1, sticky='news', padx=5, pady=5)
493
+
494
+ # Comment
495
+ label_comment = tk.Label(bodyFrame, text=self.commentLabel, bg=Config.SCIPION_BG_COLOR, bd=0)
496
+ label_comment.grid(row=1, column=0, sticky='nw', padx=(15, 10), pady=15)
497
+ # Comment box
498
+ self.textComment = Text(bodyFrame, height=self.commentHeight,
499
+ width=self.commentWidth)
500
+ self.textComment.setReadOnly(False)
501
+ self.textComment.setText(self.valueComment)
502
+ self.textComment.grid(row=1, column=1, sticky='news', padx=5, pady=5)
503
+ self.initial_focus = self.textLabel
504
+
505
+ def getLabel(self):
506
+ return self.textLabel.get()
507
+
508
+ def getComment(self):
509
+ return self.textComment.getText()
510
+
511
+ def apply(self):
512
+ self.obj.setObjLabel(self.getLabel())
513
+ self.obj.setObjComment(self.getComment())
514
+
515
+ if self.obj.hasObjId():
516
+ self.mapper.store(self.obj)
517
+ self.mapper.commit()
518
+
519
+ def buttonbox(self, btnFrame):
520
+ # Cancel the binding of <Return> key
521
+ Dialog.buttonbox(self, btnFrame)
522
+ # self.bind("<Return>", self._noReturn)
523
+ self.unbind("<Return>")
524
+
525
+ def _noReturn(self, e):
526
+ pass
527
+
528
+
529
+ """ Functions to display dialogs """
530
+
531
+
532
+ def askYesNo(title, msg, parent):
533
+ d = YesNoDialog(parent, title, msg)
534
+ return d.resultYes()
535
+
536
+
537
+ def askYesNoCancel(title, msg, parent):
538
+ d = YesNoDialog(parent, title, msg, showCancel=True)
539
+ return d.result
540
+
541
+
542
+ def askSingleAllCancel(title, msg, parent):
543
+ d = GenericDialog(parent, title, msg,
544
+ Icon.ALERT,
545
+ buttons=[('Single', RESULT_RUN_SINGLE),
546
+ ('All', RESULT_RUN_ALL),
547
+ ('Cancel', RESULT_CANCEL)],
548
+ default='Single',
549
+ icons={RESULT_CANCEL: Icon.BUTTON_CANCEL,
550
+ RESULT_RUN_SINGLE: Icon.BUTTON_SELECT,
551
+ RESULT_RUN_ALL: Icon.ACTION_EXECUTE})
552
+
553
+ return d.result
554
+
555
+
556
+ def showInfo(title, msg, parent):
557
+ MessageDialog(parent, title, msg, Icon.INFO)
558
+
559
+
560
+ def showWarning(title, msg, parent):
561
+ MessageDialog(parent, title, msg, Icon.ALERT)
562
+
563
+
564
+ def showError(title, msg, parent, exception=None):
565
+ ExceptionDialog(parent, title, msg, Icon.ERROR, exception=exception)
566
+
567
+
568
+ def askString(title, label, parent, entryWidth=20, defaultValue='', headerLabel=None):
569
+ d = EntryDialog(parent, title, label, entryWidth, defaultValue, headerLabel)
570
+ return d.value
571
+
572
+
573
+ def askColor(parent, defaultColor='black'):
574
+ (rgbcolor, hexcolor) = _askColor(defaultColor, parent=parent)
575
+ return hexcolor
576
+
577
+
578
+ def askPath(title=None, msg="Select a file of a folder", path=".", onlyFolders=False, master=None, returnBaseName=False):
579
+ from pyworkflow.gui.browser import FileBrowserWindow
580
+
581
+ if title is None:
582
+ title = "Select a folder" if onlyFolders else "Select a file"
583
+ browserW = FileBrowserWindow(title, master=master, path=path, onlyFolders=onlyFolders)
584
+ browserW.show(modal=True)
585
+
586
+ result = browserW.getLastSelection()
587
+ if returnBaseName:
588
+ result=os.path.basename(result)
589
+
590
+ return result
591
+
592
+ class ListDialog(Dialog):
593
+ """
594
+ Dialog to select an element from a list.
595
+ It is implemented using the Tree widget.
596
+ """
597
+
598
+ def __init__(self, parent, title, provider, message=None, **kwargs):
599
+ """ From kwargs:
600
+
601
+ :param message: message tooltip to show when browsing.
602
+ :param validateSelectionCallback: a callback function to validate selected items.
603
+ :param previewCallback: method to be called on item click to fill the callback frame.
604
+ :param selectmode: 'extended' by default. Selection mode of the tk.Tree
605
+ :param selectOnDoubleClick: (False). If True, double click will trigger "Select" button click
606
+ :param allowsEmptySelection: (False). Allows empty selection
607
+ :param allowSelect: if set to False, the 'Select' button will not be shown.
608
+ :param allowsEmptySelection: if set to True, it will not validate that at least one element was selected.
609
+ """
610
+ self.values = []
611
+ self.provider = provider
612
+ self.message = message
613
+ self.validateSelectionCallback = kwargs.get('validateSelectionCallback', None)
614
+ self.previewCallBack = kwargs.get('previewCallback', None)
615
+
616
+ self._selectmode = kwargs.get('selectmode', 'extended')
617
+ self._selectOnDoubleClick = kwargs.get('selectOnDoubleClick', False)
618
+ self._allowsEmptySelection = kwargs.get('allowsEmptySelection', False)
619
+
620
+ if "buttons" not in kwargs:
621
+ buttons=[]
622
+ if kwargs.get('allowSelect', True):
623
+ buttons.append(('Select', RESULT_YES))
624
+ if kwargs.get('cancelButton', False):
625
+ buttons.append(('Close', RESULT_CLOSE))
626
+ else:
627
+ buttons.append(('Cancel', RESULT_CANCEL))
628
+ kwargs['buttons'] = buttons
629
+ Dialog.__init__(self, parent, title, **kwargs)
630
+
631
+ def body(self, bodyFrame):
632
+ bodyFrame.config()
633
+ gui.configureWeigths(bodyFrame)
634
+ dialogFrame = tk.Frame(bodyFrame)
635
+ dialogFrame.grid(row=0, column=0, sticky='news', padx=5, pady=5)
636
+ dialogFrame.config()
637
+ gui.configureWeigths(dialogFrame, row=1)
638
+ self._createFilterBox(dialogFrame)
639
+ self._createTree(dialogFrame)
640
+ if self.previewCallBack:
641
+ self._createPreviewPanel(dialogFrame)
642
+
643
+ if self.message:
644
+ label = tk.Label(bodyFrame, text=self.message, compound=tk.LEFT,
645
+ image=self.getImage(Icon.LIGHTBULB))
646
+ label.grid(row=2, column=0, sticky='nw', padx=5, pady=5)
647
+ # CAncel this, now focus is set to the search box. self.initial_focus = self.tree
648
+
649
+ def _createTree(self, parent):
650
+ self.tree = BoundTree(parent, self.provider, selectmode=self._selectmode, style=LIST_TREEVIEW)
651
+ if self._selectOnDoubleClick:
652
+ self.tree.itemDoubleClick = lambda obj: self._handleResult(RESULT_YES)
653
+
654
+ if self.previewCallBack:
655
+ self.tree.itemClick = self._itemClick
656
+
657
+ self.tree.grid(row=1, column=0)
658
+
659
+ def _itemClick(self, obj):
660
+ self.previewCallBack(obj, self.previewFrame)
661
+
662
+ def _createPreviewPanel(self, parent):
663
+ self.previewFrame = tk.Frame(parent)
664
+ self.previewFrame.grid(row=1, column=1)
665
+
666
+ def _createFilterBox(self, content):
667
+ """ Create the Frame with Filter widgets """
668
+
669
+ self.searchBoxframe = tk.Frame(content)
670
+ label = tk.Label(self.searchBoxframe, text="Filter")
671
+ label.grid(row=0, column=0, sticky='nw')
672
+ self._searchVar = tk.StringVar(value='')
673
+ self.entry = tk.Entry(self.searchBoxframe, bg=Config.SCIPION_BG_COLOR,
674
+ textvariable=self._searchVar, width=40,
675
+ font=gui.getDefaultFont())
676
+
677
+ self.entry.bind('<KeyRelease>', self._onSearch)
678
+ self.entry.focus_set()
679
+ self.entry.grid(row=0, column=1, sticky='news')
680
+ self.initial_focus=self.entry
681
+ self.searchBoxframe.grid(row=0, column=0, sticky='news', padx=5,
682
+ pady=(10, 5))
683
+
684
+ def refresh(self):
685
+ """ Refreshes the list taking into account the filter"""
686
+ self._onSearch()
687
+
688
+ def _onSearch(self, e=None):
689
+
690
+ def comparison():
691
+ pattern = self._searchVar.get().lower()
692
+ return [w[0] for w in self.lista.items()
693
+ if pattern in self.lista.get(w[0]).lower()]
694
+
695
+ self.tree.update()
696
+ self.lista = {}
697
+
698
+ for item in self.tree.get_children():
699
+
700
+ itemStr = self.tree.item(item)['text']
701
+ for value in self.tree.item(item)['values']:
702
+ if isinstance(value, int):
703
+ itemStr = itemStr + ' ' + str(value)
704
+ else:
705
+ itemStr = itemStr + ' ' + value
706
+
707
+ self.lista[item] = itemStr
708
+
709
+ if self._searchVar.get() != '':
710
+ matchs = comparison()
711
+ if matchs:
712
+ for item in self.tree.get_children():
713
+ if item not in matchs:
714
+ self.tree.delete(item)
715
+ else:
716
+ self.tree.delete(*self.tree.get_children())
717
+
718
+ def apply(self):
719
+ self.values = self.tree.getSelectedObjects()
720
+
721
+ def validate(self):
722
+ self.apply() # load self.values with selected items
723
+ err = ''
724
+
725
+ if self.values:
726
+ if self.validateSelectionCallback:
727
+ err = self.validateSelectionCallback(self.values)
728
+ else:
729
+ if not self._allowsEmptySelection:
730
+ err = "Please select an element"
731
+
732
+ if err:
733
+ showError("Validation error", err, self)
734
+ return False
735
+
736
+ return True
737
+
738
+
739
+ class ToolbarButton:
740
+ """
741
+ Store information about the buttons that will be added to the toolbar.
742
+ """
743
+
744
+ def __init__(self, text, command, icon=None, tooltip=None, shortcut=None):
745
+ self.text = text
746
+ self.command = command
747
+ self.icon = icon
748
+ self.tooltip = tooltip
749
+ self.shortcut = shortcut
750
+
751
+
752
+ class ToolbarListDialog(ListDialog):
753
+ """
754
+ This class extend from ListDialog to allow an
755
+ extra toolbar to handle operations over the elements
756
+ in the list (e.g. Edit, New, Delete).
757
+ """
758
+
759
+ def __init__(self, parent, title, provider,
760
+ message=None, toolbarButtons=None, **kwargs):
761
+ """ From kwargs:
762
+ message: message tooltip to show when browsing.
763
+ selected: the item that should be selected.
764
+ validateSelectionCallback:
765
+ a callback function to validate selected items.
766
+ allowSelect: if set to False, the 'Select' button will not
767
+ be shown.
768
+ """
769
+ self.toolbarButtons = toolbarButtons
770
+ self._itemDoubleClick = kwargs.get('itemDoubleClick', None)
771
+ self._itemOnClick = kwargs.get('itemOnClick', None)
772
+ ListDialog.__init__(self, parent, title, provider, message, **kwargs)
773
+
774
+ def body(self, bodyFrame):
775
+ gui.configureWeigths(bodyFrame, 1, 0)
776
+
777
+ # Add an extra frame to insert the Toolbar
778
+ # and another one for the ListDialog's body
779
+ self.toolbarFrame = tk.Frame(bodyFrame)
780
+ self.toolbarFrame.grid(row=0, column=0, sticky='new')
781
+
782
+ subBody = tk.Frame(bodyFrame)
783
+ subBody.grid(row=1, column=0, sticky='news', padx=5, pady=5)
784
+ ListDialog.body(self, subBody)
785
+
786
+ if self.toolbarButtons:
787
+ for i, b in enumerate(self.toolbarButtons):
788
+ self.addButton(b, i)
789
+
790
+ if self._itemDoubleClick:
791
+ self.tree.itemDoubleClick = self._itemDoubleClick
792
+
793
+ if self._itemOnClick:
794
+ self.tree.itemOnClick = self._itemOnClick
795
+
796
+ def addButton(self, button, col):
797
+
798
+ self._addButton(self.toolbarFrame, button.command, text=button.text, icon=button.icon, col=col, tooltip=button.tooltip, shortcut=button.shortcut)
799
+
800
+
801
+ class FlashMessage:
802
+ def __init__(self, master, msg, delay=5, relief='solid', func=None):
803
+ self.root = tk.Toplevel(master=master)
804
+ # hides until know geometry
805
+ self.root.withdraw()
806
+ self.root.wm_overrideredirect(1)
807
+ tk.Label(self.root, text=" %s " % msg,
808
+ bd=1, bg='DodgerBlue4', fg='white').pack()
809
+ gui.centerWindows(self.root, refWindows=master)
810
+ self.root.deiconify()
811
+ self.root.grab_set()
812
+ self.msg = msg
813
+
814
+ if func:
815
+ self.root.update_idletasks()
816
+ self.root.after(10, self.process, func)
817
+ else:
818
+ self.root.after(int(delay * 1000), self.close)
819
+ self.root.wait_window(self.root)
820
+
821
+ def process(self, func):
822
+ func()
823
+ self.root.destroy()
824
+
825
+ def close(self):
826
+ self.root.destroy()
827
+
828
+
829
+ class FloatingMessage:
830
+ def __init__(self, master, msg, xPos=None, yPos=None, textWidth=280,
831
+ font='Helvetica', size=12, bd=1, bg=Config.SCIPION_MAIN_COLOR, fg='white'):
832
+ if xPos is None:
833
+ xPos = (master.winfo_width() - textWidth) / 2
834
+ yPos = master.winfo_height() / 2
835
+
836
+ self.floatingMessage = tk.Label(master, text=" %s " % msg,
837
+ bd=bd, bg=bg, fg=fg)
838
+ self.floatingMessage.place(x=xPos, y=yPos, width=textWidth)
839
+ self.floatingMessage.config(font=(font, size))
840
+
841
+ def setMessage(self, msg):
842
+ self.floatingMessage.config(text=msg)
843
+
844
+ def show(self):
845
+ self.floatingMessage.update_idletasks()
846
+
847
+ def close(self):
848
+ self.floatingMessage.destroy()
849
+
850
+
851
+
852
+ class SearchBaseWindow(Window):
853
+ """ Base window for searching in a list
854
+ You are going to implement several elements:
855
+
856
+ columnsConfig: a dictionary with elements with this structure:
857
+ <column-key>: (<title>,{kwargs for tree.column method}, weight, <casting_method>(optional, otherwise str))
858
+
859
+ Example:
860
+
861
+ columnConfig = {
862
+ '#0': ('Status', {'width': 50, 'minwidth': 50, 'stretch': tk.NO}, 3),
863
+ 'protocol': ('Protocol', {'width': 300, 'stretch': tk.FALSE}), 5,
864
+ 'streaming': ('Streamified', {'width': 100, 'stretch': tk.FALSE}, 3),
865
+ 'installed': ('Installation', {'width': 110, 'stretch': tk.FALSE}, 3),
866
+ 'help': ('Help', {'minwidth': 300, 'stretch': tk.YES}, 3),
867
+ 'score': ('Score', {'width': 50, 'stretch': tk.FALSE}, 3, int),
868
+ }
869
+
870
+ _createResultsTree method
871
+ _onSearchClick method
872
+
873
+ See SearchProtocolWindow as an example
874
+
875
+ """
876
+ COLUMN_TEXT_INDEX = 0
877
+ COLUMN_KWARGS_INDEX = 1
878
+ WEIGHT_INDEX = 2
879
+ CASTING_INDEX = 3
880
+ columnConfig = {} # Columns configuration
881
+
882
+ def __init__(self, parentWindow, title="Search element", onClick=None, onDoubleClick=None, **kwargs):
883
+ super().__init__(title=title,
884
+ masterWindow=parentWindow)
885
+
886
+ self.onClick = self._click if onClick is None else onClick
887
+ self.onDoubleClick = self._double_click if onDoubleClick is None else onDoubleClick
888
+
889
+ content = tk.Frame(self.root, bg=Config.SCIPION_BG_COLOR)
890
+ self._createContent(content)
891
+ content.grid(row=0, column=0, sticky='news')
892
+ content.columnconfigure(0, weight=1)
893
+ content.rowconfigure(1, weight=1)
894
+
895
+ def getColumnKeys(self):
896
+ return self.columnConfig.keys()
897
+
898
+ def _createContent(self, content):
899
+ self._createSearchBox(content)
900
+ self._createResultsBox(content)
901
+
902
+ def _createSearchBox(self, content):
903
+ """ Create the Frame with Search widgets """
904
+ frame = tk.Frame(content, bg=Config.SCIPION_BG_COLOR)
905
+
906
+ label = tk.Label(frame, text="Search", bg=Config.SCIPION_BG_COLOR)
907
+ label.grid(row=0, column=0, sticky='nw')
908
+ self._searchVar = tk.StringVar()
909
+ entry = tk.Entry(frame, bg='white', textvariable=self._searchVar, font=gui.getDefaultFont())
910
+ entry.bind(TK.RETURN, self._onSearchClick)
911
+ entry.bind(TK.ENTER, self._onSearchClick)
912
+ entry.focus_set()
913
+ entry.grid(row=0, column=1, sticky='nw')
914
+ self.initial_focus=entry
915
+ btn = widgets.IconButton(frame, "Search",
916
+ imagePath=Icon.ACTION_SEARCH,
917
+ command=self._onSearchClick)
918
+ btn.grid(row=0, column=2, sticky='nw')
919
+
920
+ frame.grid(row=0, column=0, sticky='new', padx=5, pady=(10, 5))
921
+
922
+ return frame
923
+
924
+ def _createResultsBox(self, content):
925
+ frame = tk.Frame(content, bg=Color.ALT_COLOR, padx=5, pady=5)
926
+ configureWeigths(frame)
927
+ self._resultsTree = self._createResultsTree(frame,
928
+ show=None,
929
+ columns=list(self.getColumnKeys())[1:])
930
+ self._configureTreeColumns()
931
+ self._resultsTree.grid(row=0, column=0, sticky='news')
932
+ frame.grid(row=1, column=0, sticky='news', padx=5, pady=5)
933
+
934
+ def _createResultsTree(self, frame, show, columns):
935
+
936
+ t = Tree(frame, show=show, columns=columns, style=LIST_TREEVIEW)
937
+ t.column('#0', minwidth=100)
938
+ t.bind("<Button-1>", self.onClick)
939
+ t.bind("<Double-1>", self.onDoubleClick)
940
+ return t
941
+
942
+ def _click(self, event):
943
+ """ To be implemented, triggered on tree-view click """
944
+ pass
945
+
946
+ def _double_click(self, event):
947
+ """ To be implemented, triggered on tree-view double click """
948
+ pass
949
+
950
+ def addSearchWeight(self, line2Search, searchtext):
951
+ # Adds a weight value for the search
952
+ weight = 0
953
+
954
+ linelower = [str(v).lower() for v in line2Search]
955
+
956
+ for index, column in enumerate(self.columnConfig.values()):
957
+
958
+ if searchtext in linelower[index]:
959
+ # prioritize findings in label
960
+ weight += column[self.WEIGHT_INDEX] * 2
961
+
962
+ elif " " in searchtext:
963
+ for word in searchtext.split():
964
+ if word in linelower[index]:
965
+ weight += column[self.WEIGHT_INDEX]
966
+
967
+ return line2Search + (weight,)
968
+
969
+ def _configureTreeColumns(self):
970
+
971
+ for key, columnConf in self.columnConfig.items():
972
+ casting = str if len(columnConf) <= self.CASTING_INDEX else columnConf[self.CASTING_INDEX]
973
+ self._resultsTree.column(key, **columnConf[self.COLUMN_KWARGS_INDEX])
974
+ self._resultsTree.heading(key,
975
+ text=columnConf[self.COLUMN_TEXT_INDEX],
976
+ command=lambda bound_key=key, bound_casting=casting:
977
+ self._resultsTree.sortByColumn(bound_key, False, casting=bound_casting))
978
+
979
+ def _onSearchClick(self, e=None):
980
+ """ To be implemented, triggered on search button click"""
981
+ pass