spacr 0.2.2__py3-none-any.whl → 0.2.4__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.
- spacr/app_annotate.py +3 -4
- spacr/core.py +58 -229
- spacr/gui.py +2 -1
- spacr/gui_core.py +83 -36
- spacr/gui_elements.py +422 -72
- spacr/gui_utils.py +58 -32
- spacr/io.py +121 -37
- spacr/measure.py +6 -8
- spacr/resources/icons/abort.png +0 -0
- spacr/resources/icons/classify.png +0 -0
- spacr/resources/icons/make_masks.png +0 -0
- spacr/resources/icons/mask.png +0 -0
- spacr/resources/icons/measure.png +0 -0
- spacr/resources/icons/ml_analyze.png +0 -0
- spacr/resources/icons/recruitment.png +0 -0
- spacr/resources/icons/regression.png +0 -0
- spacr/resources/icons/run.png +0 -0
- spacr/resources/icons/spacr_logo_rotation.gif +0 -0
- spacr/resources/icons/train_cellpose.png +0 -0
- spacr/resources/icons/umap.png +0 -0
- spacr/settings.py +1 -4
- spacr/utils.py +55 -0
- {spacr-0.2.2.dist-info → spacr-0.2.4.dist-info}/METADATA +1 -1
- spacr-0.2.4.dist-info/RECORD +58 -0
- spacr/alpha.py +0 -807
- spacr/annotate_app.py +0 -670
- spacr/annotate_app_v2.py +0 -670
- spacr/app_make_masks_v2.py +0 -686
- spacr/classify_app.py +0 -201
- spacr/cli.py +0 -41
- spacr/foldseek.py +0 -779
- spacr/get_alfafold_structures.py +0 -72
- spacr/gui_2.py +0 -157
- spacr/gui_annotate.py +0 -145
- spacr/gui_classify_app.py +0 -201
- spacr/gui_make_masks_app.py +0 -927
- spacr/gui_make_masks_app_v2.py +0 -688
- spacr/gui_mask_app.py +0 -249
- spacr/gui_measure_app.py +0 -246
- spacr/gui_run.py +0 -58
- spacr/gui_sim_app.py +0 -0
- spacr/gui_wrappers.py +0 -149
- spacr/icons/abort.png +0 -0
- spacr/icons/abort.svg +0 -1
- spacr/icons/download.png +0 -0
- spacr/icons/download.svg +0 -1
- spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/download_for_offline_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/logo_spacr.png +0 -0
- spacr/icons/make_masks.png +0 -0
- spacr/icons/make_masks.svg +0 -1
- spacr/icons/map_barcodes.png +0 -0
- spacr/icons/map_barcodes.svg +0 -1
- spacr/icons/mask.png +0 -0
- spacr/icons/mask.svg +0 -1
- spacr/icons/measure.png +0 -0
- spacr/icons/measure.svg +0 -1
- spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/play_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/run.png +0 -0
- spacr/icons/run.svg +0 -1
- spacr/icons/sequencing.png +0 -0
- spacr/icons/sequencing.svg +0 -1
- spacr/icons/settings.png +0 -0
- spacr/icons/settings.svg +0 -1
- spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/settings_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.png +0 -0
- spacr/icons/stop_circle_100dp_E8EAED_FILL0_wght100_GRAD-25_opsz48.svg +0 -1
- spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.png +0 -0
- spacr/icons/theater_comedy_100dp_E8EAED_FILL0_wght100_GRAD200_opsz48.svg +0 -1
- spacr/make_masks_app.py +0 -929
- spacr/make_masks_app_v2.py +0 -688
- spacr/mask_app.py +0 -249
- spacr/measure_app.py +0 -246
- spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model +0 -0
- spacr/models/cp/toxo_plaque_cyto_e25000_X1120_Y1120.CP_model_settings.csv +0 -23
- spacr/models/cp/toxo_pv_lumen.CP_model +0 -0
- spacr/old_code.py +0 -358
- spacr/resources/icons/abort.svg +0 -1
- spacr/resources/icons/annotate.svg +0 -1
- spacr/resources/icons/classify.svg +0 -1
- spacr/resources/icons/download.svg +0 -1
- spacr/resources/icons/icon.psd +0 -0
- spacr/resources/icons/make_masks.svg +0 -1
- spacr/resources/icons/map_barcodes.svg +0 -1
- spacr/resources/icons/mask.svg +0 -1
- spacr/resources/icons/measure.svg +0 -1
- spacr/resources/icons/run.svg +0 -1
- spacr/resources/icons/run_2.png +0 -0
- spacr/resources/icons/run_2.svg +0 -1
- spacr/resources/icons/sequencing.svg +0 -1
- spacr/resources/icons/settings.svg +0 -1
- spacr/resources/icons/train_cellpose.svg +0 -1
- spacr/test_gui.py +0 -0
- spacr-0.2.2.dist-info/RECORD +0 -126
- /spacr/resources/icons/{cellpose.png → cellpose_all.png} +0 -0
- {spacr-0.2.2.dist-info → spacr-0.2.4.dist-info}/LICENSE +0 -0
- {spacr-0.2.2.dist-info → spacr-0.2.4.dist-info}/WHEEL +0 -0
- {spacr-0.2.2.dist-info → spacr-0.2.4.dist-info}/entry_points.txt +0 -0
- {spacr-0.2.2.dist-info → spacr-0.2.4.dist-info}/top_level.txt +0 -0
spacr/gui_elements.py
CHANGED
@@ -3,7 +3,7 @@ import tkinter as tk
|
|
3
3
|
from tkinter import ttk
|
4
4
|
import tkinter.font as tkFont
|
5
5
|
from queue import Queue
|
6
|
-
from tkinter import Label
|
6
|
+
from tkinter import Label, Frame, Button
|
7
7
|
import numpy as np
|
8
8
|
from PIL import Image, ImageOps, ImageTk
|
9
9
|
from concurrent.futures import ThreadPoolExecutor
|
@@ -15,19 +15,20 @@ from skimage.draw import polygon, line
|
|
15
15
|
from skimage.transform import resize
|
16
16
|
from scipy.ndimage import binary_fill_holes, label
|
17
17
|
from tkinter import ttk, scrolledtext
|
18
|
-
import platform
|
19
18
|
|
20
|
-
def
|
19
|
+
def set_default_font(root, font_name="Arial", size=12):
|
20
|
+
default_font = (font_name, size)
|
21
|
+
root.option_add("*Font", default_font)
|
22
|
+
root.option_add("*TButton.Font", default_font)
|
23
|
+
root.option_add("*TLabel.Font", default_font)
|
24
|
+
root.option_add("*TEntry.Font", default_font)
|
21
25
|
|
22
|
-
|
23
|
-
bg_color = '#313131'
|
24
|
-
else:
|
25
|
-
bg_color = '#000000'
|
26
|
+
def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font_family="Arial", font_size=12, bg_color='black', fg_color='white', active_color='blue', inactive_color='dark_gray'):
|
26
27
|
|
27
28
|
if active_color == 'teal':
|
28
29
|
active_color = '#008080'
|
29
30
|
if inactive_color == 'dark_gray':
|
30
|
-
inactive_color = '#050505'
|
31
|
+
inactive_color = '#2B2B2B' # '#333333' #'#050505'
|
31
32
|
if bg_color == 'black':
|
32
33
|
bg_color = '#000000'
|
33
34
|
if fg_color == 'white':
|
@@ -35,27 +36,39 @@ def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font
|
|
35
36
|
if active_color == 'blue':
|
36
37
|
active_color = '#007BFF'
|
37
38
|
|
39
|
+
padding = '5 5 5 5'
|
38
40
|
font_style = tkFont.Font(family=font_family, size=font_size)
|
39
|
-
|
40
|
-
style.
|
41
|
-
|
42
|
-
style.configure('
|
43
|
-
style.
|
44
|
-
style.configure('
|
45
|
-
style.configure('
|
46
|
-
style.
|
47
|
-
style.configure('TLabel',
|
41
|
+
|
42
|
+
style.theme_use('clam')
|
43
|
+
|
44
|
+
style.configure('TEntry', padding=padding)
|
45
|
+
style.configure('TCombobox', padding=padding)
|
46
|
+
style.configure('Spacr.TEntry', padding=padding)
|
47
|
+
style.configure('TEntry', padding=padding)
|
48
|
+
style.configure('Spacr.TEntry', padding=padding)
|
49
|
+
style.configure('Custom.TLabel', padding=padding)
|
50
|
+
#style.configure('Spacr.TCheckbutton', padding=padding)
|
51
|
+
style.configure('TButton', padding=padding)
|
52
|
+
|
48
53
|
style.configure('TFrame', background=bg_color)
|
49
54
|
style.configure('TPanedwindow', background=bg_color)
|
50
|
-
style.configure('
|
51
|
-
|
52
|
-
|
53
|
-
style.configure('
|
54
|
-
style.
|
55
|
-
style.
|
56
|
-
|
57
|
-
|
58
|
-
style.configure('
|
55
|
+
style.configure('TLabel', background=bg_color, foreground=fg_color, font=font_style)
|
56
|
+
|
57
|
+
|
58
|
+
#style.configure('Custom.TLabel', padding='5 5 5 5', borderwidth=1, relief='flat', background=bg_color, foreground=fg_color, font=font_style)
|
59
|
+
#style.configure('Spacr.TCheckbutton', background=bg_color, foreground=fg_color, indicatoron=False, relief='flat', font="15")
|
60
|
+
#style.map('Spacr.TCheckbutton', background=[('selected', bg_color), ('active', bg_color)], foreground=[('selected', fg_color), ('active', fg_color)])
|
61
|
+
|
62
|
+
|
63
|
+
#style.configure('TNotebook', background=bg_color, tabmargins=[2, 5, 2, 0])
|
64
|
+
#style.configure('TNotebook.Tab', background=bg_color, foreground=fg_color, padding=[5, 5], font=font_style)
|
65
|
+
#style.map('TNotebook.Tab', background=[('selected', active_color), ('active', active_color)], foreground=[('selected', fg_color), ('active', fg_color)])
|
66
|
+
#style.configure('TButton', background=bg_color, foreground=fg_color, padding='5 5 5 5', font=font_style)
|
67
|
+
#style.map('TButton', background=[('active', active_color), ('disabled', inactive_color)])
|
68
|
+
#style.configure('Vertical.TScrollbar', background=bg_color, troughcolor=bg_color, bordercolor=bg_color)
|
69
|
+
#style.configure('Horizontal.TScrollbar', background=bg_color, troughcolor=bg_color, bordercolor=bg_color)
|
70
|
+
#style.configure('Custom.TLabelFrame', font=(font_family, font_size, 'bold'), background=bg_color, foreground='white', relief='solid', borderwidth=1)
|
71
|
+
#style.configure('Custom.TLabelFrame.Label', background=bg_color, foreground='white', font=(font_family, font_size, 'bold'))
|
59
72
|
|
60
73
|
if parent_frame:
|
61
74
|
parent_frame.configure(bg=bg_color)
|
@@ -86,20 +99,245 @@ def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font
|
|
86
99
|
|
87
100
|
return {'font_family': font_family, 'font_size': font_size, 'bg_color': bg_color, 'fg_color': fg_color, 'active_color': active_color, 'inactive_color': inactive_color}
|
88
101
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
102
|
+
class spacrEntry(tk.Frame):
|
103
|
+
def __init__(self, parent, textvariable=None, outline=False, *args, **kwargs):
|
104
|
+
super().__init__(parent, *args, **kwargs)
|
105
|
+
|
106
|
+
# Set dark style
|
107
|
+
style_out = set_dark_style(ttk.Style())
|
108
|
+
self.bg_color = style_out['inactive_color']
|
109
|
+
self.active_color = style_out['active_color']
|
110
|
+
self.fg_color = style_out['fg_color']
|
111
|
+
self.outline = outline
|
112
|
+
self.font_family = style_out['font_family']
|
113
|
+
self.font_size = style_out['font_size']
|
114
|
+
|
115
|
+
# Set the background color of the frame
|
116
|
+
self.configure(bg=style_out['bg_color'])
|
117
|
+
|
118
|
+
# Create a canvas for the rounded rectangle background
|
119
|
+
self.canvas_width = 220 # Adjusted for padding
|
120
|
+
self.canvas_height = 40 # Adjusted for padding
|
121
|
+
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=style_out['bg_color'])
|
122
|
+
self.canvas.pack()
|
123
|
+
|
124
|
+
self.entry = tk.Entry(self, textvariable=textvariable, bd=0, highlightthickness=0, fg=self.fg_color, font=(self.font_family, self.font_size), bg=self.bg_color)
|
125
|
+
self.entry.place(relx=0.5, rely=0.5, anchor=tk.CENTER, width=190, height=20) # Centered positioning
|
126
|
+
|
127
|
+
# Bind events to change the background color on focus
|
128
|
+
self.entry.bind("<FocusIn>", self.on_focus_in)
|
129
|
+
self.entry.bind("<FocusOut>", self.on_focus_out)
|
130
|
+
|
131
|
+
self.draw_rounded_rectangle(self.bg_color)
|
132
|
+
|
133
|
+
def draw_rounded_rectangle(self, color):
|
134
|
+
radius = 15 # Increased radius for more rounded corners
|
135
|
+
x0, y0 = 10, 5
|
136
|
+
x1, y1 = 210, 35
|
137
|
+
self.canvas.delete("all")
|
138
|
+
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
|
139
|
+
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
|
140
|
+
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=color)
|
141
|
+
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=color)
|
142
|
+
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
|
143
|
+
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
|
144
|
+
|
145
|
+
def on_focus_in(self, event):
|
146
|
+
self.draw_rounded_rectangle(self.active_color)
|
147
|
+
self.entry.config(bg=self.active_color)
|
148
|
+
|
149
|
+
def on_focus_out(self, event):
|
150
|
+
self.draw_rounded_rectangle(self.bg_color)
|
151
|
+
self.entry.config(bg=self.bg_color)
|
152
|
+
|
153
|
+
class spacrCheck(tk.Frame):
|
154
|
+
def __init__(self, parent, text="", variable=None, *args, **kwargs):
|
155
|
+
super().__init__(parent, *args, **kwargs)
|
156
|
+
|
157
|
+
style_out = set_dark_style(ttk.Style())
|
158
|
+
self.bg_color = style_out['bg_color']
|
159
|
+
self.active_color = style_out['active_color']
|
160
|
+
self.fg_color = style_out['fg_color']
|
161
|
+
self.inactive_color = style_out['inactive_color']
|
162
|
+
self.variable = variable
|
163
|
+
|
164
|
+
self.configure(bg=self.bg_color)
|
165
|
+
|
166
|
+
# Create a canvas for the rounded square background
|
167
|
+
self.canvas_width = 20
|
168
|
+
self.canvas_height = 20
|
169
|
+
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=self.bg_color)
|
170
|
+
self.canvas.pack()
|
171
|
+
|
172
|
+
# Draw the initial rounded square based on the variable's value
|
173
|
+
self.draw_rounded_square(self.active_color if self.variable.get() else self.inactive_color)
|
174
|
+
|
175
|
+
# Bind variable changes to update the checkbox
|
176
|
+
self.variable.trace_add('write', self.update_check)
|
177
|
+
|
178
|
+
# Bind click event to toggle the variable
|
179
|
+
self.canvas.bind("<Button-1>", self.toggle_variable)
|
180
|
+
|
181
|
+
def draw_rounded_square(self, color):
|
182
|
+
radius = 5 # Adjust the radius for more rounded corners
|
183
|
+
x0, y0 = 2, 2
|
184
|
+
x1, y1 = 18, 18
|
185
|
+
self.canvas.delete("all")
|
186
|
+
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=self.fg_color)
|
187
|
+
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=self.fg_color)
|
188
|
+
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=self.fg_color)
|
189
|
+
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=self.fg_color)
|
190
|
+
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
|
191
|
+
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
|
192
|
+
self.canvas.create_line(x0 + radius / 2, y0, x1 - radius / 2, y0, fill=self.fg_color)
|
193
|
+
self.canvas.create_line(x0 + radius / 2, y1, x1 - radius / 2, y1, fill=self.fg_color)
|
194
|
+
self.canvas.create_line(x0, y0 + radius / 2, x0, y1 - radius / 2, fill=self.fg_color)
|
195
|
+
self.canvas.create_line(x1, y0 + radius / 2, x1, y1 - radius / 2, fill=self.fg_color)
|
196
|
+
|
197
|
+
def update_check(self, *args):
|
198
|
+
self.draw_rounded_square(self.active_color if self.variable.get() else self.inactive_color)
|
199
|
+
|
200
|
+
def toggle_variable(self, event):
|
201
|
+
self.variable.set(not self.variable.get())
|
202
|
+
|
203
|
+
class spacrCombo(tk.Frame):
|
204
|
+
def __init__(self, parent, textvariable=None, values=None, *args, **kwargs):
|
205
|
+
super().__init__(parent, *args, **kwargs)
|
206
|
+
|
207
|
+
# Set dark style
|
208
|
+
style_out = set_dark_style(ttk.Style())
|
209
|
+
self.bg_color = style_out['bg_color']
|
210
|
+
self.active_color = style_out['active_color']
|
211
|
+
self.fg_color = style_out['fg_color']
|
212
|
+
self.inactive_color = style_out['inactive_color']
|
213
|
+
self.font_family = style_out['font_family']
|
214
|
+
self.font_size = style_out['font_size']
|
215
|
+
|
216
|
+
self.values = values or []
|
217
|
+
|
218
|
+
# Create a canvas for the rounded rectangle background
|
219
|
+
self.canvas_width = 220 # Adjusted for padding
|
220
|
+
self.canvas_height = 40 # Adjusted for padding
|
221
|
+
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=self.bg_color)
|
222
|
+
self.canvas.pack()
|
223
|
+
|
224
|
+
self.var = textvariable if textvariable else tk.StringVar()
|
225
|
+
self.selected_value = self.var.get()
|
226
|
+
|
227
|
+
# Create the label to display the selected value
|
228
|
+
self.label = tk.Label(self, text=self.selected_value, bg=self.inactive_color, fg=self.fg_color, font=(self.font_family, self.font_size))
|
229
|
+
self.label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
230
|
+
|
231
|
+
# Bind events to open the dropdown menu
|
232
|
+
self.canvas.bind("<Button-1>", self.on_click)
|
233
|
+
self.label.bind("<Button-1>", self.on_click)
|
234
|
+
|
235
|
+
self.draw_rounded_rectangle(self.inactive_color)
|
236
|
+
|
237
|
+
self.dropdown_menu = None
|
238
|
+
|
239
|
+
def draw_rounded_rectangle(self, color):
|
240
|
+
radius = 15 # Increased radius for more rounded corners
|
241
|
+
x0, y0 = 10, 5
|
242
|
+
x1, y1 = 210, 35
|
243
|
+
self.canvas.delete("all")
|
244
|
+
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
|
245
|
+
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
|
246
|
+
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=color)
|
247
|
+
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=color)
|
248
|
+
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
|
249
|
+
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
|
250
|
+
self.label.config(bg=color) # Update label background to match rectangle color
|
251
|
+
|
252
|
+
def on_click(self, event):
|
253
|
+
if self.dropdown_menu is None:
|
254
|
+
self.open_dropdown()
|
255
|
+
else:
|
256
|
+
self.close_dropdown()
|
257
|
+
|
258
|
+
def open_dropdown(self):
|
259
|
+
self.draw_rounded_rectangle(self.active_color)
|
260
|
+
|
261
|
+
self.dropdown_menu = tk.Toplevel(self)
|
262
|
+
self.dropdown_menu.wm_overrideredirect(True)
|
263
|
+
|
264
|
+
x, y, width, height = self.winfo_rootx(), self.winfo_rooty(), self.winfo_width(), self.winfo_height()
|
265
|
+
self.dropdown_menu.geometry(f"{width}x{len(self.values) * 30}+{x}+{y + height}")
|
266
|
+
|
267
|
+
for index, value in enumerate(self.values):
|
268
|
+
display_text = value if value is not None else 'None'
|
269
|
+
item = tk.Label(self.dropdown_menu, text=display_text, bg=self.inactive_color, fg=self.fg_color, font=(self.font_family, self.font_size), anchor='w')
|
270
|
+
item.pack(fill='both')
|
271
|
+
item.bind("<Button-1>", lambda e, v=value: self.on_select(v))
|
272
|
+
item.bind("<Enter>", lambda e, w=item: w.config(bg=self.active_color))
|
273
|
+
item.bind("<Leave>", lambda e, w=item: w.config(bg=self.inactive_color))
|
274
|
+
|
275
|
+
def close_dropdown(self):
|
276
|
+
self.draw_rounded_rectangle(self.inactive_color)
|
277
|
+
|
278
|
+
if self.dropdown_menu:
|
279
|
+
self.dropdown_menu.destroy()
|
280
|
+
self.dropdown_menu = None
|
281
|
+
|
282
|
+
def on_select(self, value):
|
283
|
+
display_text = value if value is not None else 'None'
|
284
|
+
self.var.set(value)
|
285
|
+
self.label.config(text=display_text)
|
286
|
+
self.selected_value = value
|
287
|
+
self.close_dropdown()
|
288
|
+
|
289
|
+
def set(self, value):
|
290
|
+
display_text = value if value is not None else 'None'
|
291
|
+
self.var.set(value)
|
292
|
+
self.label.config(text=display_text)
|
293
|
+
self.selected_value = value
|
95
294
|
|
96
295
|
class spacrDropdownMenu(tk.OptionMenu):
|
97
296
|
def __init__(self, parent, variable, options, command=None, **kwargs):
|
98
297
|
self.variable = variable
|
99
|
-
self.variable.set("
|
298
|
+
self.variable.set("Settings Category")
|
100
299
|
super().__init__(parent, self.variable, *options, command=command, **kwargs)
|
101
300
|
self.update_styles()
|
102
301
|
|
302
|
+
# Hide the original button
|
303
|
+
self.configure(highlightthickness=0, relief='flat', bg='#2B2B2B', fg='#2B2B2B')
|
304
|
+
|
305
|
+
# Create custom button
|
306
|
+
self.create_custom_button()
|
307
|
+
|
308
|
+
def create_custom_button(self):
|
309
|
+
self.canvas_width = self.winfo_reqwidth() # Use the required width of the widget
|
310
|
+
self.canvas_height = 40 # Adjust the height as needed
|
311
|
+
self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg='#2B2B2B')
|
312
|
+
self.canvas.pack()
|
313
|
+
self.label = tk.Label(self.canvas, text="Settings Category", bg='#2B2B2B', fg='#ffffff', font=('Arial', 12))
|
314
|
+
self.label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
|
315
|
+
self.draw_rounded_rectangle('#2B2B2B')
|
316
|
+
|
317
|
+
# Bind the click event to open the dropdown menu
|
318
|
+
self.canvas.bind("<Button-1>", self.on_click)
|
319
|
+
self.label.bind("<Button-1>", self.on_click)
|
320
|
+
|
321
|
+
def draw_rounded_rectangle(self, color):
|
322
|
+
radius = 15
|
323
|
+
x0, y0 = 10, 5
|
324
|
+
x1, y1 = self.canvas_width - 10, self.canvas_height - 5 # Adjust based on canvas size
|
325
|
+
self.canvas.delete("all")
|
326
|
+
self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
|
327
|
+
self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
|
328
|
+
self.canvas.create_arc((x0, y1 - radius, x0 + radius, y1), start=180, extent=90, fill=color, outline=color)
|
329
|
+
self.canvas.create_arc((x1 - radius, y1 - radius, x1, y1), start=270, extent=90, fill=color, outline=color)
|
330
|
+
self.canvas.create_rectangle((x0 + radius / 2, y0, x1 - radius / 2, y1), fill=color, outline=color)
|
331
|
+
self.canvas.create_rectangle((x0, y0 + radius / 2, x1, y1 - radius / 2), fill=color, outline=color)
|
332
|
+
self.label.config(bg=color) # Update label background to match rectangle color
|
333
|
+
|
334
|
+
def on_click(self, event):
|
335
|
+
self.post_menu()
|
336
|
+
|
337
|
+
def post_menu(self):
|
338
|
+
x, y, width, height = self.winfo_rootx(), self.winfo_rooty(), self.winfo_width(), self.winfo_height()
|
339
|
+
self.menu.post(x, y + height)
|
340
|
+
|
103
341
|
def update_styles(self, active_categories=None):
|
104
342
|
style = ttk.Style()
|
105
343
|
style_out = set_dark_style(style, widgets=[self])
|
@@ -124,6 +362,56 @@ class spacrCheckbutton(ttk.Checkbutton):
|
|
124
362
|
style = ttk.Style()
|
125
363
|
_ = set_dark_style(style, widgets=[self])
|
126
364
|
|
365
|
+
class spacrProgressBar(ttk.Progressbar):
|
366
|
+
def __init__(self, parent, *args, **kwargs):
|
367
|
+
super().__init__(parent, *args, **kwargs)
|
368
|
+
|
369
|
+
# Get the style colors
|
370
|
+
style_out = set_dark_style(ttk.Style())
|
371
|
+
|
372
|
+
self.fg_color = style_out['fg_color']
|
373
|
+
self.bg_color = style_out['bg_color']
|
374
|
+
self.active_color = style_out['active_color']
|
375
|
+
self.inactive_color = style_out['inactive_color']
|
376
|
+
|
377
|
+
# Configure the style for the progress bar
|
378
|
+
self.style = ttk.Style()
|
379
|
+
self.style.configure(
|
380
|
+
"spacr.Horizontal.TProgressbar",
|
381
|
+
troughcolor=self.bg_color,
|
382
|
+
background=self.active_color,
|
383
|
+
thickness=20,
|
384
|
+
troughrelief='flat',
|
385
|
+
borderwidth=0
|
386
|
+
)
|
387
|
+
self.configure(style="spacr.Horizontal.TProgressbar")
|
388
|
+
|
389
|
+
# Set initial value to 0
|
390
|
+
self['value'] = 0
|
391
|
+
|
392
|
+
# Create the progress label
|
393
|
+
self.progress_label = tk.Label(parent, text="Processing: 0/0", anchor='w', justify='left', bg=self.inactive_color, fg=self.fg_color)
|
394
|
+
self.progress_label.grid(row=1, column=0, columnspan=2, pady=5, padx=5, sticky='ew')
|
395
|
+
|
396
|
+
# Initialize attributes for time and operation
|
397
|
+
self.operation_type = None
|
398
|
+
self.time_image = None
|
399
|
+
self.time_batch = None
|
400
|
+
self.time_left = None
|
401
|
+
|
402
|
+
def update_label(self):
|
403
|
+
# Update the progress label with current progress and additional info
|
404
|
+
label_text = f"Processing: {self['value']}/{self['maximum']}"
|
405
|
+
if self.operation_type:
|
406
|
+
label_text += f", {self.operation_type}"
|
407
|
+
if self.time_image:
|
408
|
+
label_text += f", Time/image: {self.time_image:.3f} sec"
|
409
|
+
if self.time_batch:
|
410
|
+
label_text += f", Time/batch: {self.time_batch:.3f} sec"
|
411
|
+
if self.time_left:
|
412
|
+
label_text += f", Time_left: {self.time_left:.3f} min"
|
413
|
+
self.progress_label.config(text=label_text)
|
414
|
+
|
127
415
|
class spacrFrame(ttk.Frame):
|
128
416
|
def __init__(self, container, width=None, *args, bg='black', **kwargs):
|
129
417
|
super().__init__(container, *args, **kwargs)
|
@@ -196,7 +484,7 @@ class spacrLabel(tk.Frame):
|
|
196
484
|
self.canvas.itemconfig(self.label_text, text=text)
|
197
485
|
|
198
486
|
class spacrButton(tk.Frame):
|
199
|
-
def __init__(self, parent, text="", command=None, font=None, icon_name=None, size=50, show_text=True, outline=
|
487
|
+
def __init__(self, parent, text="", command=None, font=None, icon_name=None, size=50, show_text=True, outline=False, *args, **kwargs):
|
200
488
|
super().__init__(parent, *args, **kwargs)
|
201
489
|
|
202
490
|
self.text = text.capitalize() # Capitalize only the first letter of the text
|
@@ -220,10 +508,12 @@ class spacrButton(tk.Frame):
|
|
220
508
|
# Apply dark style and get color settings
|
221
509
|
color_settings = set_dark_style(ttk.Style(), containers=[self], widgets=[self.canvas])
|
222
510
|
|
511
|
+
self.inactive_color = color_settings['inactive_color']
|
512
|
+
|
223
513
|
if self.outline:
|
224
|
-
self.button_bg = self.create_rounded_rectangle(2, 2, self.button_width + 2, self.size + 2, radius=20, fill=
|
514
|
+
self.button_bg = self.create_rounded_rectangle(2, 2, self.button_width + 2, self.size + 2, radius=20, fill=self.inactive_color, outline=color_settings['fg_color'])
|
225
515
|
else:
|
226
|
-
self.button_bg = self.create_rounded_rectangle(2, 2, self.button_width + 2, self.size + 2, radius=20, fill=
|
516
|
+
self.button_bg = self.create_rounded_rectangle(2, 2, self.button_width + 2, self.size + 2, radius=20, fill=self.inactive_color, outline=self.inactive_color)
|
227
517
|
|
228
518
|
self.load_icon()
|
229
519
|
self.font_style = font if font else ("Arial", 12) # Default font if not provided
|
@@ -238,7 +528,7 @@ class spacrButton(tk.Frame):
|
|
238
528
|
self.canvas.bind("<Leave>", self.on_leave)
|
239
529
|
self.canvas.bind("<Button-1>", self.on_click)
|
240
530
|
|
241
|
-
self.bg_color =
|
531
|
+
self.bg_color = self.inactive_color
|
242
532
|
self.active_color = color_settings['active_color']
|
243
533
|
self.fg_color = color_settings['fg_color']
|
244
534
|
self.is_zoomed_in = False # Track zoom state for smooth transitions
|
@@ -255,7 +545,7 @@ class spacrButton(tk.Frame):
|
|
255
545
|
icon_image = Image.open(self.get_icon_path("default"))
|
256
546
|
print(f'Icon not found: {icon_path}. Using default icon instead.')
|
257
547
|
|
258
|
-
initial_size = int(self.size * 0.
|
548
|
+
initial_size = int(self.size * 0.65) # 65% of button size initially
|
259
549
|
self.original_icon_image = icon_image.resize((initial_size, initial_size), Image.Resampling.LANCZOS)
|
260
550
|
self.icon_photo = ImageTk.PhotoImage(self.original_icon_image)
|
261
551
|
|
@@ -270,13 +560,13 @@ class spacrButton(tk.Frame):
|
|
270
560
|
self.canvas.itemconfig(self.button_bg, fill=self.active_color)
|
271
561
|
self.update_description(event)
|
272
562
|
if not self.is_zoomed_in:
|
273
|
-
self.animate_zoom(
|
563
|
+
self.animate_zoom(0.85) # Zoom in the icon to 85% of button size
|
274
564
|
|
275
565
|
def on_leave(self, event=None):
|
276
|
-
self.canvas.itemconfig(self.button_bg, fill=self.
|
566
|
+
self.canvas.itemconfig(self.button_bg, fill=self.inactive_color)
|
277
567
|
self.clear_description(event)
|
278
568
|
if self.is_zoomed_in:
|
279
|
-
self.animate_zoom(
|
569
|
+
self.animate_zoom(0.65) # Reset the icon size to 65% of button size
|
280
570
|
|
281
571
|
def on_click(self, event=None):
|
282
572
|
if self.command:
|
@@ -321,7 +611,7 @@ class spacrButton(tk.Frame):
|
|
321
611
|
parent = parent.master
|
322
612
|
|
323
613
|
def animate_zoom(self, target_scale, steps=10, delay=10):
|
324
|
-
current_scale =
|
614
|
+
current_scale = 0.85 if self.is_zoomed_in else 0.65
|
325
615
|
step_scale = (target_scale - current_scale) / steps
|
326
616
|
self._animate_step(current_scale, step_scale, steps, delay)
|
327
617
|
|
@@ -335,13 +625,13 @@ class spacrButton(tk.Frame):
|
|
335
625
|
|
336
626
|
def zoom_icon(self, scale_factor):
|
337
627
|
# Resize the original icon image
|
338
|
-
new_size = int(self.size *
|
628
|
+
new_size = int(self.size * scale_factor)
|
339
629
|
resized_icon = self.original_icon_image.resize((new_size, new_size), Image.Resampling.LANCZOS)
|
340
630
|
self.icon_photo = ImageTk.PhotoImage(resized_icon)
|
341
631
|
|
342
632
|
# Update the icon on the canvas
|
343
633
|
self.canvas.itemconfig(self.button_icon, image=self.icon_photo)
|
344
|
-
self.canvas.image = self.icon_photo
|
634
|
+
self.canvas.image = self.icon_photo
|
345
635
|
|
346
636
|
class spacrSwitch(ttk.Frame):
|
347
637
|
def __init__(self, parent, text="", variable=None, command=None, *args, **kwargs):
|
@@ -1296,14 +1586,19 @@ class ModifyMaskApp:
|
|
1296
1586
|
self.update_display()
|
1297
1587
|
|
1298
1588
|
class AnnotateApp:
|
1299
|
-
def __init__(self, root, db_path, src, image_type=None, channels=None,
|
1589
|
+
def __init__(self, root, db_path, src, image_type=None, channels=None, image_size=200, annotation_column='annotate', normalize=False, percentiles=(1, 99), measurement=None, threshold=None):
|
1300
1590
|
self.root = root
|
1301
1591
|
self.db_path = db_path
|
1302
1592
|
self.src = src
|
1303
1593
|
self.index = 0
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1594
|
+
|
1595
|
+
if isinstance(image_size, list):
|
1596
|
+
self.image_size = (int(image_size[0]), int(image_size[0]))
|
1597
|
+
elif isinstance(image_size, int):
|
1598
|
+
self.image_size = (image_size, image_size)
|
1599
|
+
else:
|
1600
|
+
raise ValueError("Invalid image size")
|
1601
|
+
|
1307
1602
|
self.annotation_column = annotation_column
|
1308
1603
|
self.image_type = image_type
|
1309
1604
|
self.channels = channels
|
@@ -1315,22 +1610,72 @@ class AnnotateApp:
|
|
1315
1610
|
self.adjusted_to_original_paths = {}
|
1316
1611
|
self.terminate = False
|
1317
1612
|
self.update_queue = Queue()
|
1318
|
-
self.status_label = Label(self.root, text="", font=("Arial", 12))
|
1319
|
-
self.status_label.grid(row=self.grid_rows + 1, column=0, columnspan=self.grid_cols)
|
1320
1613
|
self.measurement = measurement
|
1321
1614
|
self.threshold = threshold
|
1322
1615
|
|
1616
|
+
style_out = set_dark_style(ttk.Style())
|
1617
|
+
self.root.configure(bg=style_out['inactive_color'])
|
1618
|
+
|
1323
1619
|
self.filtered_paths_annotations = []
|
1324
1620
|
self.prefilter_paths_annotations()
|
1325
1621
|
|
1326
1622
|
self.db_update_thread = threading.Thread(target=self.update_database_worker)
|
1327
1623
|
self.db_update_thread.start()
|
1328
1624
|
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1625
|
+
# Set the initial window size and make it fit the screen size
|
1626
|
+
self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}")
|
1627
|
+
self.root.update_idletasks()
|
1628
|
+
|
1629
|
+
# Create the status label
|
1630
|
+
self.status_label = Label(root, text="", font=("Arial", 12), bg=self.root.cget('bg'))
|
1631
|
+
self.status_label.grid(row=2, column=0, padx=10, pady=10, sticky="w")
|
1632
|
+
|
1633
|
+
# Place the buttons at the bottom right
|
1634
|
+
self.button_frame = Frame(root, bg=self.root.cget('bg'))
|
1635
|
+
self.button_frame.grid(row=2, column=1, padx=10, pady=10, sticky="se")
|
1636
|
+
|
1637
|
+
self.next_button = Button(self.button_frame, text="Next", command=self.next_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
|
1638
|
+
self.next_button.pack(side="right", padx=5)
|
1639
|
+
|
1640
|
+
self.previous_button = Button(self.button_frame, text="Back", command=self.previous_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
|
1641
|
+
self.previous_button.pack(side="right", padx=5)
|
1642
|
+
|
1643
|
+
self.exit_button = Button(self.button_frame, text="Exit", command=self.shutdown, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
|
1644
|
+
self.exit_button.pack(side="right", padx=5)
|
1645
|
+
|
1646
|
+
# Calculate grid rows and columns based on the root window size and image size
|
1647
|
+
self.calculate_grid_dimensions()
|
1648
|
+
|
1649
|
+
# Create a frame to hold the image grid
|
1650
|
+
self.grid_frame = Frame(root, bg=self.root.cget('bg'))
|
1651
|
+
self.grid_frame.grid(row=0, column=0, columnspan=2, padx=0, pady=0, sticky="nsew")
|
1652
|
+
|
1653
|
+
for i in range(self.grid_rows * self.grid_cols):
|
1654
|
+
label = Label(self.grid_frame, bg=self.root.cget('bg'))
|
1655
|
+
label.grid(row=i // self.grid_cols, column=i % self.grid_cols, padx=2, pady=2, sticky="nsew")
|
1332
1656
|
self.labels.append(label)
|
1333
1657
|
|
1658
|
+
# Make the grid frame resize with the window
|
1659
|
+
self.root.grid_rowconfigure(0, weight=1)
|
1660
|
+
self.root.grid_columnconfigure(0, weight=1)
|
1661
|
+
self.root.grid_columnconfigure(1, weight=1)
|
1662
|
+
|
1663
|
+
for row in range(self.grid_rows):
|
1664
|
+
self.grid_frame.grid_rowconfigure(row, weight=1)
|
1665
|
+
for col in range(self.grid_cols):
|
1666
|
+
self.grid_frame.grid_columnconfigure(col, weight=1)
|
1667
|
+
|
1668
|
+
def calculate_grid_dimensions(self):
|
1669
|
+
window_width = self.root.winfo_width()
|
1670
|
+
window_height = self.root.winfo_height()
|
1671
|
+
|
1672
|
+
self.grid_cols = window_width // (self.image_size[0] + 4)
|
1673
|
+
self.grid_rows = (window_height - self.button_frame.winfo_height() - 4) // (self.image_size[1] + 4)
|
1674
|
+
|
1675
|
+
# Update to make sure grid_rows and grid_cols are at least 1
|
1676
|
+
self.grid_cols = max(1, self.grid_cols)
|
1677
|
+
self.grid_rows = max(1, self.grid_rows)
|
1678
|
+
|
1334
1679
|
def prefilter_paths_annotations(self):
|
1335
1680
|
from .io import _read_and_join_tables
|
1336
1681
|
from .utils import is_list_of_lists
|
@@ -1584,6 +1929,7 @@ class AnnotateApp:
|
|
1584
1929
|
self.update_queue.put(self.pending_updates.copy())
|
1585
1930
|
self.pending_updates.clear()
|
1586
1931
|
self.index += self.grid_rows * self.grid_cols
|
1932
|
+
self.prefilter_paths_annotations() # Re-fetch annotations from the database
|
1587
1933
|
self.load_images()
|
1588
1934
|
|
1589
1935
|
def previous_page(self):
|
@@ -1593,34 +1939,38 @@ class AnnotateApp:
|
|
1593
1939
|
self.index -= self.grid_rows * self.grid_cols
|
1594
1940
|
if self.index < 0:
|
1595
1941
|
self.index = 0
|
1942
|
+
self.prefilter_paths_annotations() # Re-fetch annotations from the database
|
1596
1943
|
self.load_images()
|
1597
1944
|
|
1598
1945
|
def shutdown(self):
|
1599
1946
|
self.terminate = True
|
1600
1947
|
self.update_queue.put(self.pending_updates.copy())
|
1601
|
-
self.pending_updates
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1948
|
+
if not self.pending_updates:
|
1949
|
+
self.pending_updates.clear()
|
1950
|
+
self.db_update_thread.join()
|
1951
|
+
self.root.quit()
|
1952
|
+
self.root.destroy()
|
1953
|
+
print(f'Quit application')
|
1954
|
+
else:
|
1955
|
+
print('Waiting for pending updates to finish before quitting')
|
1606
1956
|
|
1607
1957
|
def create_menu_bar(root):
|
1608
1958
|
from .gui import initiate_root
|
1609
1959
|
gui_apps = {
|
1610
|
-
"Mask": (lambda frame: initiate_root(frame, 'mask'), "Generate cellpose masks for cells, nuclei and pathogen images."),
|
1611
|
-
"Measure": (lambda frame: initiate_root(frame, 'measure'), "Measure single object intensity and morphological feature. Crop and save single object image"),
|
1612
|
-
"Annotate": (lambda frame: initiate_root(frame, 'annotate'), "Annotation single object images on a grid. Annotations are saved to database."),
|
1613
|
-
"Make Masks": (lambda frame: initiate_root(frame, 'make_masks'), "Adjust pre-existing Cellpose models to your specific dataset for improved performance"),
|
1614
|
-
"Classify": (lambda frame: initiate_root(frame, 'classify'), "Train Torch Convolutional Neural Networks (CNNs) or Transformers to classify single object images."),
|
1615
|
-
"Sequencing": (lambda frame: initiate_root(frame, 'sequencing'), "Analyze sequencing data."),
|
1616
|
-
"Umap": (lambda frame: initiate_root(frame, 'umap'), "Generate UMAP embeddings with datapoints represented as images."),
|
1617
|
-
"Train Cellpose": (lambda frame: initiate_root(frame, 'train_cellpose'), "Train custom Cellpose models."),
|
1618
|
-
"ML Analyze": (lambda frame: initiate_root(frame, 'ml_analyze'), "Machine learning analysis of data."),
|
1619
|
-
"Cellpose Masks": (lambda frame: initiate_root(frame, 'cellpose_masks'), "Generate Cellpose masks."),
|
1620
|
-
"Cellpose All": (lambda frame: initiate_root(frame, 'cellpose_all'), "Run Cellpose on all images."),
|
1621
|
-
"Map Barcodes": (lambda frame: initiate_root(frame, 'map_barcodes'), "Map barcodes to data."),
|
1622
|
-
"Regression": (lambda frame: initiate_root(frame, 'regression'), "Perform regression analysis."),
|
1623
|
-
"Recruitment": (lambda frame: initiate_root(frame, 'recruitment'), "Analyze recruitment data.")
|
1960
|
+
"Mask": (lambda frame: initiate_root(frame, settings_type='mask'), "Generate cellpose masks for cells, nuclei and pathogen images."),
|
1961
|
+
"Measure": (lambda frame: initiate_root(frame, settings_type='measure'), "Measure single object intensity and morphological feature. Crop and save single object image"),
|
1962
|
+
"Annotate": (lambda frame: initiate_root(frame, settings_type='annotate'), "Annotation single object images on a grid. Annotations are saved to database."),
|
1963
|
+
"Make Masks": (lambda frame: initiate_root(frame, settings_type='make_masks'), "Adjust pre-existing Cellpose models to your specific dataset for improved performance"),
|
1964
|
+
"Classify": (lambda frame: initiate_root(frame, settings_type='classify'), "Train Torch Convolutional Neural Networks (CNNs) or Transformers to classify single object images."),
|
1965
|
+
"Sequencing": (lambda frame: initiate_root(frame, settings_type='sequencing'), "Analyze sequencing data."),
|
1966
|
+
"Umap": (lambda frame: initiate_root(frame, settings_type='umap'), "Generate UMAP embeddings with datapoints represented as images."),
|
1967
|
+
"Train Cellpose": (lambda frame: initiate_root(frame, settings_type='train_cellpose'), "Train custom Cellpose models."),
|
1968
|
+
"ML Analyze": (lambda frame: initiate_root(frame, settings_type='ml_analyze'), "Machine learning analysis of data."),
|
1969
|
+
"Cellpose Masks": (lambda frame: initiate_root(frame, settings_type='cellpose_masks'), "Generate Cellpose masks."),
|
1970
|
+
"Cellpose All": (lambda frame: initiate_root(frame, settings_type='cellpose_all'), "Run Cellpose on all images."),
|
1971
|
+
"Map Barcodes": (lambda frame: initiate_root(frame, settings_type='map_barcodes'), "Map barcodes to data."),
|
1972
|
+
"Regression": (lambda frame: initiate_root(frame, settings_type='regression'), "Perform regression analysis."),
|
1973
|
+
"Recruitment": (lambda frame: initiate_root(frame, settings_type='recruitment'), "Analyze recruitment data.")
|
1624
1974
|
}
|
1625
1975
|
|
1626
1976
|
def load_app_wrapper(app_name, app_func):
|