spacr 0.2.3__py3-none-any.whl → 0.2.5__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 (61) hide show
  1. spacr/app_annotate.py +3 -4
  2. spacr/core.py +100 -282
  3. spacr/gui.py +20 -38
  4. spacr/gui_core.py +406 -499
  5. spacr/gui_elements.py +395 -60
  6. spacr/gui_utils.py +393 -73
  7. spacr/io.py +130 -50
  8. spacr/measure.py +199 -154
  9. spacr/plot.py +108 -42
  10. spacr/resources/font/open_sans/OFL.txt +93 -0
  11. spacr/resources/font/open_sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  12. spacr/resources/font/open_sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  13. spacr/resources/font/open_sans/README.txt +100 -0
  14. spacr/resources/font/open_sans/static/OpenSans-Bold.ttf +0 -0
  15. spacr/resources/font/open_sans/static/OpenSans-BoldItalic.ttf +0 -0
  16. spacr/resources/font/open_sans/static/OpenSans-ExtraBold.ttf +0 -0
  17. spacr/resources/font/open_sans/static/OpenSans-ExtraBoldItalic.ttf +0 -0
  18. spacr/resources/font/open_sans/static/OpenSans-Italic.ttf +0 -0
  19. spacr/resources/font/open_sans/static/OpenSans-Light.ttf +0 -0
  20. spacr/resources/font/open_sans/static/OpenSans-LightItalic.ttf +0 -0
  21. spacr/resources/font/open_sans/static/OpenSans-Medium.ttf +0 -0
  22. spacr/resources/font/open_sans/static/OpenSans-MediumItalic.ttf +0 -0
  23. spacr/resources/font/open_sans/static/OpenSans-Regular.ttf +0 -0
  24. spacr/resources/font/open_sans/static/OpenSans-SemiBold.ttf +0 -0
  25. spacr/resources/font/open_sans/static/OpenSans-SemiBoldItalic.ttf +0 -0
  26. spacr/resources/font/open_sans/static/OpenSans_Condensed-Bold.ttf +0 -0
  27. spacr/resources/font/open_sans/static/OpenSans_Condensed-BoldItalic.ttf +0 -0
  28. spacr/resources/font/open_sans/static/OpenSans_Condensed-ExtraBold.ttf +0 -0
  29. spacr/resources/font/open_sans/static/OpenSans_Condensed-ExtraBoldItalic.ttf +0 -0
  30. spacr/resources/font/open_sans/static/OpenSans_Condensed-Italic.ttf +0 -0
  31. spacr/resources/font/open_sans/static/OpenSans_Condensed-Light.ttf +0 -0
  32. spacr/resources/font/open_sans/static/OpenSans_Condensed-LightItalic.ttf +0 -0
  33. spacr/resources/font/open_sans/static/OpenSans_Condensed-Medium.ttf +0 -0
  34. spacr/resources/font/open_sans/static/OpenSans_Condensed-MediumItalic.ttf +0 -0
  35. spacr/resources/font/open_sans/static/OpenSans_Condensed-Regular.ttf +0 -0
  36. spacr/resources/font/open_sans/static/OpenSans_Condensed-SemiBold.ttf +0 -0
  37. spacr/resources/font/open_sans/static/OpenSans_Condensed-SemiBoldItalic.ttf +0 -0
  38. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Bold.ttf +0 -0
  39. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-BoldItalic.ttf +0 -0
  40. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-ExtraBold.ttf +0 -0
  41. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf +0 -0
  42. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Italic.ttf +0 -0
  43. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Light.ttf +0 -0
  44. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-LightItalic.ttf +0 -0
  45. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Medium.ttf +0 -0
  46. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-MediumItalic.ttf +0 -0
  47. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-Regular.ttf +0 -0
  48. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-SemiBold.ttf +0 -0
  49. spacr/resources/font/open_sans/static/OpenSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  50. spacr/resources/icons/logo.pdf +2786 -6
  51. spacr/resources/icons/logo_spacr.png +0 -0
  52. spacr/resources/icons/logo_spacr_1.png +0 -0
  53. spacr/settings.py +12 -87
  54. spacr/utils.py +45 -10
  55. {spacr-0.2.3.dist-info → spacr-0.2.5.dist-info}/METADATA +5 -1
  56. spacr-0.2.5.dist-info/RECORD +100 -0
  57. spacr-0.2.3.dist-info/RECORD +0 -58
  58. {spacr-0.2.3.dist-info → spacr-0.2.5.dist-info}/LICENSE +0 -0
  59. {spacr-0.2.3.dist-info → spacr-0.2.5.dist-info}/WHEEL +0 -0
  60. {spacr-0.2.3.dist-info → spacr-0.2.5.dist-info}/entry_points.txt +0 -0
  61. {spacr-0.2.3.dist-info → spacr-0.2.5.dist-info}/top_level.txt +0 -0
