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.
- pyworkflow/apps/__init__.py +29 -0
- pyworkflow/apps/pw_manager.py +37 -0
- pyworkflow/apps/pw_plot.py +51 -0
- pyworkflow/apps/pw_project.py +130 -0
- pyworkflow/apps/pw_protocol_list.py +143 -0
- pyworkflow/apps/pw_protocol_run.py +51 -0
- pyworkflow/apps/pw_run_tests.py +268 -0
- pyworkflow/apps/pw_schedule_run.py +322 -0
- pyworkflow/apps/pw_sleep.py +37 -0
- pyworkflow/apps/pw_sync_data.py +440 -0
- pyworkflow/apps/pw_viewer.py +78 -0
- pyworkflow/constants.py +1 -1
- pyworkflow/gui/__init__.py +36 -0
- pyworkflow/gui/browser.py +768 -0
- pyworkflow/gui/canvas.py +1190 -0
- pyworkflow/gui/dialog.py +981 -0
- pyworkflow/gui/form.py +2727 -0
- pyworkflow/gui/graph.py +247 -0
- pyworkflow/gui/graph_layout.py +271 -0
- pyworkflow/gui/gui.py +571 -0
- pyworkflow/gui/matplotlib_image.py +233 -0
- pyworkflow/gui/plotter.py +247 -0
- pyworkflow/gui/project/__init__.py +25 -0
- pyworkflow/gui/project/base.py +193 -0
- pyworkflow/gui/project/constants.py +139 -0
- pyworkflow/gui/project/labels.py +205 -0
- pyworkflow/gui/project/project.py +491 -0
- pyworkflow/gui/project/searchprotocol.py +240 -0
- pyworkflow/gui/project/searchrun.py +181 -0
- pyworkflow/gui/project/steps.py +171 -0
- pyworkflow/gui/project/utils.py +332 -0
- pyworkflow/gui/project/variables.py +179 -0
- pyworkflow/gui/project/viewdata.py +472 -0
- pyworkflow/gui/project/viewprojects.py +519 -0
- pyworkflow/gui/project/viewprotocols.py +2141 -0
- pyworkflow/gui/project/viewprotocols_extra.py +562 -0
- pyworkflow/gui/text.py +774 -0
- pyworkflow/gui/tooltip.py +185 -0
- pyworkflow/gui/tree.py +684 -0
- pyworkflow/gui/widgets.py +307 -0
- pyworkflow/mapper/__init__.py +26 -0
- pyworkflow/mapper/mapper.py +226 -0
- pyworkflow/mapper/sqlite.py +1583 -0
- pyworkflow/mapper/sqlite_db.py +145 -0
- pyworkflow/object.py +1 -0
- pyworkflow/plugin.py +4 -4
- pyworkflow/project/__init__.py +31 -0
- pyworkflow/project/config.py +454 -0
- pyworkflow/project/manager.py +180 -0
- pyworkflow/project/project.py +2095 -0
- pyworkflow/project/usage.py +165 -0
- pyworkflow/protocol/__init__.py +38 -0
- pyworkflow/protocol/bibtex.py +48 -0
- pyworkflow/protocol/constants.py +87 -0
- pyworkflow/protocol/executor.py +515 -0
- pyworkflow/protocol/hosts.py +318 -0
- pyworkflow/protocol/launch.py +277 -0
- pyworkflow/protocol/package.py +42 -0
- pyworkflow/protocol/params.py +781 -0
- pyworkflow/protocol/protocol.py +2712 -0
- pyworkflow/resources/protlabels.xcf +0 -0
- pyworkflow/resources/sprites.png +0 -0
- pyworkflow/resources/sprites.xcf +0 -0
- pyworkflow/template.py +1 -1
- pyworkflow/tests/__init__.py +29 -0
- pyworkflow/tests/test_utils.py +25 -0
- pyworkflow/tests/tests.py +342 -0
- pyworkflow/utils/__init__.py +38 -0
- pyworkflow/utils/dataset.py +414 -0
- pyworkflow/utils/echo.py +104 -0
- pyworkflow/utils/graph.py +169 -0
- pyworkflow/utils/log.py +293 -0
- pyworkflow/utils/path.py +528 -0
- pyworkflow/utils/process.py +154 -0
- pyworkflow/utils/profiler.py +92 -0
- pyworkflow/utils/progressbar.py +154 -0
- pyworkflow/utils/properties.py +618 -0
- pyworkflow/utils/reflection.py +129 -0
- pyworkflow/utils/utils.py +880 -0
- pyworkflow/utils/which.py +229 -0
- pyworkflow/webservices/__init__.py +8 -0
- pyworkflow/webservices/config.py +8 -0
- pyworkflow/webservices/notifier.py +152 -0
- pyworkflow/webservices/repository.py +59 -0
- pyworkflow/webservices/workflowhub.py +86 -0
- pyworkflowtests/tests/__init__.py +0 -0
- pyworkflowtests/tests/test_canvas.py +72 -0
- pyworkflowtests/tests/test_domain.py +45 -0
- pyworkflowtests/tests/test_logs.py +74 -0
- pyworkflowtests/tests/test_mappers.py +392 -0
- pyworkflowtests/tests/test_object.py +507 -0
- pyworkflowtests/tests/test_project.py +42 -0
- pyworkflowtests/tests/test_protocol_execution.py +146 -0
- pyworkflowtests/tests/test_protocol_export.py +78 -0
- pyworkflowtests/tests/test_protocol_output.py +158 -0
- pyworkflowtests/tests/test_streaming.py +47 -0
- pyworkflowtests/tests/test_utils.py +210 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/METADATA +2 -2
- scipion_pyworkflow-3.11.2.dist-info/RECORD +162 -0
- scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/WHEEL +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/entry_points.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.2.dist-info}/licenses/LICENSE.txt +0 -0
- {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')
|