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