spacr/gui_elements.py CHANGED
@@ -1,9 +1,9 @@
1
- import os, threading, time, sqlite3
1
+ import os, threading, time, sqlite3, webbrowser
2
2
  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
@@ -17,7 +17,21 @@ from scipy.ndimage import binary_fill_holes, label
17
17
  from tkinter import ttk, scrolledtext
18
18
 
19
19
  def set_default_font(root, font_name="Arial", size=12):
20
- default_font = (font_name, size)
20
+
21
+ # Define default_font before the condition
22
+ default_font = ("Arial", size)
23
+
24
+ if font_name == 'OpenSans':
25
+ try:
26
+ font_path = os.path.join(os.path.dirname(__file__), 'resources/font/open_sans/static/OpenSans-Regular.ttf')
27
+ default_font = tkFont.Font(family=font_name, size=size, file=font_path)
28
+ except Exception as e:
29
+ print(f"Failed to load OpenSans font: {e}")
30
+ # Fallback to a default font in case of an error
31
+ default_font = ("Arial", size)
32
+ else:
33
+ default_font = (font_name, size)
34
+
21
35
  root.option_add("*Font", default_font)
22
36
  root.option_add("*TButton.Font", default_font)
23
37
  root.option_add("*TLabel.Font", default_font)
@@ -47,29 +61,11 @@ def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font
47
61
  style.configure('TEntry', padding=padding)
48
62
  style.configure('Spacr.TEntry', padding=padding)
49
63
  style.configure('Custom.TLabel', padding=padding)
50
- #style.configure('Spacr.TCheckbutton', padding=padding)
51
64
  style.configure('TButton', padding=padding)
52
-
53
65
  style.configure('TFrame', background=bg_color)
54
66
  style.configure('TPanedwindow', background=bg_color)
55
67
  style.configure('TLabel', background=bg_color, foreground=fg_color, font=font_style)
56
68
 
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'))
72
-
73
69
  if parent_frame:
74
70
  parent_frame.configure(bg=bg_color)
75
71
  parent_frame.grid_rowconfigure(0, weight=1)
@@ -99,8 +95,101 @@ def set_dark_style(style, parent_frame=None, containers=None, widgets=None, font
99
95
 
100
96
  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}
101
97
 
