scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) 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 +113 -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 +267 -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 +439 -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 +760 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +979 -0
  17. pyworkflow/gui/form.py +2726 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +566 -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 +192 -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 +238 -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 +510 -0
  35. pyworkflow/gui/project/viewprotocols.py +2116 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +771 -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 +222 -0
  43. pyworkflow/mapper/sqlite.py +1581 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/project/__init__.py +31 -0
  46. pyworkflow/project/config.py +454 -0
  47. pyworkflow/project/manager.py +180 -0
  48. pyworkflow/project/project.py +2095 -0
  49. pyworkflow/project/usage.py +165 -0
  50. pyworkflow/protocol/__init__.py +38 -0
  51. pyworkflow/protocol/bibtex.py +48 -0
  52. pyworkflow/protocol/constants.py +87 -0
  53. pyworkflow/protocol/executor.py +483 -0
  54. pyworkflow/protocol/hosts.py +317 -0
  55. pyworkflow/protocol/launch.py +277 -0
  56. pyworkflow/protocol/package.py +42 -0
  57. pyworkflow/protocol/params.py +781 -0
  58. pyworkflow/protocol/protocol.py +2707 -0
  59. pyworkflow/tests/__init__.py +29 -0
  60. pyworkflow/tests/test_utils.py +25 -0
  61. pyworkflow/tests/tests.py +341 -0
  62. pyworkflow/utils/__init__.py +38 -0
  63. pyworkflow/utils/dataset.py +414 -0
  64. pyworkflow/utils/echo.py +104 -0
  65. pyworkflow/utils/graph.py +169 -0
  66. pyworkflow/utils/log.py +293 -0
  67. pyworkflow/utils/path.py +528 -0
  68. pyworkflow/utils/process.py +153 -0
  69. pyworkflow/utils/profiler.py +92 -0
  70. pyworkflow/utils/progressbar.py +154 -0
  71. pyworkflow/utils/properties.py +617 -0
  72. pyworkflow/utils/reflection.py +129 -0
  73. pyworkflow/utils/utils.py +880 -0
  74. pyworkflow/utils/which.py +229 -0
  75. pyworkflow/webservices/__init__.py +8 -0
  76. pyworkflow/webservices/config.py +8 -0
  77. pyworkflow/webservices/notifier.py +152 -0
  78. pyworkflow/webservices/repository.py +59 -0
  79. pyworkflow/webservices/workflowhub.py +74 -0
  80. pyworkflowtests/tests/__init__.py +0 -0
  81. pyworkflowtests/tests/test_canvas.py +72 -0
  82. pyworkflowtests/tests/test_domain.py +45 -0
  83. pyworkflowtests/tests/test_logs.py +74 -0
  84. pyworkflowtests/tests/test_mappers.py +392 -0
  85. pyworkflowtests/tests/test_object.py +507 -0
  86. pyworkflowtests/tests/test_project.py +42 -0
  87. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  88. pyworkflowtests/tests/test_protocol_export.py +78 -0
  89. pyworkflowtests/tests/test_protocol_output.py +158 -0
  90. pyworkflowtests/tests/test_streaming.py +47 -0
  91. pyworkflowtests/tests/test_utils.py +210 -0
  92. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +2 -2
  93. scipion_pyworkflow-3.11.1.dist-info/RECORD +161 -0
  94. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  95. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +0 -0
  96. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
  97. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,760 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es) [1]
