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
pyworkflow/gui/text.py ADDED
@@ -0,0 +1,771 @@
1
+ # **************************************************************************
2
+ # *
3
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
4
+ # *
5
+ # * [1] SciLifeLab, Stockholm University
6
+ # *
7
+ # * This program is free software: you can redistribute it and/or modify
8
+ # * it under the terms of the GNU General Public License as published by
9
+ # * the Free Software Foundation, either version 3 of the License, or
10
+ # * (at your option) any later version.
11
+ # *
12
+ # * This program is distributed in the hope that it will be useful,
13
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # * GNU General Public License for more details.
16
+ # *
17
+ # * You should have received a copy of the GNU General Public License
18
+ # * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ # *
20
+ # * All comments concerning this program package may be sent to the
21
+ # * e-mail address 'scipion@cnb.csic.es'
22
+ # *
23
+ # **************************************************************************
24
+ """
25
+ Text based widgets.
26
+ """
27
+
28
+
29
+ import os
30
+ import sys
31
+ import time
32
+ import webbrowser
33
+ import subprocess
34
+ import tkinter.ttk as ttk
35
+ import tkinter as tk
36
+ import tkinter.messagebox as tkMessageBox
37
+
38
+ import pyworkflow as pw
39
+ from pyworkflow import ASCII_COLOR_2_TKINTER
40
+ from pyworkflow.utils import (HYPER_BOLD, HYPER_ITALIC, HYPER_LINK1, HYPER_LINK2,
41
+ parseHyperText, renderLine, renderTextFile,
42
+ which, envVarOn, expandPattern)
43
+ from pyworkflow.utils.properties import Message, Color, Icon
44
+ from . import gui
45
+ from .widgets import Scrollable, IconButton
46
+ from .tooltip import ToolTip
47
+
48
+
49
+ # Define a function to open files cleanly in a system-dependent way
50
+ if sys.platform.startswith('darwin'): # macs use the "open" command
51
+ def _open_cmd(path, tkParent=None):
52
+ subprocess.Popen(['open', path])
53
+ elif os.name == 'nt': # there is a function os.startfile for windows
54
+ def _open_cmd(path, tkParent=None):
55
+ os.startfile(path)
56
+ elif os.name == 'posix': # linux systems and so on
57
+ def find_prog(*args):
58
+ """Return the first argument that is a program in PATH"""
59
+ for command in args:
60
+ if which(command):
61
+ return command
62
+ return None
63
+
64
+ x_open = find_prog('xdg-open', 'gnome-open', 'kde-open', 'gvfs-open')
65
+ editor = pw.Config.SCIPION_TEXT_EDITOR
66
+ if not editor:
67
+ editor = find_prog('pluma', 'gedit', 'kwrite', 'geany', 'kate',
68
+ 'emacs', 'nedit', 'mousepad', 'code')
69
+
70
+ def _open_cmd(path, tkParent=None):
71
+ # If it is an url, open with browser.
72
+ if path.startswith('http://') or path.startswith('https://') or path.endswith('.html'):
73
+ try:
74
+ webbrowser.open_new_tab(path)
75
+ return
76
+ except:
77
+ pass
78
+ # OK, it is a file. Check if it does exist
79
+ # and notify if it does not
80
+ if not os.path.isfile(path):
81
+ try:
82
+ # if tkRoot is null the error message may be behind
83
+ # other windows
84
+ tkMessageBox.showerror("File Error", # bar title
85
+ "File not found\n(%s)" % path, # message
86
+ parent=tkParent)
87
+ return
88
+ except:
89
+ return
90
+
91
+ if x_open: # standard way to open
92
+ proc = subprocess.Popen([x_open, path])
93
+ time.sleep(1)
94
+ if proc.poll() in [None, 0]:
95
+ return # yay! that's the way to do it!
96
+ if editor: # last card: try to open it in an editor
97
+ proc = subprocess.Popen([editor, path])
98
+ time.sleep(1)
99
+ if proc.poll() in [None, 0]:
100
+ return # hope we found your fav editor :)
101
+ print('WARNING: Cannot open %s' % path) # nothing worked! :(
102
+ else:
103
+ def _open_cmd(path, tkParent=None):
104
+ try:
105
+ tkMessageBox.showerror("Unknown System", # bar title
106
+ 'Unknown system, so cannot open %s' % path, # message
107
+ parent=tkParent)
108
+ return
109
+ except:
110
+ pass
111
+
112
+
113
+ class HyperlinkManager:
114
+ """ Tkinter Text Widget Hyperlink Manager, taken from:
115
+ http://effbot.org/zone/tkinter-text-hyperlink.htm """
116
+ def __init__(self, text):
117
+ self.text = text
118
+ self.text.tag_config("hyper", foreground=pw.Config.SCIPION_MAIN_COLOR, underline=1)
119
+ self.text.tag_bind("hyper", "<Enter>", self._enter)
120
+ self.text.tag_bind("hyper", "<Leave>", self._leave)
121
+ self.text.tag_bind("hyper", "<Button-1>", self._click)
122
+ self.reset()
123
+
124
+ def reset(self):
125
+ self.links = {}
126
+
127
+ def add(self, action):
128
+ # add an action to the manager. returns tags to use in
129
+ # associated text widget
130
+ tag = "hyper-%d" % len(self.links)
131
+ self.links[tag] = action
132
+ return "hyper", tag
133
+
134
+ def _enter(self, event):
135
+ self.text.config(cursor="hand2")
136
+
137
+ def _leave(self, event):
138
+ self.text.config(cursor="")
139
+
140
+ def _click(self, event):
141
+ for tag in self.text.tag_names(tk.CURRENT):
142
+ if tag[:6] == "hyper-":
143
+ self.links[tag]()
144
+ return
145
+
146
+
147
+ class Text(tk.Text, Scrollable):
148
+ """ Base Text widget with some functionality
149
+ that will be used by children classes.
150
+ """
151
+ def __init__(self, master, **opts):
152
+ if 'handlers' in opts:
153
+ self.handlers = opts.pop('handlers')
154
+ else:
155
+ self.handlers = {}
156
+ opts['font'] = gui.fontNormal
157
+ defaults = self.getDefaults()
158
+ defaults.update(opts)
159
+ Scrollable.__init__(self, master, tk.Text, wrap=tk.WORD, **opts)
160
+ self._createWidgets(master, **defaults)
161
+ self.configureTags()
162
+
163
+ def _createWidgets(self, master, **opts):
164
+ """This is an internal function to create the Text, the Scrollbar and the Frame"""
165
+
166
+ # create a popup menu
167
+ self.menu = tk.Menu(master, tearoff=0, postcommand=self.updateMenu)
168
+ self.menu.add_command(label="Copy to clipboard", command=self.copyToClipboard)
169
+ self.menu.add_command(label="Open path", command=self.openFile)
170
+ # Associate with right click
171
+ self.bind("<Button-1>", self.onClick)
172
+ self.bind("<Button-3>", self.onRightClick)
173
+
174
+ def getDefaults(self):
175
+ """This should be implemented in subclasses to provide defaults"""
176
+ return {}
177
+
178
+ def configureTags(self):
179
+ """This should be implemented to create specific tags"""
180
+ pass
181
+
182
+ def addLine(self, line):
183
+ """Should be implemented to add a line """
184
+ self.insert(tk.END, line + '\n')
185
+
186
+ def addNewline(self):
187
+ self.insert(tk.END, '\n')
188
+
189
+ def goBegin(self):
190
+ self.see(0.0)
191
+
192
+ def goEnd(self):
193
+ self.see(tk.END)
194
+
195
+ def isAtEnd(self):
196
+ return self.scrollbar.get() == 1.0
197
+
198
+ def clear(self):
199
+ self.delete(0.0, tk.END)
200
+
201
+ def getText(self):
202
+
203
+ textWithNewLine = self.get(0.0, tk.END)
204
+
205
+ # Remove the last new line
206
+ return textWithNewLine.rstrip('\n')
207
+
208
+ def setText(self, text):
209
+ """ Replace the current text with new one. """
210
+ self.clear()
211
+ self.addText(text)
212
+
213
+ def addText(self, text):
214
+ """ Add some text to the current state. """
215
+ if isinstance(text, list):
216
+ for line in text:
217
+ self.addLine(line)
218
+ else:
219
+ for line in text.splitlines():
220
+ self.addLine(line)
221
+
222
+ def onClick(self, e=None):
223
+ self.selection = None
224
+ self.selection_clear()
225
+ self.menu.unpost()
226
+
227
+ def onRightClick(self, e):
228
+ try:
229
+ self.selection = self.selection_get().strip()
230
+ self.menu.post(e.x_root, e.y_root)
231
+ except tk.TclError as e:
232
+ pass
233
+
234
+ def copyToClipboard(self, e=None):
235
+ self.clipboard_clear()
236
+ self.clipboard_append(self.selection)
237
+
238
+ def openFile(self):
239
+ # What happens when you right-click and select "Open path"
240
+ self.openPath(self.selection)
241
+
242
+ def openPath(self, path):
243
+ """Try to open the selected path"""
244
+ path = expandPattern(path)
245
+
246
+ # If the path is a dir, open it with scipion browser dir <path>
247
+ if os.path.isdir(path):
248
+ dpath = (path if os.path.isabs(path)
249
+ else os.path.join(os.getcwd(), path))
250
+ subprocess.Popen([pw.PYTHON, pw.getViewerScript(), dpath])
251
+ return
252
+
253
+ # If it is a file, interpret it correctly and open it with DataView
254
+ dirname = os.path.dirname(path)
255
+ fname = os.path.basename(path)
256
+ if '@' in fname:
257
+ path = os.path.join(dirname, fname.split('@', 1)[-1])
258
+ else:
259
+ path = os.path.join(dirname, fname)
260
+
261
+ if os.path.exists(path) or path.startswith("http"):
262
+ from pwem import emlib
263
+ fn = emlib.FileName(path)
264
+ if fn is not None and (fn.isImage() or fn.isMetaData()):
265
+ # fn is None if xmippLib is the xmippLib ghost library
266
+ from pwem.viewers import DataView
267
+ DataView(path).show()
268
+ else:
269
+ _open_cmd(path)
270
+ else:
271
+ # This is probably one special reference, like sci-open:... that
272
+ # can be interpreted with our handlers.
273
+ tag = path.split(':', 1)[0] if ':' in path else None
274
+ if tag in self.handlers:
275
+ self.handlers[tag](path.split(':', 1)[-1])
276
+ else:
277
+ print("Can't find %s" % path)
278
+
279
+ def updateMenu(self, e=None):
280
+ state = 'normal'
281
+ # if not xmippExists(self.selection):
282
+ # state = 'disabled'#self.menu.entryconfig(1, background="green")
283
+ self.menu.entryconfig(1, state=state)
284
+
285
+ def setReadOnly(self, value):
286
+ state = tk.NORMAL
287
+ if value:
288
+ state = tk.DISABLED
289
+ self.config(state=state)
290
+
291
+ def highlight(self, pattern, tag, start="1.0", end="end", regexp=False):
292
+ """ Apply the given tag to all text that matches the given pattern
293
+
294
+ If 'regexp' is set to True, pattern will be treated as a regular expression
295
+ Taken from:
296
+ http://stackoverflow.com/questions/3781670/tkinter-text-highlighting-in-python
297
+ """
298
+ start = self.index(start)
299
+ end = self.index(end)
300
+ self.mark_set("matchStart", start)
301
+ self.mark_set("matchEnd", start)
302
+ self.mark_set("searchLimit", end)
303
+
304
+ count = tk.IntVar()
305
+ while True:
306
+ index = self.search(pattern, "matchEnd", "searchLimit",
307
+ count=count, regexp=regexp)
308
+ if index == "":
309
+ break
310
+ self.mark_set("matchStart", index)
311
+ self.mark_set("matchEnd", "%s+%sc" % (index, count.get()))
312
+ self.tag_add(tag, "matchStart", "matchEnd")
313
+
314
+
315
+ def configureColorTags(text):
316
+ """ Create tags in text (of type tk.Text) for all the supported colors. """
317
+ try:
318
+ for color in ASCII_COLOR_2_TKINTER.values():
319
+ text.tag_config(color, foreground=color)
320
+ return True
321
+ except Exception as e:
322
+ print("Colors still not available (%s)" % e)
323
+ return False
324
+
325
+
326
+ class TaggedText(Text):
327
+ """
328
+ Implement a Text that will recognize some basic tags
329
+ *some_text* will display some_text in bold
330
+ _some_text_ will display some_text in italic
331
+ some_link or [[some_link][some_label]] will display some_link
332
+ as hyperlink or some_label as hyperlink to some_link
333
+ also colors are recognized if set option colors=True
334
+ """
335
+ def __init__(self, master, colors=True, **opts):
336
+ self.colors = colors
337
+ Text.__init__(self, master, **opts)
338
+ self.hm = HyperlinkManager(self)
339
+
340
+ def getDefaults(self):
341
+ return {'bg': pw.Config.SCIPION_BG_COLOR, 'bd': 0}
342
+ # It used to have also 'font': gui.fontNormal but that stops
343
+ # this file from running. Apparently there is no fontNormal in gui.
344
+
345
+ def configureTags(self):
346
+ self.tag_config('normal', justify=tk.LEFT, font=gui.fontNormal)
347
+ self.tag_config(HYPER_BOLD, justify=tk.LEFT, font=gui.fontBold)
348
+ self.tag_config(HYPER_ITALIC, justify=tk.LEFT, font=gui.fontItalic)
349
+ if self.colors:
350
+ self.colors = configureColorTags(self)
351
+ # Color can be unavailable, so disable use of colors
352
+
353
+ @staticmethod
354
+ def openLink(link):
355
+ webbrowser.open_new_tab(link) # Open in the same browser, new tab
356
+
357
+ @staticmethod
358
+ def mailTo(email):
359
+ webbrowser.open("mailto:" + email)
360
+
361
+ def matchHyperText(self, match, tag):
362
+ """ Process when a match a found and store indexes inside string."""
363
+ self.insert(tk.END, self.line[self.lastIndex:match.start()])
364
+ g1 = match.group(tag)
365
+
366
+ if tag == HYPER_BOLD or tag == HYPER_ITALIC:
367
+ self.insert(tk.END, ' ' + g1, tag)
368
+ elif tag == HYPER_LINK1:
369
+ self.insert(tk.END, g1, self.hm.add(lambda: self.openLink(g1)))
370
+ elif tag == HYPER_LINK2:
371
+ label = match.group('link2_label')
372
+ if g1.startswith('http'):
373
+ self.insert(tk.END, label, self.hm.add(lambda: self.openLink(g1)))
374
+ elif g1.startswith('mailto:'):
375
+ self.insert(tk.END, label, self.hm.add(lambda: self.mailTo(g1)))
376
+ else:
377
+ self.insert(tk.END, label, self.hm.add(lambda: self.openPath(g1)))
378
+ self.lastIndex = match.end()
379
+
380
+ return g1
381
+
382
+ def addLine(self, line):
383
+ self.line = line
384
+ self.lastIndex = 0
385
+ if line is not None:
386
+ parseHyperText(line, self.matchHyperText)
387
+ Text.addLine(self, line[self.lastIndex:])
388
+
389
+
390
+ class OutputText(Text):
391
+ """
392
+ Implement a Text that will show file content
393
+ and handle console metacharacter for colored output
394
+ """
395
+ def __init__(self, master, filename, colors=True, t_refresh=0, maxSize=400, **opts):
396
+ """ colors flag indicate if try to parse color meta-characters
397
+ t_refresh is the refresh time in seconds, 0 means no refresh
398
+ """
399
+ self.filename = filename
400
+ self.colors = colors
401
+ self.t_refresh = t_refresh
402
+ self.maxSize = maxSize
403
+
404
+ self.refreshAlarm = None # Identifier returned by after()
405
+ self.lineNo = 0
406
+ self.offset = 0
407
+ self.lastLine = ''
408
+ Text.__init__(self, master, **opts)
409
+ self.hm = HyperlinkManager(self)
410
+ self.doRefresh()
411
+
412
+ def getDefaults(self):
413
+ return {'bg': "black", 'fg': 'white', 'bd': 0,
414
+ 'height': 30, 'width': 100}
415
+ # It used to have also 'font': gui.fontNormal but that stops this
416
+ # file from running. Apparently there is no fontNormal in gui.
417
+
418
+ def configureTags(self):
419
+ if self.colors:
420
+ configureColorTags(self)
421
+
422
+ def _removeLastLine(self):
423
+ line = int(self.index(tk.END).split('.')[0])
424
+ if line > 0:
425
+ line -= 1
426
+ self.delete('%d.0' % line, tk.END)
427
+
428
+ def addLine(self, line):
429
+ renderLine(line, self._addChunk, self.lineNo)
430
+
431
+ def _addChunk(self, txt, fmt=None):
432
+ """
433
+ Add text txt to the widget, with format fmt.
434
+ fmt can be a color (like 'red') or a link that looks like 'link:url'.
435
+ """
436
+ if self.colors and fmt is not None:
437
+ if fmt.startswith('link:'):
438
+ fname = fmt.split(':', 1)[-1]
439
+ self.insert(tk.END, txt, self.hm.add(lambda: openTextFileEditor(fname)))
440
+ else:
441
+ self.insert(tk.END, txt, fmt)
442
+ else:
443
+ self.insert(tk.END, txt)
444
+
445
+ def _notifyLine(self, line):
446
+ if '\r' in self.lastLine and '\r' in line:
447
+ self._removeLastLine()
448
+ self.addNewline()
449
+
450
+ self.lastLine = line
451
+
452
+ def readFile(self, clear=False):
453
+ self.setReadOnly(False)
454
+
455
+ if clear:
456
+ self.offset = 0
457
+ self.lineNo = 0
458
+ self.clear()
459
+
460
+ if os.path.exists(self.filename):
461
+ self.offset, self.lineNo = renderTextFile(self.filename,
462
+ self._addChunk,
463
+ offset=self.offset,
464
+ lineNo=self.lineNo,
465
+ maxSize=self.maxSize,
466
+ notifyLine=self._notifyLine,
467
+ errors='replace')
468
+
469
+ # I'm cancelling this message. If file does not exist ... text is empty.
470
+ # else:
471
+ # self.insert(tk.END, "File '%s' doesn't exist" % self.filename)
472
+
473
+ self.setReadOnly(True)
474
+ # self.goEnd()
475
+
476
+ def doRefresh(self):
477
+ # First stop pending refreshes
478
+ if self.refreshAlarm:
479
+ self.after_cancel(self.refreshAlarm)
480
+ self.refreshAlarm = None
481
+
482
+ self.readFile()
483
+
484
+ if self.t_refresh > 0:
485
+ self.refreshAlarm = self.after(self.t_refresh*1000, self.doRefresh)
486
+
487
+
488
+ class TextFileViewer(tk.Frame):
489
+ """ Implementation of a simple text file viewer """
490
+
491
+ # Not used? --> LabelBgColor = "white"
492
+
493
+ def __init__(self, master, fileList=[],
494
+ allowSearch=True, allowRefresh=True, allowOpen=False,
495
+ font=None, maxSize=400, width=100, height=30):
496
+ tk.Frame.__init__(self, master)
497
+ self.searchList = None
498
+ self.lastSearch = None
499
+ self.refreshAlarm = None
500
+ self._lastTabIndex = None
501
+ self.fileList = [] # Files being visualized
502
+ self.taList = [] # Text areas (OutputText, a scrollable TkText)
503
+ self.fontDict = {}
504
+ self._allowSearch = allowSearch
505
+ self._allowRefresh = allowRefresh
506
+ self._allowOpen = allowOpen
507
+ self._font = font # allow a font to be passed as argument to be used
508
+ self.maxSize = maxSize
509
+ self.width = width
510
+ self.height = height
511
+
512
+ self.createWidgets(fileList)
513
+ self.master = master
514
+ self.addBinding()
515
+
516
+ def addFile(self, filename):
517
+ self.fileList.append(filename)
518
+ self._addFileTab(filename)
519
+
520
+ def clear(self):
521
+ """ Remove all added files. """
522
+ self.fileList = []
523
+ for _ in self.taList:
524
+ self.notebook.forget(0)
525
+ self.taList = []
526
+ self._lastTabIndex = None
527
+
528
+ def _addFileTab(self, filename):
529
+ tab = tk.Frame(self.notebook)
530
+ tab.rowconfigure(0, weight=1)
531
+ tab.columnconfigure(0, weight=1)
532
+ kwargs = {'bg': 'black',
533
+ 'fg': 'white'}
534
+
535
+ if self._font is not None:
536
+ kwargs['font'] = self._font
537
+
538
+ t = OutputText(tab, filename, width=self.width, height=self.height,
539
+ maxSize=self.maxSize, **kwargs)
540
+ t.frame.grid(column=0, row=0, padx=5, pady=5, sticky='nsew')
541
+ self.taList.append(t)
542
+ tabText = " %s " % os.path.basename(filename)
543
+ self.notebook.add(tab, text=tabText)
544
+
545
+ def createWidgets(self, fileList):
546
+ # registerCommonFonts()
547
+ self.columnconfigure(0, weight=1)
548
+ self.rowconfigure(1, weight=1)
549
+
550
+ # Create toolbar frame
551
+ toolbarFrame = tk.Frame(self)
552
+ toolbarFrame.grid(column=0, row=0, padx=5, sticky='new')
553
+ gui.configureWeigths(toolbarFrame)
554
+ # Add the search box
555
+ right = tk.Frame(toolbarFrame)
556
+ right.grid(column=1, row=0, sticky='ne')
557
+ self.searchVar = tk.StringVar()
558
+ if self._allowSearch:
559
+ tk.Label(right, text='Search:').grid(row=0, column=3, padx=5)
560
+ self.searchEntry = tk.Entry(right, textvariable=self.searchVar,
561
+ font=self._font)
562
+ self.searchEntry.grid(row=0, column=4, sticky='ew', padx=5)
563
+
564
+ # self.searchEntry.bind('<Return>', self.findText)
565
+ # self.searchEntry.bind('<KP_Enter>', self.findText)
566
+ # # btn = IconButton(right, "Search", Icon.ACTION_SEARCH,
567
+ # tooltip=Message.TOOLTIP_SEARCH,
568
+ # command=self.findText, bg=None)
569
+ # btn.grid(row=0, column=5, padx=(0, 5))
570
+
571
+ btn = IconButton(right, "Next", Icon.ACTION_FIND_NEXT,
572
+ tooltip=Message.TOOLTIP_SEARCH_NEXT,
573
+ command=self.findText, bg=None)
574
+ btn.grid(row=0, column=5, padx=(0, 5))
575
+
576
+ btn = IconButton(right, "Previous", Icon.ACTION_FIND_PREVIOUS,
577
+ tooltip=Message.TOOLTIP_SEARCH_PREVIOUS,
578
+ command=self.findPrevText, bg=None)
579
+ btn.grid(row=0, column=6, padx=(0, 5))
580
+
581
+ if self._allowRefresh:
582
+ btn = IconButton(right, "Refresh", Icon.ACTION_REFRESH,
583
+ tooltip=Message.TOOLTIP_REFRESH,
584
+ command=self._onRefresh, bg=None)
585
+ btn.grid(row=0, column=7, padx=(0, 5), pady=2)
586
+ if self._allowOpen:
587
+ btn = IconButton(right, "Open external", Icon.ACTION_REFERENCES,
588
+ tooltip=Message.TOOLTIP_EXTERNAL,
589
+ command=self._openExternal, bg=None)
590
+ btn.grid(row=0, column=8, padx=(0, 5), pady=2)
591
+
592
+ # Create tabs frame
593
+ tabsFrame = tk.Frame(self)
594
+ tabsFrame.grid(column=0, row=1, padx=5, pady=(0, 5), sticky="nsew")
595
+ tabsFrame.columnconfigure(0, weight=1)
596
+ tabsFrame.rowconfigure(0, weight=1)
597
+ self.notebook = ttk.Notebook(tabsFrame)
598
+ self.notebook.rowconfigure(0, weight=1)
599
+ self.notebook.columnconfigure(0, weight=1)
600
+ for f in fileList:
601
+ self._addFileTab(f)
602
+ self.notebook.grid(column=0, row=0, sticky='nsew', padx=5, pady=5)
603
+ self.notebook.bind('<<NotebookTabChanged>>', self._tabChanged)
604
+
605
+ def _tabChanged(self, e=None):
606
+ self._lastTabIndex = self.notebook.select()
607
+ # reset the search
608
+ self.lastSearch = None
609
+ # Setting the focus, captures it when selecting protocols and
610
+ # therefore "deleting" using keys or other future shortcut for the canvas
611
+ # will not work.
612
+ # self.searchEntry.focus_set()
613
+
614
+ def addBinding(self):
615
+
616
+ shortcutDefinitions = [(lambda e: self.findText(), "Trigger the search", ['<Return>']),
617
+ (lambda e: self.findText(), "Trigger the search", ['<KP_Enter>']),
618
+ (lambda e: self.findText(matchCase=True), "Trigger a case sensitive search", ['<Shift-Return>']),
619
+ (lambda e: self.findText(), "Move to the next highlighted item", ["<Down>", '<F3>']),
620
+ (lambda e: self.findText(-1), "Move to the previous highlighted item", ["<Up>", '<Shift-F3>']),
621
+ (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE + 2), "Increase the font size",["<Control-KP_Add>"]),
622
+ (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE), "Increase the font size",["<Control-KP_Subtract>"]),
623
+ ]
624
+ tooltip = "Shortcuts:"
625
+
626
+ for callback, help, keys in shortcutDefinitions:
627
+ tooltip += "\n" + help + ": "
628
+ for key in keys:
629
+ self.searchEntry.bind(key, callback)
630
+ tooltip += key
631
+
632
+ # Add a tooltip
633
+ ToolTip(self.searchEntry, tooltip, 800)
634
+
635
+ def getIndex(self):
636
+ """ Return the index of the selected tab. """
637
+ selected = self.notebook.select()
638
+ if selected:
639
+ return self.notebook.index(selected)
640
+ return -1
641
+
642
+ def setIndex(self, index):
643
+ """ Select the tab with the given index. """
644
+ if index != -1:
645
+ self.notebook.select(self.notebook.tabs()[index])
646
+
647
+ def selectedText(self):
648
+ index = self.getIndex()
649
+ if index != -1:
650
+ return self.taList[index]
651
+ return None
652
+
653
+ def changeFont(self, event=""):
654
+ for font in self.fontDict.values():
655
+ gui.changeFontSize(font, event)
656
+
657
+ def refreshAll(self, clear=False, goEnd=False):
658
+ """ Refresh all output textareas. """
659
+ for ta in self.taList:
660
+ ta.readFile(clear)
661
+ if goEnd:
662
+ ta.goEnd()
663
+ if self._lastTabIndex is not None:
664
+ self.notebook.select(self._lastTabIndex)
665
+
666
+ def _onRefresh(self, e=None):
667
+ """ Action triggered when the 'Refresh' icon is clicked. """
668
+ self.refreshAll(clear=False, goEnd=True)
669
+
670
+ def refreshOutput(self, e=None):
671
+ if self.refreshAlarm:
672
+ self.after_cancel(self.refreshAlarm)
673
+ self.refreshAlarm = None
674
+ text = self.selectedText()
675
+ if text:
676
+ text.readFile()
677
+
678
+ def changePosition(self, index):
679
+ self.selectedText().see(index)
680
+
681
+ def findPrevText(self):
682
+ self.findText(-1)
683
+
684
+ def modifyFontSize(self, newSize):
685
+ text = self.selectedText()
686
+ text['font']=(None, newSize)
687
+
688
+ def findText(self, direction=1, matchCase=0):
689
+ text = self.selectedText()
690
+ str = self.searchVar.get()
691
+ if text:
692
+ if str is None or str != self.lastSearch:
693
+ self.buildSearchList(text, str, matchCase=matchCase)
694
+ self.lastSearch = str
695
+
696
+ else:
697
+ self.nextSearchIndex(text, direction)
698
+ self.searchEntry.focus_set()
699
+
700
+ def buildSearchList(self, text, str, matchCase=0):
701
+ text.tag_remove('found', '1.0', tk.END)
702
+ list = []
703
+ if str:
704
+ idx = '1.0'
705
+ while True:
706
+ idx = text.search(str, idx, nocase=not matchCase, stopindex=tk.END)
707
+ if not idx:
708
+ break
709
+ lastidx = '%s+%dc' % (idx, len(str))
710
+ text.tag_add('found', idx, lastidx)
711
+ list.append((idx, lastidx))
712
+ idx = lastidx
713
+ text.tag_config('found', foreground='white', background='blue')
714
+ # Set class variables
715
+ self.searchList = list
716
+ self.currentIndex = -1
717
+ self.nextSearchIndex(text) # select first element
718
+
719
+ def nextSearchIndex(self, text, direction=1):
720
+ # use direction=-1 to go backward
721
+ text.tag_remove('found_current', '1.0', tk.END)
722
+ if len(self.searchList) == 0:
723
+ return
724
+ self.currentIndex = (self.currentIndex + direction) % len(self.searchList)
725
+ idx, lastidx = self.searchList[self.currentIndex]
726
+ text.tag_config('found_current', foreground='yellow', background='red')
727
+ text.tag_add('found_current', idx, lastidx)
728
+ text.see(idx)
729
+
730
+ def _openExternal(self):
731
+ """ Open a new window with an external viewer. """
732
+ if envVarOn('SCIPION_EXTERNAL_VIEWER'):
733
+ if not self.taList:
734
+ return
735
+ openTextFileEditor(self.taList[max(self.getIndex(), 0)].filename)
736
+ else:
737
+ showTextFileViewer("File viewer", self.fileList, self.windows)
738
+
739
+
740
+ def openTextFile(filename):
741
+ """ Open a text file with an external or default viewer. """
742
+ if envVarOn('SCIPION_EXTERNAL_VIEWER'):
743
+ openTextFileEditor(filename)
744
+ else:
745
+ showTextFileViewer("File viewer", [filename])
746
+
747
+
748
+ def openTextFileEditor(filename, tkParent=None):
749
+ try:
750
+ _open_cmd(filename, tkParent)
751
+ except:
752
+ showTextFileViewer("File viewer", [filename])
753
+
754
+
755
+ def showTextFileViewer(title, filelist, parent=None, main=False):
756
+ w = gui.Window(title, parent, minsize=(600, 400))
757
+ viewer = TextFileViewer(w.root, filelist, maxSize=-1, font=w.font)
758
+ viewer.grid(row=0, column=0, sticky='news')
759
+ gui.configureWeigths(w.root)
760
+ w.show()
761
+
762
+
763
+ if __name__ == '__main__':
764
+ root = tk.Tk()
765
+ root.withdraw()
766
+ root.title("View files")
767
+ l = TextFileViewer(root, fileList=sys.argv[1:])
768
+ l.pack(side=tk.TOP, fill=tk.BOTH)
769
+ gui.centerWindows(root)
770
+ root.deiconify()
771
+ root.mainloop()