98
+ class spacrContainer(tk.Frame):
99
+ def __init__(self, parent, orient=tk.VERTICAL, bg=None, *args, **kwargs):
100
+ super().__init__(parent, *args, **kwargs)
101
+ self.orient = orient
102
+ self.bg = bg if bg else 'lightgrey'
103
+ self.sash_thickness = 10
104
+
105
+ self.panes = []
106
+ self.sashes = []
107
+ self.bind("<Configure>", self.on_configure)
108
+
109
+ self.grid_rowconfigure(0, weight=1)
110
+ self.grid_columnconfigure(0, weight=1)
111
+
112
+ def add(self, widget, stretch='always'):
113
+ print(f"Adding widget: {widget} with stretch: {stretch}")
114
+ pane = tk.Frame(self, bg=self.bg)
115
+ pane.grid_propagate(False)
116
+ widget.grid(in_=pane, sticky="nsew") # Use grid for the widget within the pane
117
+ self.panes.append((pane, widget))
118
+
119
+ if len(self.panes) > 1:
120
+ self.create_sash()
121
+
122
+ self.reposition_panes()
123
+
124
+ def create_sash(self):
125
+ sash = tk.Frame(self, bg=self.bg, cursor='sb_v_double_arrow' if self.orient == tk.VERTICAL else 'sb_h_double_arrow', height=self.sash_thickness, width=self.sash_thickness)
126
+ sash.bind("<Enter>", self.on_enter_sash)
127
+ sash.bind("<Leave>", self.on_leave_sash)
128
+ sash.bind("<ButtonPress-1>", self.start_resize)
129
+ self.sashes.append(sash)
130
+
131
+ def reposition_panes(self):
132
+ if not self.panes:
133
+ return
134
+
135
+ total_size = self.winfo_height() if self.orient == tk.VERTICAL else self.winfo_width()
136
+ pane_size = total_size // len(self.panes)
137
+
138
+ print(f"Total size: {total_size}, Pane size: {pane_size}, Number of panes: {len(self.panes)}")
139
+
140
+ for i, (pane, widget) in enumerate(self.panes):
141
+ if self.orient == tk.VERTICAL:
142
+ pane.grid(row=i * 2, column=0, sticky="nsew", pady=(0, self.sash_thickness if i < len(self.panes) - 1 else 0))
143
+ else:
144
+ pane.grid(row=0, column=i * 2, sticky="nsew", padx=(0, self.sash_thickness if i < len(self.panes) - 1 else 0))
145
+
146
+ for i, sash in enumerate(self.sashes):
147
+ if self.orient == tk.VERTICAL:
148
+ sash.grid(row=(i * 2) + 1, column=0, sticky="ew")
149
+ else:
150
+ sash.grid(row=0, column=(i * 2) + 1, sticky="ns")
151
+
152
+ def on_configure(self, event):
153
+ print(f"Configuring container: {self}")
154
+ self.reposition_panes()
155
+
156
+ def on_enter_sash(self, event):
157
+ event.widget.config(bg='blue')
158
+
159
+ def on_leave_sash(self, event):
160
+ event.widget.config(bg=self.bg)
161
+
162
+ def start_resize(self, event):
163
+ sash = event.widget
164
+ self.start_pos = event.y_root if self.orient == tk.VERTICAL else event.x_root
165
+ self.start_size = sash.winfo_y() if self.orient == tk.VERTICAL else sash.winfo_x()
166
+ sash.bind("<B1-Motion>", self.perform_resize)
167
+
168
+ def perform_resize(self, event):
169
+ sash = event.widget
170
+ delta = (event.y_root - self.start_pos) if self.orient == tk.VERTICAL else (event.x_root - self.start_pos)
171
+ new_size = self.start_size + delta
172
+
173
+ for i, (pane, widget) in enumerate(self.panes):
174
+ if self.orient == tk.VERTICAL:
175
+ new_row = max(0, new_size // self.sash_thickness)
176
+ if pane.winfo_y() >= new_size:
177
+ pane.grid_configure(row=new_row)
178
+ elif pane.winfo_y() < new_size and i > 0:
179
+ previous_row = max(0, (new_size - pane.winfo_height()) // self.sash_thickness)
180
+ self.panes[i - 1][0].grid_configure(row=previous_row)
181
+ else:
182
+ new_col = max(0, new_size // self.sash_thickness)
183
+ if pane.winfo_x() >= new_size:
184
+ pane.grid_configure(column=new_col)
185
+ elif pane.winfo_x() < new_size and i > 0:
186
+ previous_col = max(0, (new_size - pane.winfo_width()) // self.sash_thickness)
187
+ self.panes[i - 1][0].grid_configure(column=previous_col)
188
+
189
+ self.reposition_panes()
190
+
102
191
  class spacrEntry(tk.Frame):
103
- def __init__(self, parent, textvariable=None, outline=False, *args, **kwargs):
192
+ def __init__(self, parent, textvariable=None, outline=False, width=None, *args, **kwargs):
104
193
  super().__init__(parent, *args, **kwargs)
105
194
 
106
195
  # Set dark style
@@ -116,13 +205,17 @@ class spacrEntry(tk.Frame):
116
205
  self.configure(bg=style_out['bg_color'])
117
206
 
118
207
  # Create a canvas for the rounded rectangle background
119
- self.canvas_width = 220 # Adjusted for padding
208
+ if width is None:
209
+ self.canvas_width = 220 # Adjusted for padding
210
+ else:
211
+ self.canvas_width = width
120
212
  self.canvas_height = 40 # Adjusted for padding
121
213
  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
214
  self.canvas.pack()
123
215
 
216
+ # Create the entry widget
124
217
  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
218
+ self.entry.place(relx=0.5, rely=0.5, anchor=tk.CENTER, width=self.canvas_width - 30, height=20) # Centered positioning
126
219
 
127
220
  # Bind events to change the background color on focus
128
221
  self.entry.bind("<FocusIn>", self.on_focus_in)
@@ -133,7 +226,7 @@ class spacrEntry(tk.Frame):
133
226
  def draw_rounded_rectangle(self, color):
134
227
  radius = 15 # Increased radius for more rounded corners
135
228
  x0, y0 = 10, 5
136
- x1, y1 = 210, 35
229
+ x1, y1 = self.canvas_width - 10, self.canvas_height - 5
137
230
  self.canvas.delete("all")
138
231
  self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
139
232
  self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
@@ -201,7 +294,7 @@ class spacrCheck(tk.Frame):
201
294
  self.variable.set(not self.variable.get())
202
295
 
203
296
  class spacrCombo(tk.Frame):
204
- def __init__(self, parent, textvariable=None, values=None, *args, **kwargs):
297
+ def __init__(self, parent, textvariable=None, values=None, width=None, *args, **kwargs):
205
298
  super().__init__(parent, *args, **kwargs)
206
299
 
207
300
  # Set dark style
@@ -216,7 +309,7 @@ class spacrCombo(tk.Frame):
216
309
  self.values = values or []
217
310
 
218
311
  # Create a canvas for the rounded rectangle background
219
- self.canvas_width = 220 # Adjusted for padding
312
+ self.canvas_width = width if width is not None else 220 # Adjusted for padding
220
313
  self.canvas_height = 40 # Adjusted for padding
221
314
  self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg=self.bg_color)
222
315
  self.canvas.pack()
@@ -239,7 +332,7 @@ class spacrCombo(tk.Frame):
239
332
  def draw_rounded_rectangle(self, color):
240
333
  radius = 15 # Increased radius for more rounded corners
241
334
  x0, y0 = 10, 5
242
- x1, y1 = 210, 35
335
+ x1, y1 = self.canvas_width - 10, self.canvas_height - 5
243
336
  self.canvas.delete("all")
244
337
  self.canvas.create_arc((x0, y0, x0 + radius, y0 + radius), start=90, extent=90, fill=color, outline=color)
245
338
  self.canvas.create_arc((x1 - radius, y0, x1, y0 + radius), start=0, extent=90, fill=color, outline=color)
@@ -305,6 +398,73 @@ class spacrDropdownMenu(tk.OptionMenu):
305
398
  # Create custom button
306
399
  self.create_custom_button()
307
400
 
401
+ def create_custom_button(self):
402
+ self.canvas_width = self.winfo_reqwidth() + 20 # Adjust width based on required size
403
+ self.canvas_height = 40 # Adjust the height as needed
404
+ self.canvas = tk.Canvas(self, width=self.canvas_width, height=self.canvas_height, bd=0, highlightthickness=0, relief='ridge', bg='#2B2B2B')
405
+ self.canvas.pack()
406
+ self.label = tk.Label(self.canvas, text="Settings Category", bg='#2B2B2B', fg='#ffffff', font=('Arial', 12))
407
+ self.label.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
408
+ self.draw_rounded_rectangle('#2B2B2B')
409
+
410
+ # Bind the click event to open the dropdown menu
411
+ self.canvas.bind("<Button-1>", self.on_click)
412
+ self.label.bind("<Button-1>", self.on_click)
413
+
414
+ def draw_rounded_rectangle(self, color):
415
+ radius = 15
416
+ x0, y0 = 10, 5
417
+ x1, y1 = self.canvas_width - 10, self.canvas_height - 5 # Adjust based on canvas size
418
+
419
+ self.canvas.delete("all")
420
+
421
+ # Create the rounded rectangle
422
+ self.canvas.create_arc(x0, y0, x0 + 2 * radius, y0 + 2 * radius, start=90, extent=90, fill=color, outline=color)
423
+ self.canvas.create_arc(x1 - 2 * radius, y0, x1, y0 + 2 * radius, start=0, extent=90, fill=color, outline=color)
424
+ self.canvas.create_arc(x0, y1 - 2 * radius, x0 + 2 * radius, y1, start=180, extent=90, fill=color, outline=color)
425
+ self.canvas.create_arc(x1 - 2 * radius, y1 - 2 * radius, x1, y1, start=270, extent=90, fill=color, outline=color)
426
+
427
+ self.canvas.create_rectangle(x0 + radius, y0, x1 - radius, y1, fill=color, outline=color)
428
+ self.canvas.create_rectangle(x0, y0 + radius, x1, y1 - radius, fill=color, outline=color)
429
+
430
+ self.label.config(bg=color) # Update label background to match rectangle color
431
+
432
+ def on_click(self, event):
433
+ self.post_menu()
434
+
435
+ def post_menu(self):
436
+ x, y, width, height = self.winfo_rootx(), self.winfo_rooty(), self.winfo_width(), self.winfo_height()
437
+ self.menu.post(x, y + height)
438
+
439
+ def update_styles(self, active_categories=None):
440
+ style = ttk.Style()
441
+ style_out = set_dark_style(style, widgets=[self])
442
+ self.menu = self['menu']
443
+ style_out = set_dark_style(style, widgets=[self.menu])
444
+
445
+ if active_categories is not None:
446
+ for idx in range(self.menu.index("end") + 1):
447
+ option = self.menu.entrycget(idx, "label")
448
+ if option in active_categories:
449
+ self.menu.entryconfig(idx, background=style_out['active_color'], foreground=style_out['fg_color'])
450
+ else:
451
+ self.menu.entryconfig(idx, background=style_out['bg_color'], foreground=style_out['fg_color'])
452
+
453
+
454
+
455
+ class spacrDropdownMenu_v1(tk.OptionMenu):
456
+ def __init__(self, parent, variable, options, command=None, **kwargs):
457
+ self.variable = variable
458
+ self.variable.set("Settings Category")
459
+ super().__init__(parent, self.variable, *options, command=command, **kwargs)
460
+ self.update_styles()
461
+
462
+ # Hide the original button
463
+ self.configure(highlightthickness=0, relief='flat', bg='#2B2B2B', fg='#2B2B2B')
464
+
465
+ # Create custom button
466
+ self.create_custom_button()
467
+
308
468
  def create_custom_button(self):
309
469
  self.canvas_width = self.winfo_reqwidth() # Use the required width of the widget
310
470
  self.canvas_height = 40 # Adjust the height as needed
@@ -363,14 +523,16 @@ class spacrCheckbutton(ttk.Checkbutton):
363
523
  _ = set_dark_style(style, widgets=[self])
364
524
 
365
525
  class spacrProgressBar(ttk.Progressbar):
366
- def __init__(self, parent, *args, **kwargs):
526
+ def __init__(self, parent, label=True, *args, **kwargs):
367
527
  super().__init__(parent, *args, **kwargs)
368
528
 
369
529
  # Get the style colors
370
530
  style_out = set_dark_style(ttk.Style())
371
- self.inactive_color = style_out['inactive_color']
531
+
532
+ self.fg_color = style_out['fg_color']
372
533
  self.bg_color = style_out['bg_color']
373
534
  self.active_color = style_out['active_color']
535
+ self.inactive_color = style_out['inactive_color']
374
536
 
375
537
  # Configure the style for the progress bar
376
538
  self.style = ttk.Style()
@@ -387,44 +549,155 @@ class spacrProgressBar(ttk.Progressbar):
387
549
  # Set initial value to 0
388
550
  self['value'] = 0
389
551
 
552
+ # Track whether to show the progress label
553
+ self.label = label
554
+
555
+ # Create the progress label (defer placement)
556
+ if self.label:
557
+ self.progress_label = tk.Label(parent, text="Processing: 0/0", anchor='w', justify='left', bg=self.inactive_color, fg=self.fg_color)
558
+ self.progress_label.grid_forget() # Temporarily hide it
559
+
560
+ # Initialize attributes for time and operation
561
+ self.operation_type = None
562
+ self.time_image = None
563
+ self.time_batch = None
564
+ self.time_left = None
565
+
566
+ def set_label_position(self):
567
+ if self.label and self.progress_label:
568
+ row_info = self.grid_info().get('row', 0)
569
+ col_info = self.grid_info().get('column', 0)
570
+ col_span = self.grid_info().get('columnspan', 1)
571
+ self.progress_label.grid(row=row_info + 1, column=col_info, columnspan=col_span, pady=5, padx=5, sticky='ew')
572
+
573
+ def update_label(self):
574
+ if self.label and self.progress_label:
575
+ # Update the progress label with current progress and additional info
576
+ label_text = f"Processing: {self['value']}/{self['maximum']}"
577
+ if self.operation_type:
578
+ label_text += f", {self.operation_type}"
579
+ if self.time_image:
580
+ label_text += f", Time/image: {self.time_image:.3f} sec"
581
+ if self.time_batch:
582
+ label_text += f", Time/batch: {self.time_batch:.3f} sec"
583
+ if self.time_left:
584
+ label_text += f", Time_left: {self.time_left:.3f} min"
585
+ self.progress_label.config(text=label_text)
586
+
587
+ def spacrScrollbarStyle(style, inactive_color, active_color):
588
+ # Check if custom elements already exist to avoid duplication
589
+ if not style.element_names().count('custom.Vertical.Scrollbar.trough'):
590
+ style.element_create('custom.Vertical.Scrollbar.trough', 'from', 'clam')
591
+ if not style.element_names().count('custom.Vertical.Scrollbar.thumb'):
592
+ style.element_create('custom.Vertical.Scrollbar.thumb', 'from', 'clam')
593
+
594
+ style.layout('Custom.Vertical.TScrollbar',
595
+ [('Vertical.Scrollbar.trough', {'children': [('Vertical.Scrollbar.thumb', {'expand': '1', 'sticky': 'nswe'})], 'sticky': 'ns'})])
596
+
597
+ style.configure('Custom.Vertical.TScrollbar',
598
+ background=inactive_color,
599
+ troughcolor=inactive_color,
600
+ bordercolor=inactive_color,
601
+ lightcolor=inactive_color,
602
+ darkcolor=inactive_color)
603
+
604
+ style.map('Custom.Vertical.TScrollbar',
605
+ background=[('!active', inactive_color), ('active', active_color)],
606
+ troughcolor=[('!active', inactive_color), ('active', inactive_color)],
607
+ bordercolor=[('!active', inactive_color), ('active', inactive_color)],
608
+ lightcolor=[('!active', inactive_color), ('active', active_color)],
609
+ darkcolor=[('!active', inactive_color), ('active', active_color)])
610
+
390
611
  class spacrFrame(ttk.Frame):
391
- def __init__(self, container, width=None, *args, bg='black', **kwargs):
612
+ def __init__(self, container, width=None, *args, bg='black', radius=20, scrollbar=True, textbox=False, **kwargs):
392
613
  super().__init__(container, *args, **kwargs)
393
614
  self.configure(style='TFrame')
394
615
  if width is None:
395
616
  screen_width = self.winfo_screenwidth()
396
617
  width = screen_width // 4
397
- canvas = tk.Canvas(self, bg=bg, width=width)
398
- scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
618
+
619
+ # Create the canvas
620
+ canvas = tk.Canvas(self, bg=bg, width=width, highlightthickness=0)
621
+ self.rounded_rectangle(canvas, 0, 0, width, self.winfo_screenheight(), radius, fill=bg)
622
+
623
+ # Define scrollbar styles
624
+ style_out = set_dark_style(ttk.Style())
625
+ self.inactive_color = style_out['inactive_color']
626
+ self.active_color = style_out['active_color']
627
+ self.fg_color = style_out['fg_color'] # Foreground color for text
628
+
629
+ # Set custom scrollbar style
630
+ style = ttk.Style()
631
+ spacrScrollbarStyle(style, self.inactive_color, self.active_color)
632
+
633
+ # Create scrollbar with custom style if scrollbar option is True
634
+ if scrollbar:
635
+ scrollbar_widget = ttk.Scrollbar(self, orient="vertical", command=canvas.yview, style='Custom.Vertical.TScrollbar')
636
+
637
+ if textbox:
638
+ self.scrollable_frame = tk.Text(canvas, bg=bg, fg=self.fg_color, wrap=tk.WORD)
639
+ else:
640
+ self.scrollable_frame = ttk.Frame(canvas, style='TFrame')
399
641
 
400
- self.scrollable_frame = ttk.Frame(canvas, style='TFrame')
401
642
  self.scrollable_frame.bind(
402
643
  "<Configure>",
403
644
  lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
404
645
  )
405
646
  canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
406
- canvas.configure(yscrollcommand=scrollbar.set)
647
+ if scrollbar:
648
+ canvas.configure(yscrollcommand=scrollbar_widget.set)
407
649
 
408
650
  canvas.grid(row=0, column=0, sticky="nsew")
409
- scrollbar.grid(row=0, column=1, sticky="ns")
651
+ if scrollbar:
652
+ scrollbar_widget.grid(row=0, column=1, sticky="ns")
410
653
 
411
654
  self.grid_rowconfigure(0, weight=1)
412
655
  self.grid_columnconfigure(0, weight=1)
413
- self.grid_columnconfigure(1, weight=0)
656
+ if scrollbar:
657
+ self.grid_columnconfigure(1, weight=0)
414
658
 
415
- style = ttk.Style()
416
- _ = set_dark_style(style, containers=[self], widgets=[canvas, scrollbar, self.scrollable_frame])
659
+ _ = set_dark_style(style, containers=[self], widgets=[canvas, self.scrollable_frame])
660
+ if scrollbar:
661
+ _ = set_dark_style(style, widgets=[scrollbar_widget])
662
+
663
+ def rounded_rectangle(self, canvas, x1, y1, x2, y2, radius=20, **kwargs):
664
+ points = [
665
+ x1 + radius, y1,
666
+ x2 - radius, y1,
667
+ x2 - radius, y1,
668
+ x2, y1,
669
+ x2, y1 + radius,
670
+ x2, y2 - radius,
671
+ x2, y2 - radius,
672
+ x2, y2,
673
+ x2 - radius, y2,
674
+ x1 + radius, y2,
675
+ x1 + radius, y2,
676
+ x1, y2,
677
+ x1, y2 - radius,
678
+ x1, y2 - radius,
679
+ x1, y1 + radius,
680
+ x1, y1 + radius,
681
+ x1, y1
682
+ ]
683
+ return canvas.create_polygon(points, **kwargs, smooth=True)
417
684
 
418
685
  class spacrLabel(tk.Frame):
419
- def __init__(self, parent, text="", font=None, style=None, align="right", **kwargs):
686
+ def __init__(self, parent, text="", font=None, style=None, align="right", height=None, **kwargs):
420
687
  valid_kwargs = {k: v for k, v in kwargs.items() if k not in ['foreground', 'background', 'font', 'anchor', 'justify', 'wraplength']}
421
688
  super().__init__(parent, **valid_kwargs)
422
689
 
423
690
  self.text = text
424
691
  self.align = align
425
- screen_height = self.winfo_screenheight()
426
- label_height = screen_height // 50
427
- label_width = label_height * 10
692
+
693
+ if height is None:
694
+ screen_height = self.winfo_screenheight()
695
+ label_height = screen_height // 50
696
+ label_width = label_height * 10
697
+ else:
698
+ label_height = height
699
+ label_width = label_height * 10
700
+
428
701
  style_out = set_dark_style(ttk.Style())
429
702
 
430
703
  self.canvas = tk.Canvas(self, width=label_width, height=label_height, highlightthickness=0, bg=style_out['bg_color'])
@@ -459,7 +732,7 @@ class spacrLabel(tk.Frame):
459
732
  self.canvas.itemconfig(self.label_text, text=text)
460
733
 
461
734
  class spacrButton(tk.Frame):
462
- def __init__(self, parent, text="", command=None, font=None, icon_name=None, size=50, show_text=True, outline=False, *args, **kwargs):
735
+ def __init__(self, parent, text="", command=None, font=None, icon_name=None, size=50, show_text=True, outline=False, animation=True, *args, **kwargs):
463
736
  super().__init__(parent, *args, **kwargs)
464
737
 
465
738
  self.text = text.capitalize() # Capitalize only the first letter of the text
@@ -468,6 +741,7 @@ class spacrButton(tk.Frame):
468
741
  self.size = size
469
742
  self.show_text = show_text
470
743
  self.outline = outline
744
+ self.animation = animation # Add animation attribute
471
745
 
472
746
  style_out = set_dark_style(ttk.Style())
473
747
 
@@ -534,13 +808,13 @@ class spacrButton(tk.Frame):
534
808
  def on_enter(self, event=None):
535
809
  self.canvas.itemconfig(self.button_bg, fill=self.active_color)
536
810
  self.update_description(event)
537
- if not self.is_zoomed_in:
811
+ if self.animation and not self.is_zoomed_in:
538
812
  self.animate_zoom(0.85) # Zoom in the icon to 85% of button size
539
813
 
540
814
  def on_leave(self, event=None):
541
815
  self.canvas.itemconfig(self.button_bg, fill=self.inactive_color)
542
816
  self.clear_description(event)
543
- if self.is_zoomed_in:
817
+ if self.animation and self.is_zoomed_in:
544
818
  self.animate_zoom(0.65) # Reset the icon size to 65% of button size
545
819
 
546
820
  def on_click(self, event=None):
@@ -1561,14 +1835,19 @@ class ModifyMaskApp:
1561
1835
  self.update_display()
1562
1836
 
1563
1837
  class AnnotateApp:
1564
- def __init__(self, root, db_path, src, image_type=None, channels=None, grid_rows=None, grid_cols=None, image_size=(200, 200), annotation_column='annotate', normalize=False, percentiles=(1,99), measurement=None, threshold=None):
1838
+ 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):
1565
1839
  self.root = root
1566
1840
  self.db_path = db_path
1567
1841
  self.src = src
1568
1842
  self.index = 0
1569
- self.grid_rows = grid_rows
1570
- self.grid_cols = grid_cols
1571
- self.image_size = image_size
1843
+
1844
+ if isinstance(image_size, list):
1845
+ self.image_size = (int(image_size[0]), int(image_size[0]))
1846
+ elif isinstance(image_size, int):
1847
+ self.image_size = (image_size, image_size)
1848
+ else:
1849
+ raise ValueError("Invalid image size")
1850
+
1572
1851
  self.annotation_column = annotation_column
1573
1852
  self.image_type = image_type
1574
1853
  self.channels = channels
@@ -1580,22 +1859,72 @@ class AnnotateApp:
1580
1859
  self.adjusted_to_original_paths = {}
1581
1860
  self.terminate = False
1582
1861
  self.update_queue = Queue()
1583
- self.status_label = Label(self.root, text="", font=("Arial", 12))
1584
- self.status_label.grid(row=self.grid_rows + 1, column=0, columnspan=self.grid_cols)
1585
1862
  self.measurement = measurement
1586
1863
  self.threshold = threshold
1587
1864
 
1865
+ style_out = set_dark_style(ttk.Style())
1866
+ self.root.configure(bg=style_out['inactive_color'])
1867
+
1588
1868
  self.filtered_paths_annotations = []
1589
1869
  self.prefilter_paths_annotations()
1590
1870
 
1591
1871
  self.db_update_thread = threading.Thread(target=self.update_database_worker)
1592
1872
  self.db_update_thread.start()
1593
1873
 
1594
- for i in range(grid_rows * grid_cols):
1595
- label = Label(root)
1596
- label.grid(row=i // grid_cols, column=i % grid_cols)
1874
+ # Set the initial window size and make it fit the screen size
1875
+ self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}")
1876
+ self.root.update_idletasks()
1877
+
1878
+ # Create the status label
1879
+ self.status_label = Label(root, text="", font=("Arial", 12), bg=self.root.cget('bg'))
1880
+ self.status_label.grid(row=2, column=0, padx=10, pady=10, sticky="w")
1881
+
1882
+ # Place the buttons at the bottom right
1883
+ self.button_frame = Frame(root, bg=self.root.cget('bg'))
1884
+ self.button_frame.grid(row=2, column=1, padx=10, pady=10, sticky="se")
1885
+
1886
+ self.next_button = Button(self.button_frame, text="Next", command=self.next_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
1887
+ self.next_button.pack(side="right", padx=5)
1888
+
1889
+ self.previous_button = Button(self.button_frame, text="Back", command=self.previous_page, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
1890
+ self.previous_button.pack(side="right", padx=5)
1891
+
1892
+ self.exit_button = Button(self.button_frame, text="Exit", command=self.shutdown, bg='black', fg='white', highlightbackground='white', highlightcolor='white', highlightthickness=1)
1893
+ self.exit_button.pack(side="right", padx=5)
1894
+
1895
+ # Calculate grid rows and columns based on the root window size and image size
1896
+ self.calculate_grid_dimensions()
1897
+
1898
+ # Create a frame to hold the image grid
1899
+ self.grid_frame = Frame(root, bg=self.root.cget('bg'))
1900
+ self.grid_frame.grid(row=0, column=0, columnspan=2, padx=0, pady=0, sticky="nsew")
1901
+
1902
+ for i in range(self.grid_rows * self.grid_cols):
1903
+ label = Label(self.grid_frame, bg=self.root.cget('bg'))
1904
+ label.grid(row=i // self.grid_cols, column=i % self.grid_cols, padx=2, pady=2, sticky="nsew")
1597
1905
  self.labels.append(label)
1598
1906
 
1907
+ # Make the grid frame resize with the window
1908
+ self.root.grid_rowconfigure(0, weight=1)
1909
+ self.root.grid_columnconfigure(0, weight=1)
1910
+ self.root.grid_columnconfigure(1, weight=1)
1911
+
1912
+ for row in range(self.grid_rows):
1913
+ self.grid_frame.grid_rowconfigure(row, weight=1)
1914
+ for col in range(self.grid_cols):
1915
+ self.grid_frame.grid_columnconfigure(col, weight=1)
1916
+
1917
+ def calculate_grid_dimensions(self):
1918
+ window_width = self.root.winfo_width()
1919
+ window_height = self.root.winfo_height()
1920
+
1921
+ self.grid_cols = window_width // (self.image_size[0] + 4)
1922
+ self.grid_rows = (window_height - self.button_frame.winfo_height() - 4) // (self.image_size[1] + 4)
1923
+
1924
+ # Update to make sure grid_rows and grid_cols are at least 1
1925
+ self.grid_cols = max(1, self.grid_cols)
1926
+ self.grid_rows = max(1, self.grid_rows)
1927
+
1599
1928
  def prefilter_paths_annotations(self):
1600
1929
  from .io import _read_and_join_tables
1601
1930
  from .utils import is_list_of_lists
@@ -1849,6 +2178,7 @@ class AnnotateApp:
1849
2178
  self.update_queue.put(self.pending_updates.copy())
1850
2179
  self.pending_updates.clear()
1851
2180
  self.index += self.grid_rows * self.grid_cols
2181
+ self.prefilter_paths_annotations() # Re-fetch annotations from the database
1852
2182
  self.load_images()
1853
2183
 
1854
2184
  def previous_page(self):
@@ -1858,16 +2188,20 @@ class AnnotateApp:
1858
2188
  self.index -= self.grid_rows * self.grid_cols
1859
2189
  if self.index < 0:
1860
2190
  self.index = 0
2191
+ self.prefilter_paths_annotations() # Re-fetch annotations from the database
1861
2192
  self.load_images()
1862
2193
 
1863
2194
  def shutdown(self):
1864
2195
  self.terminate = True
1865
2196
  self.update_queue.put(self.pending_updates.copy())
1866
- self.pending_updates.clear()
1867
- self.db_update_thread.join()
1868
- self.root.quit()
1869
- self.root.destroy()
1870
- print(f'Quit application')
2197
+ if not self.pending_updates:
2198
+ self.pending_updates.clear()
2199
+ self.db_update_thread.join()
2200
+ self.root.quit()
2201
+ self.root.destroy()
2202
+ print(f'Quit application')
2203
+ else:
2204
+ print('Waiting for pending updates to finish before quitting')
1871
2205
 
1872
2206
  def create_menu_bar(root):
1873
2207
  from .gui import initiate_root
@@ -1905,6 +2239,7 @@ def create_menu_bar(root):
1905
2239
 
1906
2240
  # Add a separator and an exit option
1907
2241
  app_menu.add_separator()
2242
+ app_menu.add_command(label="Help", command=lambda: webbrowser.open("https://readthedocs.org/projects/spacr/badge/?version=latest"))
1908
2243
  app_menu.add_command(label="Exit", command=root.quit)
1909
2244
 
1910
2245
  # Configure the menu for the root window