4
+ # * Jose Gutierrez (jose.gutierrez@cnb.csic.es) [2]
5
+ # *
6
+ # * [1] SciLifeLab, Stockholm University
7
+ # * [2] Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
8
+ # *
9
+ # * This program is free software: you can redistribute it and/or modify
10
+ # * it under the terms of the GNU General Public License as published by
11
+ # * the Free Software Foundation, either version 3 of the License, or
12
+ # * (at your option) any later version.
13
+ # *
14
+ # * This program is distributed in the hope that it will be useful,
15
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # * GNU General Public License for more details.
18
+ # *
19
+ # * You should have received a copy of the GNU General Public License
20
+ # * along with this program. If not, see <https://www.gnu.org/licenses/>.
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
+ In this module a simple ObjectBrowser is implemented.
28
+ This class can be subclasses to extend its functionality.
29
+ A concrete use of ObjectBrowser is FileBrowser, where the
30
+ elements to inspect and preview are files.
31
+ """
32
+ import os.path
33
+ import stat
34
+ import tkinter as tk
35
+ import time
36
+ import logging
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ import pyworkflow.utils as pwutils
41
+ from . import gui, LIST_TREEVIEW
42
+ from .tree import BoundTree, TreeProvider
43
+ from .text import TaggedText, openTextFileEditor
44
+ from .widgets import Button, HotButton
45
+ from .. import Config
46
+
47
+ PARENT_FOLDER = ".."
48
+
49
+
50
+ class ObjectBrowser(tk.Frame):
51
+ """ This class will implement a simple object browser.
52
+ Basically, it will display a list of elements at the left
53
+ panel and can display a preview and description on the
54
+ right panel for the selected element.
55
+ An ObjectView will be used to grab information for
56
+ each element such as: icon, preview and description.
57
+ A TreeProvider will be used to populate the list (Tree).
58
+ """
59
+
60
+ def __init__(self, parent, treeProvider,
61
+ showPreview=True, showPreviewTop=True,
62
+ **args):
63
+ tk.Frame.__init__(self, parent, **args)
64
+ self.treeProvider = treeProvider
65
+ self._lastSelected = None
66
+ gui.configureWeigths(self)
67
+ self.showPreviewTop = showPreviewTop
68
+ # The main layout will be two panes,
69
+ # At the left containing the elements list
70
+ # and the right containing the preview and description
71
+ p = tk.PanedWindow(self, orient=tk.HORIZONTAL)
72
+ p.grid(row=0, column=0, sticky='news')
73
+
74
+ leftPanel = tk.Frame(p)
75
+ self._fillLeftPanel(leftPanel)
76
+ p.add(leftPanel, padx=5, pady=5)
77
+ p.paneconfig(leftPanel, minsize=300)
78
+
79
+ if showPreview:
80
+ rightPanel = tk.Frame(p)
81
+ self._fillRightPanel(rightPanel)
82
+ p.add(rightPanel, padx=5, pady=5)
83
+ p.paneconfig(rightPanel, minsize=200)
84
+
85
+ # Register a callback when the item is clicked
86
+ self.tree.itemClick = self._itemClicked
87
+
88
+ def _fillLeftPanel(self, frame):
89
+ gui.configureWeigths(frame)
90
+ self.tree = BoundTree(frame, self.treeProvider, style=LIST_TREEVIEW)
91
+ self.tree.grid(row=0, column=0, sticky='news')
92
+ self.itemConfig = self.tree.itemConfig
93
+ self.getImage = self.tree.getImage
94
+
95
+ def _fillRightPanel(self, frame):
96
+ frame.columnconfigure(0, weight=1)
97
+
98
+ if self.showPreviewTop:
99
+ top = tk.Frame(frame)
100
+ top.grid(row=0, column=0, sticky='news')
101
+ frame.rowconfigure(0, weight=3)
102
+ gui.configureWeigths(top)
103
+ top.rowconfigure(0, minsize=200)
104
+ self._fillRightTop(top)
105
+
106
+ bottom = tk.Frame(frame)
107
+ bottom.grid(row=1, column=0, sticky='news')
108
+ frame.rowconfigure(1, weight=1)
109
+ gui.configureWeigths(bottom)
110
+ bottom.rowconfigure(1, weight=1)
111
+ self._fillRightBottom(bottom)
112
+
113
+ def _fillRightTop(self, top):
114
+ self.noImage = self.getImage(pwutils.Icon.NO_IMAGE_128)
115
+ self.label = tk.Label(top, image=self.noImage)
116
+ self.label.grid(row=0, column=0, sticky='news')
117
+
118
+ def _fillRightBottom(self, bottom):
119
+ self.text = TaggedText(bottom, width=40, height=15, bg=Config.SCIPION_BG_COLOR,
120
+ takefocus=0)
121
+ self.text.grid(row=0, column=0, sticky='news')
122
+
123
+ def _itemClicked(self, obj):
124
+ self._lastSelected = obj
125
+ img, desc = self.treeProvider.getObjectPreview(obj)
126
+ # Update image preview
127
+ if self.showPreviewTop:
128
+ if isinstance(img, (str, pwutils.SpriteImage)):
129
+ img = self.getImage(img)
130
+ if img is None:
131
+ img = self.noImage
132
+ self.label.config(image=img)
133
+
134
+ # Update text preview
135
+ self.text.setReadOnly(False)
136
+ self.text.clear()
137
+
138
+ if desc is not None:
139
+ self.text.addText(desc)
140
+ self.text.setReadOnly(True)
141
+ if hasattr(self, 'entryLabel') and not self._lastSelected.isDir():
142
+ self.entryVar.set(self._lastSelected.getFileName())
143
+
144
+ def getSelected(self):
145
+ """ Return the selected object. """
146
+ return self._lastSelected
147
+
148
+
149
+ # ------------ Classes and Functions related to File browsing --------------
150
+
151
+ class FileInfo(object):
152
+ """ This class will store some information about a file.
153
+ It will serve to display files items in the Tree.
154
+ """
155
+
156
+ def __init__(self, path, filename):
157
+ self._fullpath = os.path.join(path, filename)
158
+ self._filename = filename
159
+ if os.path.exists(self._fullpath):
160
+ self._stat = os.stat(self._fullpath)
161
+ else:
162
+ self._stat = None
163
+
164
+ def isDir(self):
165
+ return stat.S_ISDIR(self._stat.st_mode) if self._stat else False
166
+
167
+ def getFileName(self):
168
+ return self._filename
169
+
170
+ def getPath(self):
171
+ return self._fullpath
172
+
173
+ def getSize(self):
174
+ return self._stat.st_size if self._stat else 0
175
+
176
+ def getSizeStr(self):
177
+ """ Return a human readable string of the file size."""
178
+ return pwutils.prettySize(self.getSize()) if self._stat else '0'
179
+
180
+ def getDateStr(self):
181
+ return pwutils.dateStr(self.getDate()) if self._stat else '0'
182
+
183
+ def getDate(self):
184
+ return self._stat.st_mtime if self._stat else 0
185
+
186
+ def isLink(self):
187
+ return os.path.islink(self._fullpath)
188
+
189
+
190
+ class FileHandler(object):
191
+ """ This class will be used to get the icon, preview and info
192
+ from the different types of objects.
193
+ It should be used with FileTreeProvider, where different
194
+ types of handlers can be registered.
195
+ """
196
+
197
+ def getFileIcon(self, objFile):
198
+ """ Return the icon name for a given file. """
199
+ if objFile.isDir():
200
+ icon = pwutils.Icon.FOLDER if not objFile.isLink() else pwutils.Icon.FOLDER_LINK
201
+ else:
202
+ icon = pwutils.Icon.FILE if not objFile.isLink() else pwutils.Icon.FILE_LINK
203
+
204
+ return icon
205
+
206
+ def getFilePreview(self, objFile):
207
+ """ Return the preview image and description for the specific object."""
208
+ if objFile.isDir():
209
+ return pwutils.Icon.FOLDER_OPEN, None
210
+ return None, None
211
+
212
+ def getFileActions(self, objFile):
213
+ """ Return actions that can be done with this object.
214
+ Actions will be displayed in the context menu
215
+ and the first one will be the default when double-click.
216
+ """
217
+ return []
218
+
219
+
220
+ class FSFileHandler(FileHandler):
221
+
222
+ def __init__(self):
223
+ self._refresh_callback = None
224
+
225
+ def setRefresh(self, refresh_callback):
226
+ self._refresh_callback = refresh_callback
227
+
228
+ def copyToClipboard(self, file):
229
+ import pyperclip
230
+ pyperclip.copy(file)
231
+ logger.info(f'{file} copy to clipboard')
232
+
233
+ def deleteFile(self, file):
234
+ pwutils.cleanPath(file)
235
+
236
+ if self._refresh_callback:
237
+ self._refresh_callback(None)
238
+ def getFileActions(self, objFile):
239
+ """ Return basic os actions like delete or copy to clipboard
240
+ """
241
+ fn = objFile.getPath()
242
+ return [('Copy path', lambda: self.copyToClipboard(fn), pwutils.Icon.ACTION_COPY),
243
+ ('Delete', lambda: self.deleteFile(fn), pwutils.Icon.DELETE_OPERATION)
244
+ ]
245
+
246
+
247
+ class TextFileHandler(FileHandler):
248
+ def __init__(self, textIcon):
249
+ FileHandler.__init__(self)
250
+ self._icon = textIcon
251
+
252
+ def getFileIcon(self, objFile):
253
+ return self._icon
254
+
255
+
256
+ class SqlFileHandler(FileHandler):
257
+ def getFileIcon(self, objFile):
258
+ return pwutils.Icon.DB
259
+
260
+
261
+ class FileTreeProvider(TreeProvider):
262
+ """ Populate a tree with files and folders of a given path """
263
+
264
+ _FILE_HANDLERS = {}
265
+ _FS_HANDLER = FSFileHandler()
266
+ FILE_COLUMN = 'File'
267
+ SIZE_COLUMN = 'Size'
268
+
269
+ @classmethod
270
+ def registerFileHandler(cls, fileHandler, *extensions):
271
+ """ Register a FileHandler for a given file extension.
272
+ Params:
273
+ fileHandler: the FileHandler that will take care of extensions.
274
+ *extensions: the extensions list that will be associated to this
275
+ FileHandler.
276
+ """
277
+ for fileExt in extensions:
278
+ handlersList = cls._FILE_HANDLERS.get(fileExt, [])
279
+ handlersList.append(fileHandler)
280
+ cls._FILE_HANDLERS[fileExt] = handlersList
281
+
282
+ def __init__(self, currentDir, showHidden, onlyFolders, browser):
283
+ TreeProvider.__init__(self, sortingColumnName=self.FILE_COLUMN)
284
+ self._currentDir = os.path.abspath(currentDir)
285
+ self._showHidden = showHidden
286
+ self._onlyFolders = onlyFolders
287
+ self._browser = browser
288
+ self.getColumns = lambda: [(self.FILE_COLUMN, 300),
289
+ (self.SIZE_COLUMN, 70), ('Time', 150)]
290
+
291
+ def getFileHandlers(self, obj):
292
+ filename = obj.getFileName()
293
+ fileExt = pwutils.getExt(filename)
294
+ fhs = self._FILE_HANDLERS.get(fileExt,[])
295
+ # add basic options: delete, copy. They do not depend on the extension.
296
+ if self._FS_HANDLER not in fhs:
297
+ fhs.append(self._FS_HANDLER)
298
+ return fhs
299
+
300
+ def getObjectInfo(self, obj):
301
+ filename = obj.getFileName()
302
+ fileHandlers = self.getFileHandlers(obj)
303
+ icon = fileHandlers[0].getFileIcon(obj)
304
+
305
+ info = {'key': filename, 'text': filename,
306
+ 'values': (obj.getSizeStr(), obj.getDateStr()), 'image': icon
307
+ }
308
+
309
+ return info
310
+
311
+ def getObjectPreview(self, obj):
312
+
313
+ try:
314
+ # Look for any preview available
315
+ fileHandlers = self.getFileHandlers(obj)
316
+
317
+ for fileHandler in fileHandlers:
318
+ preview = fileHandler.getFilePreview(obj)
319
+ if preview:
320
+ img, desc = preview
321
+ if obj.isLink():
322
+ desc = "Is a link" if desc is None else desc + "\nIs a link."
323
+ return img, desc
324
+
325
+ except Exception as e:
326
+ msg = "Couldn't get preview for %s" % obj
327
+ logger.error(msg, exc_info=e)
328
+ return None, msg + " See scipion GUI log window for more details."
329
+
330
+ def getObjectActions(self, obj):
331
+ fileHandlers = self.getFileHandlers(obj)
332
+ actions = []
333
+ for fileHandler in fileHandlers:
334
+ if fileHandler == self._FS_HANDLER:
335
+ fileHandler.setRefresh(self._browser._actionRefresh)
336
+ actions += fileHandler.getFileActions(obj)
337
+ # Always allow the option to open as text
338
+ # specially useful for unknown formats
339
+ fn = obj.getPath()
340
+ actions.append(("Open external program",
341
+ lambda: openTextFileEditor(fn), pwutils.Icon.ACTION_REFERENCES))
342
+
343
+ return actions
344
+
345
+ def getObjects(self):
346
+
347
+ fileInfoList = []
348
+ if not self._currentDir == pwutils.ROOT:
349
+ fileInfoList.append(FileInfo(self._currentDir, PARENT_FOLDER))
350
+
351
+ try:
352
+ # This might fail if there is not granted
353
+ files = os.listdir(self._currentDir)
354
+
355
+ for f in files:
356
+
357
+ fullPath = os.path.join(self._currentDir, f)
358
+ # If f is a file and only need folders
359
+ if self._onlyFolders and not os.path.isdir(fullPath):
360
+ continue
361
+
362
+ # Do not add hidden files if not requested
363
+ if not self._showHidden and f.startswith('.'):
364
+ continue
365
+
366
+ # All ok...add item.
367
+ fileInfoList.append(FileInfo(self._currentDir, f))
368
+ except Exception as e:
369
+ logger.info("Can't list files at " + self._currentDir, e)
370
+
371
+ # Sort objects
372
+ fileInfoList.sort(key=self.fileKey, reverse=not self.isSortingAscending())
373
+
374
+ return fileInfoList
375
+
376
+ def fileKey(self, f):
377
+ sortDict = {self.FILE_COLUMN: 'getFileName',
378
+ self.SIZE_COLUMN: 'getSize'}
379
+ return getattr(f, sortDict.get(self._sortingColumnName, 'getDate'))()
380
+
381
+ def getDir(self):
382
+ return self._currentDir
383
+
384
+ def setDir(self, newPath):
385
+ self._currentDir = newPath
386
+
387
+
388
+ # Some constants for the type of selection
389
+ # when the file browser is opened
390
+
391
+ SELECT_NONE = 0 # No selection, just browse files
392
+ SELECT_FILE = 1
393
+ SELECT_FOLDER = 2
394
+ SELECT_PATH = 3 # Can be either file or folder
395
+
396
+
397
+ class FileBrowser(ObjectBrowser):
398
+ """ The FileBrowser is a particular class of ObjectBrowser (Tk.Frame)
399
+ where the "objects" are just files and directories.
400
+ """
401
+
402
+ _lastSelectedFile = None
403
+ "Class scope attribute to keep the lastSelected file"
404
+
405
+ _fileSelectedAtLoading = None
406
+ "Class scope attribute to offer *Recent* shortcut"
407
+
408
+ def __init__(self, parent, initialDir='.',
409
+ selectionType=SELECT_FILE,
410
+ selectionSingle=True,
411
+ allowFilter=True,
412
+ filterFunction=None,
413
+ previewDim=144,
414
+ showHidden=False, # Show hidden files or not?
415
+ selectButton='Select', # Change the Select button text
416
+ entryLabel=None, # Display an entry for some input
417
+ entryValue='', # Display a value in the entry field
418
+ showInfo=None, # Used to notify errors or messages
419
+ shortCuts=None, # Shortcuts to common locations/paths
420
+ onlyFolders=False
421
+ ):
422
+ """
423
+
424
+ :param parent: Parent tkinter window.
425
+ :param initialDir: Folder to show when loading the dialog.
426
+ :param selectionType: Any of SELECT_NONE, SELECT_FILE, SELECT_FOLDER, SELECT_PATH.
427
+ :param showHidden: Pass True to show hidden files.
428
+ :param selectButton: text for the select button. Defaults to *Select*.
429
+ :param entryLabel: text for the entry widget. Default None. There will be no entry.
430
+ :param entryValue: default value for the entry. Needs entryLabel.
431
+ :param showInfo: callback to show a string message, otherwise _showInfo will be used.
432
+ :param shortCuts: list of extra :class:`ShortCut`
433
+ :param onlyFolders: Pass True to show only folders.
434
+ """
435
+ self.pathVar = tk.StringVar()
436
+ self.pathVar.set(os.path.abspath(initialDir))
437
+ self.pathEntry = None
438
+ self.previousSearch = None
439
+ self.previousSearchTS = None
440
+ self.shortCuts = shortCuts
441
+ self._provider = FileTreeProvider(initialDir, showHidden, onlyFolders, self)
442
+ self.selectButton = selectButton
443
+ self.entryLabel = entryLabel
444
+ self.entryVar = tk.StringVar()
445
+ self.entryVar.set(entryValue)
446
+
447
+ self.showInfo = showInfo or self._showInfo
448
+
449
+ ObjectBrowser.__init__(self, parent, self._provider)
450
+
451
+ # focuses on the browser in order to allow to move with the keyboard
452
+ self._goDir(os.path.abspath(initialDir))
453
+
454
+ buttonsFrame = tk.Frame(self)
455
+ self._fillButtonsFrame(buttonsFrame)
456
+ buttonsFrame.grid(row=1, column=0)
457
+
458
+ # Callback to be called "on Select" button key press
459
+ self.onSelect=None
460
+
461
+ def _showInfo(self, msg):
462
+ """ Default way (logger.info to console) to show a message with a given info.
463
+ """
464
+ logger.info(msg)
465
+
466
+ def _fillLeftPanel(self, frame):
467
+ """ Redefine this method to include a buttons toolbar and
468
+ also include a filter bar at the bottom of the Tree.
469
+ """
470
+ # Tree with files
471
+ frame.columnconfigure(0, weight=1)
472
+
473
+ treeFrame = tk.Frame(frame)
474
+ ObjectBrowser._fillLeftPanel(self, treeFrame)
475
+ # Register the double-click event
476
+ self.tree.itemDoubleClick = self._itemDoubleClick
477
+ # Register keypress event
478
+ self.tree.itemKeyPressed = self._itemKeyPressed
479
+
480
+ treeRow = 3
481
+ treeFrame.grid(row=treeRow, column=0, sticky='news')
482
+ # Toolbar frame
483
+ toolbarFrame = tk.Frame(frame)
484
+ self._fillToolbar(toolbarFrame)
485
+ toolbarFrame.grid(row=0, column=0, sticky='new')
486
+
487
+ pathFrame = tk.Frame(frame)
488
+ pathLabel = tk.Label(pathFrame, text='Path')
489
+ pathLabel.grid(row=0, column=0, padx=0, pady=3)
490
+ pathEntry = tk.Entry(pathFrame, bg='white', width=65,
491
+ textvariable=self.pathVar, font=gui.getDefaultFont())
492
+ pathEntry.grid(row=0, column=1, sticky='new', pady=3)
493
+ pathEntry.bind("<Return>", self._onEnterPath)
494
+ pathEntry.bind("<KP_Enter>", self._onEnterPath)
495
+ self.pathEntry = pathEntry
496
+ pathFrame.grid(row=1, column=0, sticky='new')
497
+
498
+ # Entry frame, could be used for filter
499
+ if self.entryLabel:
500
+ entryFrame = tk.Frame(frame)
501
+ entryFrame.grid(row=2, column=0, sticky='new')
502
+ tk.Label(entryFrame, text=self.entryLabel).grid(row=0, column=0,
503
+ sticky='nw', pady=3)
504
+ tk.Entry(entryFrame,
505
+ textvariable=self.entryVar,
506
+ bg=Config.SCIPION_BG_COLOR,
507
+ width=65,
508
+ font=gui.getDefaultFont()).grid(row=0, column=1, sticky='nw', pady=3)
509
+
510
+ frame.rowconfigure(treeRow, weight=1)
511
+
512
+ def _addButton(self, frame, text, image, command):
513
+ btn = tk.Label(frame, text=text, image=self.getImage(image),
514
+ compound=tk.LEFT, cursor='hand2')
515
+ btn.bind('<Button-1>', command)
516
+ btn.grid(row=0, column=self._col, sticky='nw',
517
+ padx=(0, 5), pady=5)
518
+ self._col += 1
519
+
520
+ def _fillToolbar(self, frame):
521
+ """ Fill the toolbar frame with some buttons. """
522
+ self._col = 0
523
+
524
+ self._addButton(frame, 'Refresh', pwutils.Icon.ACTION_REFRESH,
525
+ self._actionRefresh)
526
+ self._addButton(frame, 'Home', pwutils.Icon.HOME, self._actionHome)
527
+ self._addButton(frame, 'Launch folder', pwutils.Icon.ROCKET,
528
+ self._actionLaunchFolder)
529
+ self._addButton(frame, 'Working dir', pwutils.Icon.ACTION_BROWSE,
530
+ self._actionWorkingDir)
531
+ self._addButton(frame, 'Up', pwutils.Icon.ARROW_UP, self._actionUp)
532
+
533
+ self._fileSelectedAtLoading = FileBrowser._lastSelectedFile
534
+
535
+ if self._fileSelectedAtLoading is not None:
536
+ self._addButton(frame, 'Recent', None, self._actionRecent)
537
+
538
+ # Add shortcuts
539
+ self._addShortCuts(frame)
540
+
541
+ def _addShortCuts(self, frame):
542
+ """ Add shortcuts if available"""
543
+ if self.shortCuts:
544
+ for shortCut in self.shortCuts:
545
+ self._addButton(frame,
546
+ shortCut.name,
547
+ shortCut.icon,
548
+ lambda e: self._goDir(shortCut.path))
549
+
550
+ def _fillButtonsFrame(self, frame):
551
+ """ Add button to the bottom frame if the selectMode
552
+ is distinct from SELECT_NONE.
553
+ """
554
+ Button(frame, "Close", pwutils.Icon.BUTTON_CLOSE,
555
+ command=self._close).grid(row=0, column=0, padx=(0, 5))
556
+ if self.selectButton:
557
+ HotButton(frame, self.selectButton, pwutils.Icon.BUTTON_SELECT,
558
+ command=self._select).grid(row=0, column=1)
559
+
560
+ def _actionRefresh(self, e=None):
561
+ self.tree.update()
562
+
563
+ def _goDir(self, newDir):
564
+
565
+ newDir = os.path.abspath(newDir)
566
+
567
+ # Add a final "/" to the path: abspath is removing it except for "/"
568
+ if not newDir.endswith(os.path.sep):
569
+ newDir += os.path.sep
570
+
571
+ self.pathVar.set(newDir)
572
+ self.pathEntry.icursor(len(newDir))
573
+ self.treeProvider.setDir(newDir)
574
+ self.tree.update()
575
+ self.tree.focus_set()
576
+
577
+ itemKeyToFocus = PARENT_FOLDER
578
+ if PARENT_FOLDER not in self.tree._objDict:
579
+ itemKeyToFocus = self.tree.get_children()[0]
580
+
581
+ # Focusing on a item, but nothing is selected
582
+ # Current dir remains in _lastSelected
583
+ self._lastSelected = FileInfo(os.path.dirname(newDir),
584
+ os.path.basename(newDir))
585
+
586
+ FileBrowser._lastSelectedFile = self._lastSelected
587
+
588
+ self.tree.focus(itemKeyToFocus)
589
+
590
+ def _actionUp(self, e=None):
591
+ parentFolder = pwutils.getParentFolder(self.treeProvider.getDir())
592
+ self._goDir(parentFolder)
593
+
594
+ def _actionRecent(self, e=None):
595
+ self._goDir(self._fileSelectedAtLoading.getPath())
596
+
597
+ def _actionHome(self, e=None):
598
+ self._goDir(pwutils.getHomePath())
599
+
600
+ def _actionRoot(self, e=None):
601
+ self._goDir("/")
602
+
603
+ def _actionLaunchFolder(self, e=None):
604
+ self._goDir(Config.SCIPION_CWD)
605
+
606
+ def _actionWorkingDir(self, e=None):
607
+ self._goDir(os.getcwd())
608
+
609
+ def _itemDoubleClick(self, obj):
610
+ if obj.isDir():
611
+ self._goDir(obj.getPath())
612
+ else:
613
+ actions = self._provider.getObjectActions(obj)
614
+ if actions:
615
+ # actions[0] = first Action, [1] = the action callback
616
+ actions[0][1]()
617
+
618
+ def _itemKeyPressed(self, obj, e=None):
619
+
620
+ if e.keysym in [pwutils.KEYSYM.RETURN]:
621
+ self._itemDoubleClick(obj)
622
+ return
623
+
624
+ textToSearch = self._composeTextToSearch(e.char)
625
+
626
+ # locate an item in starting with that letter.
627
+ self._searchItem(textToSearch)
628
+
629
+ def _composeTextToSearch(self, newChar):
630
+
631
+ currentMiliseconds = time.time()
632
+
633
+ if (self.previousSearchTS is not None) and \
634
+ ((currentMiliseconds - self.previousSearchTS) < 0.3):
635
+ newChar = self.previousSearch + newChar
636
+
637
+ self.previousSearch = newChar
638
+ self.previousSearchTS = currentMiliseconds
639
+
640
+ return newChar
641
+
642
+ def _searchItem(self, char):
643
+ """ locate an item in starting with that letter."""
644
+ try:
645
+ self.tree.search(char)
646
+ except Exception as e:
647
+ # seems to raise an exception but selects things right.
648
+ pass
649
+
650
+ def _onEnterPath(self, e=None):
651
+ path = os.path.abspath(self.pathVar.get())
652
+ if os.path.exists(path):
653
+ self._goDir(path)
654
+
655
+ else:
656
+ self.showInfo("Path '%s' does not exists. " % path)
657
+ self.pathEntry.focus()
658
+
659
+ def onClose(self):
660
+ """ This onClose is replaced at init time in the FileBrowserWindow with its own callback"""
661
+ pass
662
+
663
+ def _close(self, e=None):
664
+ """ This _close is bound to the close button"""
665
+ self.onClose()
666
+
667
+ def _select(self, e=None):
668
+
669
+ self._lastSelected = self.getSelected()
670
+
671
+ if self._lastSelected is not None:
672
+ if self.onSelect:
673
+ self.onSelect(self._lastSelected)
674
+ else:
675
+ self.onClose()
676
+ else:
677
+ self.showInfo('Select a valid file/folder')
678
+
679
+ def getEntryValue(self):
680
+ return self.entryVar.get()
681
+
682
+ def getCurrentDir(self):
683
+ return self.treeProvider.getDir()
684
+
685
+
686
+ class ShortCut:
687
+ """ Shortcuts to paths to be displayed in the file browser"""
688
+
689
+ @staticmethod
690
+ def factory(path, name, icon=None, toolTip=""):
691
+ """ Factory method to create shortcuts"""
692
+ return ShortCut(path, name, icon, toolTip)
693
+
694
+ def __init__(self, path, name, icon=None, toolTip=""):
695
+ self.path = path
696
+ self.name = name
697
+ self.icon = icon
698
+ self.toolTip = toolTip
699
+
700
+
701
+ class BrowserWindow(gui.Window):
702
+ """ Windows to hold a browser frame inside. """
703
+
704
+ def __init__(self, title, master=None, **kwargs):
705
+ if 'minsize' not in kwargs:
706
+ kwargs['minsize'] = (800, 400)
707
+ gui.Window.__init__(self, title, master, **kwargs)
708
+
709
+ def setBrowser(self, browser, row=0, column=0):
710
+ self.browser = browser
711
+ browser.grid(row=row, column=column, sticky='news')
712
+ self.itemConfig = browser.tree.itemConfig
713
+
714
+
715
+ STANDARD_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg']
716
+
717
+
718
+ def isStandardImage(filename):
719
+ """ Check if a filename have an standard image extension. """
720
+ fnLower = filename.lower()
721
+ return any(fnLower.endswith(ext) for ext in STANDARD_IMAGE_EXTENSIONS)
722
+
723
+
724
+ class FileBrowserWindow(BrowserWindow):
725
+ """ Windows to hold a file browser frame inside. """
726
+
727
+ lastValue=None
728
+ def __init__(self, title, master=None, path=None,
729
+ onSelect=None, shortCuts=None, **kwargs):
730
+ BrowserWindow.__init__(self, title, master, **kwargs)
731
+ self.registerHandlers()
732
+ browser = FileBrowser(self.root, path,
733
+ showInfo=lambda msg: self.showInfo(msg, "Info"),
734
+ shortCuts=shortCuts,
735
+ **kwargs)
736
+ if onSelect:
737
+ def selected(obj):
738
+ self.close()
739
+ onSelect(obj)
740
+
741
+ browser.onSelect = selected
742
+ browser.onClose = self.close
743
+ self.setBrowser(browser)
744
+
745
+ def getEntryValue(self):
746
+ return self.browser.getEntryValue()
747
+
748
+ def getLastSelection(self):
749
+ return self.browser._lastSelected.getPath()
750
+ def getCurrentDir(self):
751
+ return self.browser.getCurrentDir()
752
+
753
+ def registerHandlers(self):
754
+ register = FileTreeProvider.registerFileHandler # shortcut
755
+
756
+ register(TextFileHandler(pwutils.Icon.TXT_FILE),
757
+ '.txt', '.log', '.out', '.err', '.stdout', '.stderr', '.emx',
758
+ '.json', '.xml', '.pam')
759
+ register(TextFileHandler(pwutils.Icon.PYTHON_FILE), '.py')
760
+ register(SqlFileHandler(), '.sqlite', '.db')