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
pyworkflow/gui/text.py ADDED
@@ -0,0 +1,774 @@
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
+ self.bind("Control-c", self.copyToClipboard)
174
+
175
+ def getDefaults(self):
176
+ """This should be implemented in subclasses to provide defaults"""
177
+ return {}
178
+
179
+ def configureTags(self):
180
+ """This should be implemented to create specific tags"""
181
+ pass
182
+
183
+ def addLine(self, line):
184
+ """Should be implemented to add a line """
185
+ self.insert(tk.END, line + '\n')
186
+
187
+ def addNewline(self):
188
+ self.insert(tk.END, '\n')
189
+
190
+ def goBegin(self):
191
+ self.see(0.0)
192
+
193
+ def goEnd(self):
194
+ self.see(tk.END)
195
+
196
+ def isAtEnd(self):
197
+ return self.scrollbar.get() == 1.0
198
+
199
+ def clear(self):
200
+ self.delete(0.0, tk.END)
201
+
202
+ def getText(self):
203
+
204
+ textWithNewLine = self.get(0.0, tk.END)
205
+
206
+ # Remove the last new line
207
+ return textWithNewLine.rstrip('\n')
208
+
209
+ def setText(self, text):
210
+ """ Replace the current text with new one. """
211
+ self.clear()
212
+ self.addText(text)
213
+
214
+ def addText(self, text):
215
+ """ Add some text to the current state. """
216
+ if isinstance(text, list):
217
+ for line in text:
218
+ self.addLine(line)
219
+ else:
220
+ for line in text.splitlines():
221
+ self.addLine(line)
222
+
223
+ def onClick(self, e=None):
224
+ self.selection = None
225
+ self.selection_clear()
226
+ self.menu.unpost()
227
+
228
+ def onRightClick(self, e):
229
+ try:
230
+ self.selection = self.selection_get().strip()
231
+ self.menu.post(e.x_root, e.y_root)
232
+ except tk.TclError as e:
233
+ pass
234
+
235
+ def copyToClipboard(self, e=None):
236
+ self.clipboard_clear()
237
+ self.clipboard_append(self.selection)
238
+ return "break"
239
+
240
+ def openFile(self):
241
+ # What happens when you right-click and select "Open path"
242
+ self.openPath(self.selection)
243
+
244
+ def openPath(self, path):
245
+ """Try to open the selected path"""
246
+ path = expandPattern(path)
247
+
248
+ # If the path is a dir, open it with scipion browser dir <path>
249
+ if os.path.isdir(path):
250
+ dpath = (path if os.path.isabs(path)
251
+ else os.path.join(os.getcwd(), path))
252
+ subprocess.Popen([pw.PYTHON, pw.getViewerScript(), dpath])
253
+ return
254
+
255
+ # If it is a file, interpret it correctly and open it with DataView
256
+ dirname = os.path.dirname(path)
257
+ fname = os.path.basename(path)
258
+ if '@' in fname:
259
+ path = os.path.join(dirname, fname.split('@', 1)[-1])
260
+ else:
261
+ path = os.path.join(dirname, fname)
262
+
263
+ if os.path.exists(path) or path.startswith("http"):
264
+ from pwem import emlib
265
+ fn = emlib.FileName(path)
266
+ if fn is not None and (fn.isImage() or fn.isMetaData()):
267
+ # fn is None if xmippLib is the xmippLib ghost library
268
+ from pwem.viewers import DataView
269
+ DataView(path).show()
270
+ else:
271
+ _open_cmd(path)
272
+ else:
273
+ # This is probably one special reference, like sci-open:... that
274
+ # can be interpreted with our handlers.
275
+ tag = path.split(':', 1)[0] if ':' in path else None
276
+ if tag in self.handlers:
277
+ self.handlers[tag](path.split(':', 1)[-1])
278
+ else:
279
+ print("Can't find %s" % path)
280
+
281
+ def updateMenu(self, e=None):
282
+ state = 'normal'
283
+ # if not xmippExists(self.selection):
284
+ # state = 'disabled'#self.menu.entryconfig(1, background="green")
285
+ self.menu.entryconfig(1, state=state)
286
+
287
+ def setReadOnly(self, value):
288
+ state = tk.NORMAL
289
+ if value:
290
+ state = tk.DISABLED
291
+ self.config(state=state)
292
+
293
+ def highlight(self, pattern, tag, start="1.0", end="end", regexp=False):
294
+ """ Apply the given tag to all text that matches the given pattern
295
+
296
+ If 'regexp' is set to True, pattern will be treated as a regular expression
297
+ Taken from:
298
+ http://stackoverflow.com/questions/3781670/tkinter-text-highlighting-in-python
299
+ """
300
+ start = self.index(start)
301
+ end = self.index(end)
302
+ self.mark_set("matchStart", start)
303
+ self.mark_set("matchEnd", start)
304
+ self.mark_set("searchLimit", end)
305
+
306
+ count = tk.IntVar()
307
+ while True:
308
+ index = self.search(pattern, "matchEnd", "searchLimit",
309
+ count=count, regexp=regexp)
310
+ if index == "":
311
+ break
312
+ self.mark_set("matchStart", index)
313
+ self.mark_set("matchEnd", "%s+%sc" % (index, count.get()))
314
+ self.tag_add(tag, "matchStart", "matchEnd")
315
+
316
+
317
+ def configureColorTags(text):
318
+ """ Create tags in text (of type tk.Text) for all the supported colors. """
319
+ try:
320
+ for color in ASCII_COLOR_2_TKINTER.values():
321
+ text.tag_config(color, foreground=color)
322
+ return True
323
+ except Exception as e:
324
+ print("Colors still not available (%s)" % e)
325
+ return False
326
+
327
+
328
+ class TaggedText(Text):
329
+ """
330
+ Implement a Text that will recognize some basic tags
331
+ *some_text* will display some_text in bold
332
+ _some_text_ will display some_text in italic
333
+ some_link or [[some_link][some_label]] will display some_link
334
+ as hyperlink or some_label as hyperlink to some_link
335
+ also colors are recognized if set option colors=True
336
+ """
337
+ def __init__(self, master, colors=True, **opts):
338
+ self.colors = colors
339
+ Text.__init__(self, master, **opts)
340
+ self.hm = HyperlinkManager(self)
341
+
342
+ def getDefaults(self):
343
+ return {'bg': pw.Config.SCIPION_BG_COLOR, 'bd': 0}
344
+ # It used to have also 'font': gui.fontNormal but that stops
345
+ # this file from running. Apparently there is no fontNormal in gui.
346
+
347
+ def configureTags(self):
348
+ self.tag_config('normal', justify=tk.LEFT, font=gui.fontNormal)
349
+ self.tag_config(HYPER_BOLD, justify=tk.LEFT, font=gui.fontBold)
350
+ self.tag_config(HYPER_ITALIC, justify=tk.LEFT, font=gui.fontItalic)
351
+ if self.colors:
352
+ self.colors = configureColorTags(self)
353
+ # Color can be unavailable, so disable use of colors
354
+
355
+ @staticmethod
356
+ def openLink(link):
357
+ webbrowser.open_new_tab(link) # Open in the same browser, new tab
358
+
359
+ @staticmethod
360
+ def mailTo(email):
361
+ webbrowser.open("mailto:" + email)
362
+
363
+ def matchHyperText(self, match, tag):
364
+ """ Process when a match a found and store indexes inside string."""
365
+ self.insert(tk.END, self.line[self.lastIndex:match.start()])
366
+ g1 = match.group(tag)
367
+
368
+ if tag == HYPER_BOLD or tag == HYPER_ITALIC:
369
+ self.insert(tk.END, ' ' + g1, tag)
370
+ elif tag == HYPER_LINK1:
371
+ self.insert(tk.END, g1, self.hm.add(lambda: self.openLink(g1)))
372
+ elif tag == HYPER_LINK2:
373
+ label = match.group('link2_label')
374
+ if g1.startswith('http'):
375
+ self.insert(tk.END, label, self.hm.add(lambda: self.openLink(g1)))
376
+ elif g1.startswith('mailto:'):
377
+ self.insert(tk.END, label, self.hm.add(lambda: self.mailTo(g1)))
378
+ else:
379
+ self.insert(tk.END, label, self.hm.add(lambda: self.openPath(g1)))
380
+ self.lastIndex = match.end()
381
+
382
+ return g1
383
+
384
+ def addLine(self, line):
385
+ self.line = line
386
+ self.lastIndex = 0
387
+ if line is not None:
388
+ parseHyperText(line, self.matchHyperText)
389
+ Text.addLine(self, line[self.lastIndex:])
390
+
391
+
392
+ class OutputText(Text):
393
+ """
394
+ Implement a Text that will show file content
395
+ and handle console metacharacter for colored output
396
+ """
397
+ def __init__(self, master, filename, colors=True, t_refresh=0, maxSize=400, **opts):
398
+ """ colors flag indicate if try to parse color meta-characters
399
+ t_refresh is the refresh time in seconds, 0 means no refresh
400
+ """
401
+ self.filename = filename
402
+ self.colors = colors
403
+ self.t_refresh = t_refresh
404
+ self.maxSize = maxSize
405
+
406
+ self.refreshAlarm = None # Identifier returned by after()
407
+ self.lineNo = 0
408
+ self.offset = 0
409
+ self.lastLine = ''
410
+ Text.__init__(self, master, **opts)
411
+ self.hm = HyperlinkManager(self)
412
+ self.doRefresh()
413
+
414
+ def getDefaults(self):
415
+ return {'bg': "black", 'fg': 'white', 'bd': 0,
416
+ 'height': 30, 'width': 100}
417
+ # It used to have also 'font': gui.fontNormal but that stops this
418
+ # file from running. Apparently there is no fontNormal in gui.
419
+
420
+ def configureTags(self):
421
+ if self.colors:
422
+ configureColorTags(self)
423
+
424
+ def _removeLastLine(self):
425
+ line = int(self.index(tk.END).split('.')[0])
426
+ if line > 0:
427
+ line -= 1
428
+ self.delete('%d.0' % line, tk.END)
429
+
430
+ def addLine(self, line):
431
+ renderLine(line, self._addChunk, self.lineNo)
432
+
433
+ def _addChunk(self, txt, fmt=None):
434
+ """
435
+ Add text txt to the widget, with format fmt.
436
+ fmt can be a color (like 'red') or a link that looks like 'link:url'.
437
+ """
438
+ if self.colors and fmt is not None:
439
+ if fmt.startswith('link:'):
440
+ fname = fmt.split(':', 1)[-1]
441
+ self.insert(tk.END, txt, self.hm.add(lambda: openTextFileEditor(fname)))
442
+ else:
443
+ self.insert(tk.END, txt, fmt)
444
+ else:
445
+ self.insert(tk.END, txt)
446
+
447
+ def _notifyLine(self, line):
448
+ if '\r' in self.lastLine and '\r' in line:
449
+ self._removeLastLine()
450
+ self.addNewline()
451
+
452
+ self.lastLine = line
453
+
454
+ def readFile(self, clear=False):
455
+ self.setReadOnly(False)
456
+
457
+ if clear:
458
+ self.offset = 0
459
+ self.lineNo = 0
460
+ self.clear()
461
+
462
+ if os.path.exists(self.filename):
463
+ self.offset, self.lineNo = renderTextFile(self.filename,
464
+ self._addChunk,
465
+ offset=self.offset,
466
+ lineNo=self.lineNo,
467
+ maxSize=self.maxSize,
468
+ notifyLine=self._notifyLine,
469
+ errors='replace')
470
+
471
+ # I'm cancelling this message. If file does not exist ... text is empty.
472
+ # else:
473
+ # self.insert(tk.END, "File '%s' doesn't exist" % self.filename)
474
+
475
+ self.setReadOnly(True)
476
+ # self.goEnd()
477
+
478
+ def doRefresh(self):
479
+ # First stop pending refreshes
480
+ if self.refreshAlarm:
481
+ self.after_cancel(self.refreshAlarm)
482
+ self.refreshAlarm = None
483
+
484
+ self.readFile()
485
+
486
+ if self.t_refresh > 0:
487
+ self.refreshAlarm = self.after(self.t_refresh*1000, self.doRefresh)
488
+
489
+
490
+ class TextFileViewer(tk.Frame):
491
+ """ Implementation of a simple text file viewer """
492
+
493
+ # Not used? --> LabelBgColor = "white"
494
+
495
+ def __init__(self, master, fileList=[],
496
+ allowSearch=True, allowRefresh=True, allowOpen=False,
497
+ font=None, maxSize=400, width=100, height=30):
498
+ tk.Frame.__init__(self, master)
499
+ self.searchList = None
500
+ self.lastSearch = None
501
+ self.refreshAlarm = None
502
+ self._lastTabIndex = None
503
+ self.fileList = [] # Files being visualized
504
+ self.taList = [] # Text areas (OutputText, a scrollable TkText)
505
+ self.fontDict = {}
506
+ self._allowSearch = allowSearch
507
+ self._allowRefresh = allowRefresh
508
+ self._allowOpen = allowOpen
509
+ self._font = font # allow a font to be passed as argument to be used
510
+ self.maxSize = maxSize
511
+ self.width = width
512
+ self.height = height
513
+ self.searchEntry = None
514
+ self.createWidgets(fileList)
515
+ self.master = master
516
+ self.addBinding()
517
+
518
+ def addFile(self, filename):
519
+ self.fileList.append(filename)
520
+ self._addFileTab(filename)
521
+
522
+ def clear(self):
523
+ """ Remove all added files. """
524
+ self.fileList = []
525
+ for _ in self.taList:
526
+ self.notebook.forget(0)
527
+ self.taList = []
528
+ self._lastTabIndex = None
529
+
530
+ def _addFileTab(self, filename):
531
+ tab = tk.Frame(self.notebook)
532
+ tab.rowconfigure(0, weight=1)
533
+ tab.columnconfigure(0, weight=1)
534
+ kwargs = {'bg': 'black',
535
+ 'fg': 'white'}
536
+
537
+ if self._font is not None:
538
+ kwargs['font'] = self._font
539
+
540
+ t = OutputText(tab, filename, width=self.width, height=self.height,
541
+ maxSize=self.maxSize, **kwargs)
542
+ t.frame.grid(column=0, row=0, padx=5, pady=5, sticky='nsew')
543
+ self.taList.append(t)
544
+ tabText = " %s " % os.path.basename(filename)
545
+ self.notebook.add(tab, text=tabText)
546
+
547
+ def createWidgets(self, fileList):
548
+ # registerCommonFonts()
549
+ self.columnconfigure(0, weight=1)
550
+ self.rowconfigure(1, weight=1)
551
+
552
+ # Create toolbar frame
553
+ toolbarFrame = tk.Frame(self)
554
+ toolbarFrame.grid(column=0, row=0, padx=5, sticky='new')
555
+ gui.configureWeigths(toolbarFrame)
556
+ # Add the search box
557
+ right = tk.Frame(toolbarFrame)
558
+ right.grid(column=1, row=0, sticky='ne')
559
+ self.searchVar = tk.StringVar()
560
+ if self._allowSearch:
561
+ tk.Label(right, text='Search:').grid(row=0, column=3, padx=5)
562
+ self.searchEntry = tk.Entry(right, textvariable=self.searchVar,
563
+ font=self._font)
564
+ self.searchEntry.grid(row=0, column=4, sticky='ew', padx=5)
565
+
566
+ # self.searchEntry.bind('<Return>', self.findText)
567
+ # self.searchEntry.bind('<KP_Enter>', self.findText)
568
+ # # btn = IconButton(right, "Search", Icon.ACTION_SEARCH,
569
+ # tooltip=Message.TOOLTIP_SEARCH,
570
+ # command=self.findText, bg=None)
571
+ # btn.grid(row=0, column=5, padx=(0, 5))
572
+
573
+ btn = IconButton(right, "Next", Icon.ACTION_FIND_NEXT,
574
+ tooltip=Message.TOOLTIP_SEARCH_NEXT,
575
+ command=self.findText, bg=None)
576
+ btn.grid(row=0, column=5, padx=(0, 5))
577
+
578
+ btn = IconButton(right, "Previous", Icon.ACTION_FIND_PREVIOUS,
579
+ tooltip=Message.TOOLTIP_SEARCH_PREVIOUS,
580
+ command=self.findPrevText, bg=None)
581
+ btn.grid(row=0, column=6, padx=(0, 5))
582
+
583
+ if self._allowRefresh:
584
+ btn = IconButton(right, "Refresh", Icon.ACTION_REFRESH,
585
+ tooltip=Message.TOOLTIP_REFRESH,
586
+ command=self._onRefresh, bg=None)
587
+ btn.grid(row=0, column=7, padx=(0, 5), pady=2)
588
+ if self._allowOpen:
589
+ btn = IconButton(right, "Open external", Icon.ACTION_REFERENCES,
590
+ tooltip=Message.TOOLTIP_EXTERNAL,
591
+ command=self._openExternal, bg=None)
592
+ btn.grid(row=0, column=8, padx=(0, 5), pady=2)
593
+
594
+ # Create tabs frame
595
+ tabsFrame = tk.Frame(self)
596
+ tabsFrame.grid(column=0, row=1, padx=5, pady=(0, 5), sticky="nsew")
597
+ tabsFrame.columnconfigure(0, weight=1)
598
+ tabsFrame.rowconfigure(0, weight=1)
599
+ self.notebook = ttk.Notebook(tabsFrame)
600
+ self.notebook.rowconfigure(0, weight=1)
601
+ self.notebook.columnconfigure(0, weight=1)
602
+ for f in fileList:
603
+ self._addFileTab(f)
604
+ self.notebook.grid(column=0, row=0, sticky='nsew', padx=5, pady=5)
605
+ self.notebook.bind('<<NotebookTabChanged>>', self._tabChanged)
606
+
607
+ def _tabChanged(self, e=None):
608
+ self._lastTabIndex = self.notebook.select()
609
+ # reset the search
610
+ self.lastSearch = None
611
+ # Setting the focus, captures it when selecting protocols and
612
+ # therefore "deleting" using keys or other future shortcut for the canvas
613
+ # will not work.
614
+ # self.searchEntry.focus_set()
615
+
616
+ def addBinding(self):
617
+
618
+ shortcutDefinitions = [(lambda e: self.findText(), "Trigger the search", ['<Return>']),
619
+ (lambda e: self.findText(), "Trigger the search", ['<KP_Enter>']),
620
+ (lambda e: self.findText(matchCase=True), "Trigger a case sensitive search", ['<Shift-Return>']),
621
+ (lambda e: self.findText(), "Move to the next highlighted item", ["<Down>", '<F3>']),
622
+ (lambda e: self.findText(-1), "Move to the previous highlighted item", ["<Up>", '<Shift-F3>']),
623
+ (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE + 2), "Increase the font size",["<Control-KP_Add>"]),
624
+ (lambda e: self.modifyFontSize(pw.Config.SCIPION_FONT_SIZE), "Increase the font size",["<Control-KP_Subtract>"]),
625
+ ]
626
+ tooltip = "Shortcuts:"
627
+
628
+ for callback, help, keys in shortcutDefinitions:
629
+ tooltip += "\n" + help + ": "
630
+ for key in keys:
631
+ self.searchEntry.bind(key, callback)
632
+ tooltip += key
633
+
634
+ # Add a tooltip
635
+ ToolTip(self.searchEntry, tooltip, 800)
636
+
637
+ def getIndex(self):
638
+ """ Return the index of the selected tab. """
639
+ selected = self.notebook.select()
640
+ if selected:
641
+ return self.notebook.index(selected)
642
+ return -1
643
+
644
+ def setIndex(self, index):
645
+ """ Select the tab with the given index. """
646
+ if index != -1:
647
+ self.notebook.select(self.notebook.tabs()[index])
648
+
649
+ def selectedText(self):
650
+ index = self.getIndex()
651
+ if index != -1:
652
+ return self.taList[index]
653
+ return None
654
+
655
+ def changeFont(self, event=""):
656
+ for font in self.fontDict.values():
657
+ gui.changeFontSize(font, event)
658
+
659
+ def refreshAll(self, clear=False, goEnd=False):
660
+ """ Refresh all output textareas. """
661
+ for ta in self.taList:
662
+ ta.readFile(clear)
663
+ if goEnd:
664
+ ta.goEnd()
665
+ if self._lastTabIndex is not None:
666
+ self.notebook.select(self._lastTabIndex)
667
+
668
+ def _onRefresh(self, e=None):
669
+ """ Action triggered when the 'Refresh' icon is clicked. """
670
+ self.refreshAll(clear=False, goEnd=True)
671
+
672
+ def refreshOutput(self, e=None):
673
+ if self.refreshAlarm:
674
+ self.after_cancel(self.refreshAlarm)
675
+ self.refreshAlarm = None
676
+ text = self.selectedText()
677
+ if text:
678
+ text.readFile()
679
+
680
+ def changePosition(self, index):
681
+ self.selectedText().see(index)
682
+
683
+ def findPrevText(self):
684
+ self.findText(-1)
685
+
686
+ def modifyFontSize(self, newSize):
687
+ text = self.selectedText()
688
+ text['font']=(None, newSize)
689
+
690
+ def findText(self, direction=1, matchCase=0):
691
+ text = self.selectedText()
692
+ str = self.searchVar.get()
693
+ if text:
694
+ if str is None or str != self.lastSearch:
695
+ self.buildSearchList(text, str, matchCase=matchCase)
696
+ self.lastSearch = str
697
+
698
+ else:
699
+ self.nextSearchIndex(text, direction)
700
+ self.searchEntry.focus_set()
701
+
702
+ def buildSearchList(self, text, str, matchCase=0):
703
+ text.tag_remove('found', '1.0', tk.END)
704
+ list = []
705
+ if str:
706
+ idx = '1.0'
707
+ while True:
708
+ idx = text.search(str, idx, nocase=not matchCase, stopindex=tk.END)
709
+ if not idx:
710
+ break
711
+ lastidx = '%s+%dc' % (idx, len(str))
712
+ text.tag_add('found', idx, lastidx)
713
+ list.append((idx, lastidx))
714
+ idx = lastidx
715
+ text.tag_config('found', foreground='white', background='blue')
716
+ # Set class variables
717
+ self.searchList = list
718
+ self.currentIndex = -1
719
+ self.nextSearchIndex(text) # select first element
720
+
721
+ def nextSearchIndex(self, text, direction=1):
722
+ # use direction=-1 to go backward
723
+ text.tag_remove('found_current', '1.0', tk.END)
724
+ if len(self.searchList) == 0:
725
+ return
726
+ self.currentIndex = (self.currentIndex + direction) % len(self.searchList)
727
+ idx, lastidx = self.searchList[self.currentIndex]
728
+ text.tag_config('found_current', foreground='yellow', background='red')
729
+ text.tag_add('found_current', idx, lastidx)
730
+ text.see(idx)
731
+
732
+ def _openExternal(self):
733
+ """ Open a new window with an external viewer. """
734
+ if envVarOn('SCIPION_EXTERNAL_VIEWER'):
735
+ if not self.taList:
736
+ return
737
+ openTextFileEditor(self.taList[max(self.getIndex(), 0)].filename)
738
+ else:
739
+ showTextFileViewer("File viewer", self.fileList, self.windows)
740
+
741
+
742
+ def openTextFile(filename):
743
+ """ Open a text file with an external or default viewer. """
744
+ if envVarOn('SCIPION_EXTERNAL_VIEWER'):
745
+ openTextFileEditor(filename)
746
+ else:
747
+ showTextFileViewer("File viewer", [filename])
748
+
749
+
750
+ def openTextFileEditor(filename, tkParent=None):
751
+ try:
752
+ _open_cmd(filename, tkParent)
753
+ except:
754
+ showTextFileViewer("File viewer", [filename])
755
+
756
+
757
+ def showTextFileViewer(title, filelist, parent=None, main=False):
758
+ w = gui.Window(title, parent, minsize=(600, 400))
759
+ viewer = TextFileViewer(w.root, filelist, maxSize=-1, font=w.font)
760
+ w.initial_focus=viewer.searchEntry
761
+ viewer.grid(row=0, column=0, sticky='news')
762
+ gui.configureWeigths(w.root)
763
+ w.show()
764
+
765
+
766
+ if __name__ == '__main__':
767
+ root = tk.Tk()
768
+ root.withdraw()
769
+ root.title("View files")
770
+ l = TextFileViewer(root, fileList=sys.argv[1:])
771
+ l.pack(side=tk.TOP, fill=tk.BOTH)
772
+ gui.centerWindows(root)
773
+ root.deiconify()
774
+ root.mainloop()