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
pyworkflow/gui/gui.py
ADDED
@@ -0,0 +1,571 @@
|
|
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
|
+
import os
|
25
|
+
import tkinter as tk
|
26
|
+
import tkinter.font as tkFont
|
27
|
+
import queue
|
28
|
+
from functools import partial
|
29
|
+
from tkinter.ttk import Style
|
30
|
+
|
31
|
+
import pyworkflow
|
32
|
+
import pyworkflow as pw
|
33
|
+
from pyworkflow.object import Object
|
34
|
+
from pyworkflow.utils import Message, Icon
|
35
|
+
from PIL import Image, ImageTk
|
36
|
+
|
37
|
+
from pyworkflow.utils import SpriteImage, Sprite
|
38
|
+
from .widgets import Button
|
39
|
+
import numpy as np
|
40
|
+
|
41
|
+
DEFAULT_WINDOW_CLASS = "Scipion Framework"
|
42
|
+
|
43
|
+
# --------------- GUI CONFIGURATION parameters -----------------------
|
44
|
+
# TODO: read font size and name from config file
|
45
|
+
FONT_ITALIC = 'fontItalic'
|
46
|
+
FONT_NORMAL = 'fontNormal'
|
47
|
+
FONT_BOLD = 'fontBold'
|
48
|
+
FONT_BIG = 'fontBig'
|
49
|
+
# TextColor
|
50
|
+
# cfgCitationTextColor = "dark olive green"
|
51
|
+
# cfgLabelTextColor = "black"
|
52
|
+
# cfgSectionTextColor = "blue4"
|
53
|
+
# Background Color
|
54
|
+
# cfgBgColor = "light grey"
|
55
|
+
# cfgLabelBgColor = "white"
|
56
|
+
# cfgHighlightBgColor = cfgBgColor
|
57
|
+
#This with trigger the validation of the color falling back the firebrick if fails
|
58
|
+
cfgButtonActiveBgColor = pw.Config.getActiveColor()
|
59
|
+
cfgButtonFgColor = pw.Config.SCIPION_BG_COLOR
|
60
|
+
cfgButtonActiveFgColor = pw.Config.SCIPION_BG_COLOR
|
61
|
+
cfgButtonBgColor = pw.Config.SCIPION_MAIN_COLOR
|
62
|
+
cfgEntryBgColor = "lemon chiffon"
|
63
|
+
# cfgExpertLabelBgColor = "light salmon"
|
64
|
+
# cfgSectionBgColor = cfgButtonBgColor
|
65
|
+
# Color
|
66
|
+
# cfgListSelectColor = "DeepSkyBlue4"
|
67
|
+
# cfgBooleanSelectColor = "white"
|
68
|
+
# cfgButtonSelectColor = "DeepSkyBlue2"
|
69
|
+
# Dimensions limits
|
70
|
+
# cfgMaxHeight = 650
|
71
|
+
cfgMaxWidth = 800
|
72
|
+
# cfgMaxFontSize = 14
|
73
|
+
# cfgMinFontSize = 6
|
74
|
+
cfgWrapLenght = cfgMaxWidth - 50
|
75
|
+
|
76
|
+
# Style of treeviews where row height is variable based on the font size
|
77
|
+
LIST_TREEVIEW = 'List.Treeview'
|
78
|
+
BORDERLESS_TREEVIEW = 'Borderless.Treeview'
|
79
|
+
|
80
|
+
image_cache = dict()
|
81
|
+
|
82
|
+
class Config(Object):
|
83
|
+
pass
|
84
|
+
|
85
|
+
|
86
|
+
def saveConfig(filename):
|
87
|
+
from pyworkflow.mapper import SqliteMapper
|
88
|
+
from pyworkflow.object import String, Integer
|
89
|
+
|
90
|
+
mapper = SqliteMapper(filename)
|
91
|
+
o = Config()
|
92
|
+
for k, v in globals().items():
|
93
|
+
if k.startswith('cfg'):
|
94
|
+
if type(v) is str:
|
95
|
+
value = String(v)
|
96
|
+
else:
|
97
|
+
value = Integer(v)
|
98
|
+
setattr(o, k, value)
|
99
|
+
mapper.insert(o)
|
100
|
+
mapper.commit()
|
101
|
+
|
102
|
+
|
103
|
+
# --------------- FONT related variables and functions -----------------------
|
104
|
+
def setFont(fontKey, update=False, **opts):
|
105
|
+
"""Register a tkFont and store it in a globals of this module
|
106
|
+
this method should be called only after a tk.Tk() windows has been
|
107
|
+
created."""
|
108
|
+
if not hasFont(fontKey) or update:
|
109
|
+
globals()[fontKey] = tkFont.Font(**opts)
|
110
|
+
|
111
|
+
return globals()[fontKey]
|
112
|
+
|
113
|
+
|
114
|
+
def hasFont(fontKey):
|
115
|
+
return fontKey in globals()
|
116
|
+
|
117
|
+
|
118
|
+
def aliasFont(fontAlias, fontKey):
|
119
|
+
"""Set a fontAlias as another alias name of fontKey"""
|
120
|
+
g = globals()
|
121
|
+
g[fontAlias] = g[fontKey]
|
122
|
+
|
123
|
+
|
124
|
+
def getDefaultFont():
|
125
|
+
return tk.font.nametofont("TkDefaultFont")
|
126
|
+
|
127
|
+
|
128
|
+
def getNamedFont(fontName):
|
129
|
+
return globals()[fontName]
|
130
|
+
|
131
|
+
|
132
|
+
def getBigFont():
|
133
|
+
return getNamedFont(FONT_BIG)
|
134
|
+
|
135
|
+
|
136
|
+
def setCommonFonts(window=None):
|
137
|
+
"""Set some predefined common fonts.
|
138
|
+
Same conditions of setFont applies here."""
|
139
|
+
f = setFont(FONT_NORMAL, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE)
|
140
|
+
aliasFont('fontButton', FONT_NORMAL)
|
141
|
+
|
142
|
+
# Set default font size
|
143
|
+
default_font = getDefaultFont()
|
144
|
+
default_font.configure(size=pw.Config.SCIPION_FONT_SIZE, family=pw.Config.SCIPION_FONT_NAME)
|
145
|
+
|
146
|
+
fb = setFont(FONT_BOLD, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE,
|
147
|
+
weight='bold')
|
148
|
+
fi = setFont(FONT_ITALIC, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE,
|
149
|
+
slant='italic')
|
150
|
+
|
151
|
+
setFont(FONT_BIG, family=pw.Config.SCIPION_FONT_NAME, size=pw.Config.SCIPION_FONT_SIZE+8)
|
152
|
+
|
153
|
+
if window:
|
154
|
+
window.fontBig = tkFont.Font(size=pw.Config.SCIPION_FONT_SIZE + 2, family=pw.Config.SCIPION_FONT_NAME,
|
155
|
+
weight='bold')
|
156
|
+
window.font = f
|
157
|
+
window.fontBold = fb
|
158
|
+
window.fontItalic = fi
|
159
|
+
|
160
|
+
# This adds the default value for the listbox inside a combo box
|
161
|
+
# Which seems to not react to default font!!
|
162
|
+
window.root.option_add("*TCombobox*Listbox*Font", default_font)
|
163
|
+
window.root.option_add("*TCombobox*Font", default_font)
|
164
|
+
|
165
|
+
|
166
|
+
def changeFontSizeByDeltha(font, deltha, minSize=-999, maxSize=999):
|
167
|
+
size = font['size']
|
168
|
+
new_size = size + deltha
|
169
|
+
if minSize <= new_size <= maxSize:
|
170
|
+
font.configure(size=new_size)
|
171
|
+
|
172
|
+
|
173
|
+
def changeFontSize(font, event, minSize=-999, maxSize=999):
|
174
|
+
deltha = 2
|
175
|
+
if event.char == '-':
|
176
|
+
deltha = -2
|
177
|
+
changeFontSizeByDeltha(font, deltha, minSize, maxSize)
|
178
|
+
|
179
|
+
|
180
|
+
# --------------- IMAGE related variables and functions -----------------------
|
181
|
+
def getImage(imageName, imgDict=None, tkImage=True, percent=100,
|
182
|
+
maxheight=None):
|
183
|
+
""" Search for the image in the RESOURCES path list. """
|
184
|
+
|
185
|
+
global image_cache
|
186
|
+
|
187
|
+
if imageName is None:
|
188
|
+
return None
|
189
|
+
|
190
|
+
# Rename .gif by .png. In Linux with pillow 9.2.0 gif transparency is broken so
|
191
|
+
# we need to go for png. But in the past, in Macs png didn't work and made us go from png to gif
|
192
|
+
# We are now providing the 2 formats, prioritising pngs. If png work in MAC and windows then gif
|
193
|
+
# could be deleted. Otherwise, we may need to do this replacement based on the OS.
|
194
|
+
# NOTE: "convert my-image.gif PNG32:my-image.png" has converted gifs to pngs RGBA (32 bits) it seems pillow
|
195
|
+
# needs RGBA format to deal with transparencies.
|
196
|
+
|
197
|
+
# Most protocols.conf uses .gif extension. We need to use png!.
|
198
|
+
|
199
|
+
# ImageName could be either a file name (bookmark.gif) a full path image or a SpriteImage
|
200
|
+
if isinstance(imageName, SpriteImage):
|
201
|
+
fromSprite = True
|
202
|
+
imageStr = str(imageName)
|
203
|
+
else:
|
204
|
+
fromSprite=False
|
205
|
+
imageStr = imageName
|
206
|
+
|
207
|
+
if not os.path.isabs(imageStr) and imageStr not in [Icon.WAITING]:
|
208
|
+
imageStr = imageStr.replace(".gif", ".png")
|
209
|
+
|
210
|
+
if imageStr in image_cache:
|
211
|
+
return image_cache[imageStr]
|
212
|
+
|
213
|
+
# If it is a definition of a sprite image
|
214
|
+
if fromSprite:
|
215
|
+
image = Sprite.getImage(imageName)
|
216
|
+
else:
|
217
|
+
imagePath = pw.findResource(imageStr) if not os.path.isabs(imageStr) else imageStr
|
218
|
+
image = Image.open(imagePath) if imagePath else None
|
219
|
+
|
220
|
+
if image:
|
221
|
+
# For a future dark mode we might need to invert the image but it requires some extra work to make it look nice:
|
222
|
+
# image = invertImage(image)
|
223
|
+
w, h = image.size
|
224
|
+
newSize = None
|
225
|
+
if percent != 100: # Display image with other dimensions
|
226
|
+
fp = float(percent) / 100.0
|
227
|
+
newSize = int(fp * w), int(fp * h)
|
228
|
+
elif maxheight and h > maxheight:
|
229
|
+
newSize = int(w * float(maxheight) / h), maxheight
|
230
|
+
if newSize:
|
231
|
+
image.thumbnail(newSize, Image.LANCZOS)
|
232
|
+
if tkImage:
|
233
|
+
image = ImageTk.PhotoImage(image)
|
234
|
+
|
235
|
+
image_cache[imageStr] = image
|
236
|
+
return image
|
237
|
+
|
238
|
+
def invertImage(img):
|
239
|
+
# Creating a numpy array out of the image object
|
240
|
+
img_arry = np.array(img)
|
241
|
+
|
242
|
+
# Maximum intensity value of the color mode
|
243
|
+
I_max = 255
|
244
|
+
|
245
|
+
# Subtracting 255 (max value possible in a given image
|
246
|
+
# channel) from each pixel values and storing the result
|
247
|
+
img_arry = I_max - img_arry
|
248
|
+
|
249
|
+
# Creating an image object from the resultant numpy array
|
250
|
+
return Image.fromarray(img_arry)
|
251
|
+
# ---------------- Windows geometry utilities -----------------------
|
252
|
+
def getGeometry(win):
|
253
|
+
""" Return the geometry information of the windows
|
254
|
+
It will be a tuple (width, height, x, y)
|
255
|
+
"""
|
256
|
+
return (win.winfo_reqwidth(), win.winfo_reqheight(),
|
257
|
+
win.winfo_x(), win.winfo_y())
|
258
|
+
|
259
|
+
|
260
|
+
def centerWindows(root, dim=None, refWindows=None):
|
261
|
+
"""Center a windows in the middle of the screen
|
262
|
+
or in the middle of other windows(refWindows param)"""
|
263
|
+
root.update_idletasks()
|
264
|
+
if dim is None:
|
265
|
+
gw, gh, _, _ = getGeometry(root)
|
266
|
+
else:
|
267
|
+
gw, gh = dim
|
268
|
+
if refWindows:
|
269
|
+
rw, rh, rx, ry = getGeometry(refWindows)
|
270
|
+
x = rx + (rw - gw) / 2
|
271
|
+
y = ry + (rh - gh) / 2
|
272
|
+
else:
|
273
|
+
w = root.winfo_screenwidth()
|
274
|
+
h = root.winfo_screenheight()
|
275
|
+
x = (w - gw) / 2
|
276
|
+
y = (h - gh) / 2
|
277
|
+
|
278
|
+
root.geometry("%dx%d+%d+%d" % (gw, gh, x, y))
|
279
|
+
|
280
|
+
|
281
|
+
def configureWeigths(widget, row=0, column=0):
|
282
|
+
"""This function is a shortcut to a common
|
283
|
+
used pair of calls: rowconfigure and columnconfigure
|
284
|
+
for making childs widgets take the space available"""
|
285
|
+
widget.columnconfigure(column, weight=1)
|
286
|
+
widget.rowconfigure(row, weight=1)
|
287
|
+
|
288
|
+
|
289
|
+
def defineStyle():
|
290
|
+
"""
|
291
|
+
Defines some specific behaviour of the style.
|
292
|
+
"""
|
293
|
+
|
294
|
+
# To specify the height of the rows based on the font size.
|
295
|
+
# Should be centralized somewhere.
|
296
|
+
style = Style()
|
297
|
+
defaultFont = getDefaultFont()
|
298
|
+
|
299
|
+
iconsSizePx = int((pyworkflow.Config.SCIPION_ICON_ZOOM/100 * 32))
|
300
|
+
fontHeight = defaultFont.metrics()['linespace']
|
301
|
+
rowheight = max(iconsSizePx, fontHeight)
|
302
|
+
|
303
|
+
style.configure(LIST_TREEVIEW, rowheight=rowheight,
|
304
|
+
background=pw.Config.SCIPION_BG_COLOR,
|
305
|
+
fieldbackground=pw.Config.SCIPION_BG_COLOR)
|
306
|
+
style.configure(LIST_TREEVIEW+".Heading", font=(defaultFont["family"],defaultFont["size"]))
|
307
|
+
|
308
|
+
style.configure(BORDERLESS_TREEVIEW, rowheight=rowheight,
|
309
|
+
background=pw.Config.SCIPION_BG_COLOR,
|
310
|
+
fieldbackground=pw.Config.SCIPION_BG_COLOR,
|
311
|
+
borderwidth=0, font=(defaultFont["family"],defaultFont["size"]))
|
312
|
+
|
313
|
+
|
314
|
+
class Window:
|
315
|
+
"""Class to manage a Tk windows.
|
316
|
+
It will encapsulate some basic creation and
|
317
|
+
setup functions. """
|
318
|
+
# To allow plugins to add their own menus
|
319
|
+
_pluginMenus = dict()
|
320
|
+
|
321
|
+
def __init__(self, title='', masterWindow=None, weight=True,
|
322
|
+
minsize=(500, 300), icon=Icon.SCIPION_ICON, **kwargs):
|
323
|
+
"""Create a Tk window.
|
324
|
+
title: string to use as title for the windows.
|
325
|
+
master: if not provided, the windows create will be the principal one
|
326
|
+
weight: if true, the first col and row will be configured with weight=1
|
327
|
+
minsize: a minimum size for height and width
|
328
|
+
icon: if not None, set the windows icon
|
329
|
+
"""
|
330
|
+
# Init gui plugins
|
331
|
+
pw.Config.getDomain()._discoverGUIPlugins()
|
332
|
+
|
333
|
+
if masterWindow is None:
|
334
|
+
# Unused?? Window._root = self
|
335
|
+
self._images = {}
|
336
|
+
# If a window which isn't the main Scipion window is generated from another main window, e. g. with Scipion
|
337
|
+
# template after the refactoring of the kickoff, in which a dialog is launched and then a form, being it
|
338
|
+
# called from the command line, so there's no Scipion main window. In that case, a tk.Tk() exists because if
|
339
|
+
# a tk.TopLevel(), as the dialog, is directly launched, it automatically generates a main tk.Tk(). Thus,
|
340
|
+
# after that first auto-tk.Tk(), another tk.Tk() was created here, and so the previous information was lost.
|
341
|
+
# Solution proposed is to generate the root as an invisible window if it doesn't exist previously, and make
|
342
|
+
# he first window generated a tk.Toplevel. After that, all steps executed later will go through the else
|
343
|
+
# statement, being that way each new tk.Toplevel() correctly referenced.
|
344
|
+
root = tk.Tk()
|
345
|
+
root.withdraw() # Main window, invisible
|
346
|
+
# invoke the button on the return key
|
347
|
+
root.bind_class("Button", "<Key-Return>", lambda event: event.widget.invoke())
|
348
|
+
|
349
|
+
self._class = kwargs.get("_class", DEFAULT_WINDOW_CLASS)
|
350
|
+
self.root = tk.Toplevel(class_=self._class) # Toplevel of main window
|
351
|
+
else:
|
352
|
+
class_ = masterWindow._class if hasattr(masterWindow, "_class") else DEFAULT_WINDOW_CLASS
|
353
|
+
self.root = tk.Toplevel(masterWindow.getRoot(), class_=class_)
|
354
|
+
self.root.group(masterWindow.getRoot())
|
355
|
+
self._images = masterWindow._images
|
356
|
+
|
357
|
+
self.root.withdraw()
|
358
|
+
self.root.title(title)
|
359
|
+
|
360
|
+
if weight:
|
361
|
+
configureWeigths(self.root)
|
362
|
+
if minsize is not None:
|
363
|
+
self.root.minsize(minsize[0], minsize[1])
|
364
|
+
|
365
|
+
# Set the icon
|
366
|
+
self._setIcon(icon)
|
367
|
+
|
368
|
+
self.root.protocol("WM_DELETE_WINDOW", self._onClosing)
|
369
|
+
self._w, self._h, self._x, self._y = 0, 0, 0, 0
|
370
|
+
self.root.bind("<Configure>", self._configure)
|
371
|
+
self.master = masterWindow
|
372
|
+
setCommonFonts(self)
|
373
|
+
|
374
|
+
self.initial_focus = None
|
375
|
+
|
376
|
+
if kwargs.get('enableQueue', False):
|
377
|
+
self.queue = queue.Queue(maxsize=0)
|
378
|
+
else:
|
379
|
+
self.queue = None
|
380
|
+
|
381
|
+
def _setIcon(self, icon):
|
382
|
+
|
383
|
+
if icon is not None:
|
384
|
+
try:
|
385
|
+
path = pw.findResource(icon)
|
386
|
+
# If path is None --> Icon not found
|
387
|
+
if path is None:
|
388
|
+
# By default, if icon is not found use default scipion one.
|
389
|
+
path = pw.findResource(Icon.SCIPION_ICON)
|
390
|
+
|
391
|
+
abspath = os.path.abspath(path)
|
392
|
+
|
393
|
+
img = tk.Image("photo", file=abspath)
|
394
|
+
self.root.tk.call('wm', 'iconphoto', self.root._w, img)
|
395
|
+
except Exception as e:
|
396
|
+
# Do nothing if icon could not be loaded
|
397
|
+
pass
|
398
|
+
|
399
|
+
def __processQueue(self): # called from main frame
|
400
|
+
if not self.queue.empty():
|
401
|
+
func = self.queue.get(block=False)
|
402
|
+
# executes graphic interface function
|
403
|
+
func()
|
404
|
+
self._queueTimer = self.root.after(500, self.__processQueue)
|
405
|
+
|
406
|
+
def enqueue(self, func):
|
407
|
+
""" Put some function to be executed in the GUI main thread. """
|
408
|
+
self.queue.put(func)
|
409
|
+
|
410
|
+
def getRoot(self):
|
411
|
+
return self.root
|
412
|
+
|
413
|
+
def desiredDimensions(self):
|
414
|
+
"""Override this method to calculate desired dimensions."""
|
415
|
+
return None
|
416
|
+
|
417
|
+
def _configure(self, e):
|
418
|
+
""" Filter event and call appropriate handler. """
|
419
|
+
if self.root != e.widget:
|
420
|
+
return
|
421
|
+
|
422
|
+
_, _, x, y = getGeometry(self.root)
|
423
|
+
w, h = e.width, e.height
|
424
|
+
|
425
|
+
if w != self._w or h != self._h:
|
426
|
+
self._w, self._h = w, h
|
427
|
+
self.handleResize()
|
428
|
+
|
429
|
+
if x != self._x or y != self._y:
|
430
|
+
self._x, self._y = x, y
|
431
|
+
self.handleMove()
|
432
|
+
|
433
|
+
def handleResize(self):
|
434
|
+
"""Override this method to respond to resize events."""
|
435
|
+
pass
|
436
|
+
|
437
|
+
def handleMove(self):
|
438
|
+
"""Override this method to respond to move events."""
|
439
|
+
pass
|
440
|
+
|
441
|
+
def show(self, center=True, modal=False):
|
442
|
+
"""This function will enter in the Tk mainloop"""
|
443
|
+
if center:
|
444
|
+
if self.master is None:
|
445
|
+
refw = None
|
446
|
+
else:
|
447
|
+
refw = self.master.getRoot()
|
448
|
+
centerWindows(self.root, dim=self.desiredDimensions(),
|
449
|
+
refWindows=refw)
|
450
|
+
|
451
|
+
self.root.deiconify()
|
452
|
+
self.root.focus_set()
|
453
|
+
if self.queue is not None:
|
454
|
+
self._queueTimer = self.root.after(1000, self.__processQueue)
|
455
|
+
|
456
|
+
if self.initial_focus is not None:
|
457
|
+
self.initial_focus.focus_set()
|
458
|
+
|
459
|
+
if modal:
|
460
|
+
self.root.wait_window(self.root)
|
461
|
+
else:
|
462
|
+
self.root.mainloop()
|
463
|
+
|
464
|
+
def close(self, e=None):
|
465
|
+
self.root.destroy()
|
466
|
+
# JMRT: For some reason when Tkinter has an exception
|
467
|
+
# it does not exit the application as expected and
|
468
|
+
# remains in the mainloop, so here we are forcing
|
469
|
+
# to exit the whole system (only applies for the main window)
|
470
|
+
if self.master is None:
|
471
|
+
import sys
|
472
|
+
sys.exit()
|
473
|
+
|
474
|
+
def _onClosing(self):
|
475
|
+
"""Do some cleaning before closing."""
|
476
|
+
if self.master is None:
|
477
|
+
pass
|
478
|
+
else:
|
479
|
+
self.master.getRoot().focus_set()
|
480
|
+
if self.queue is not None:
|
481
|
+
self.root.after_cancel(self._queueTimer)
|
482
|
+
self.close()
|
483
|
+
|
484
|
+
def getImage(self, imgName, percent=100, maxheight=None):
|
485
|
+
return getImage(imgName, percent=percent,
|
486
|
+
maxheight=maxheight)
|
487
|
+
|
488
|
+
def createMainMenu(self, menuConfig):
|
489
|
+
"""Create Main menu from the given MenuConfig object."""
|
490
|
+
menu = tk.Menu(self.root, font=self.font)
|
491
|
+
self._addMenuChilds(menu, menuConfig)
|
492
|
+
self._addPluginMenus(menu)
|
493
|
+
self.root.config(menu=menu)
|
494
|
+
return menu
|
495
|
+
|
496
|
+
def _addMenuChilds(self, menu, menuConfig):
|
497
|
+
"""Add entries of menuConfig in menu
|
498
|
+
(using add_cascade or add_command for sub-menus and final options)."""
|
499
|
+
# Helper function to create the main menu.
|
500
|
+
for sub in menuConfig:
|
501
|
+
menuLabel = sub.text
|
502
|
+
if not menuLabel: # empty or None label means a separator
|
503
|
+
menu.add_separator()
|
504
|
+
elif len(sub) > 0: # sub-menu
|
505
|
+
submenu = tk.Menu(self.root, tearoff=0, font=self.font)
|
506
|
+
menu.add_cascade(label=menuLabel, menu=submenu)
|
507
|
+
self._addMenuChilds(submenu, sub) # recursive filling
|
508
|
+
else: # menu option
|
509
|
+
# If there is an entry called "Browse files", when clicked it
|
510
|
+
# will call the method onBrowseFiles() (it has to be defined!)
|
511
|
+
def callback(name):
|
512
|
+
"""Return a callback function named "on<Name>"."""
|
513
|
+
f = "on%s" % "".join(x.capitalize() for x in name.split())
|
514
|
+
return lambda: getattr(self, f)()
|
515
|
+
|
516
|
+
if sub.shortCut is not None:
|
517
|
+
menuLabel += ' (' + sub.shortCut + ')'
|
518
|
+
|
519
|
+
menu.add_command(label=menuLabel, compound=tk.LEFT,
|
520
|
+
image=self.getImage(sub.icon),
|
521
|
+
command=callback(name=sub.text))
|
522
|
+
|
523
|
+
def _addPluginMenus(self, menu):
|
524
|
+
|
525
|
+
if self._pluginMenus:
|
526
|
+
submenu = tk.Menu(self.root, tearoff=0, font=self.font)
|
527
|
+
menu.add_cascade(label="Others", menu=submenu)
|
528
|
+
|
529
|
+
# For each plugin menu
|
530
|
+
for label in self._pluginMenus:
|
531
|
+
submenu.add_command(label=label, compound=tk.LEFT,
|
532
|
+
image=self.getImage(self._pluginMenus.get(label)[1]),
|
533
|
+
command=partial(self.plugin_callback, label))
|
534
|
+
|
535
|
+
def plugin_callback(self, label):
|
536
|
+
return self._pluginMenus.get(label)[0](self)
|
537
|
+
|
538
|
+
@classmethod
|
539
|
+
def registerPluginMenu(cls, label, callback, icon=None):
|
540
|
+
# TODO: have a proper model instead of a tuple?
|
541
|
+
cls._pluginMenus[label] = (callback, icon)
|
542
|
+
|
543
|
+
def showError(self, msg, header="Error", exception=None):
|
544
|
+
"""Pops up a dialog with the error message
|
545
|
+
:param msg Message to display
|
546
|
+
:param header Title of the dialog
|
547
|
+
:param exception: Optional. exception associated"""
|
548
|
+
from .dialog import showError
|
549
|
+
showError(header, msg, self.root, exception=exception)
|
550
|
+
|
551
|
+
def showInfo(self, msg, header="Info"):
|
552
|
+
from .dialog import showInfo
|
553
|
+
showInfo(header, msg, self.root)
|
554
|
+
|
555
|
+
def showWarning(self, msg, header='Warning'):
|
556
|
+
from .dialog import showWarning
|
557
|
+
showWarning(header, msg, self.root)
|
558
|
+
|
559
|
+
def askYesNo(self, title, msg):
|
560
|
+
from .dialog import askYesNo
|
561
|
+
return askYesNo(title, msg, self.root)
|
562
|
+
|
563
|
+
def createCloseButton(self, parent):
|
564
|
+
""" Create a button for closing the window, setting
|
565
|
+
the proper label and icon.
|
566
|
+
"""
|
567
|
+
return Button(parent, Message.LABEL_BUTTON_CLOSE, Icon.ACTION_CLOSE,
|
568
|
+
command=self.close)
|
569
|
+
|
570
|
+
def configureWeights(self, row=0, column=0):
|
571
|
+
configureWeigths(self.root, row, column)
|