teareduce 0.5.3__py3-none-any.whl → 0.5.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.
@@ -15,42 +15,35 @@
15
15
  # when reading user input.
16
16
  lacosmic_default_dict = {
17
17
  # L.A.Cosmic parameters for run 1
18
- 'run1_gain': {'value': 1.0, 'type': float, 'positive': True},
19
- 'run1_readnoise': {'value': 6.5, 'type': float, 'positive': True},
20
- 'run1_sigclip': {'value': 5.0, 'type': float, 'positive': True},
21
- 'run1_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
22
- 'run1_objlim': {'value': 5.0, 'type': float, 'positive': True},
23
- 'run1_niter': {'value': 4, 'type': int, 'positive': True},
24
- 'run1_verbose': {'value': False, 'type': bool},
18
+ "run1_gain": {"value": 1.0, "type": float, "positive": True},
19
+ "run1_readnoise": {"value": 6.5, "type": float, "positive": True},
20
+ "run1_sigclip": {"value": 5.0, "type": float, "positive": True},
21
+ "run1_sigfrac": {"value": 0.3, "type": float, "positive": True},
22
+ "run1_objlim": {"value": 5.0, "type": float, "positive": True},
23
+ "run1_niter": {"value": 4, "type": int, "positive": True},
24
+ "run1_verbose": {"value": True, "type": bool},
25
25
  # L.A.Cosmic parameters for run 2
26
- 'run2_gain': {'value': 1.0, 'type': float, 'positive': True},
27
- 'run2_readnoise': {'value': 6.5, 'type': float, 'positive': True},
28
- 'run2_sigclip': {'value': 3.0, 'type': float, 'positive': True},
29
- 'run2_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
30
- 'run2_objlim': {'value': 5.0, 'type': float, 'positive': True},
31
- 'run2_niter': {'value': 4, 'type': int, 'positive': True},
32
- 'run2_verbose': {'value': False, 'type': bool},
26
+ "run2_gain": {"value": 1.0, "type": float, "positive": True},
27
+ "run2_readnoise": {"value": 6.5, "type": float, "positive": True},
28
+ "run2_sigclip": {"value": 3.0, "type": float, "positive": True},
29
+ "run2_sigfrac": {"value": 0.3, "type": float, "positive": True},
30
+ "run2_objlim": {"value": 5.0, "type": float, "positive": True},
31
+ "run2_niter": {"value": 4, "type": int, "positive": True},
32
+ "run2_verbose": {"value": True, "type": bool},
33
33
  # Dilation of the mask
34
- 'dilation': {'value': 0, 'type': int, 'positive': True},
34
+ "dilation": {"value": 0, "type": int, "positive": True},
35
+ "borderpadd": {"value": 10, "type": int, "positive": True},
35
36
  # Limits for the image section to process (pixels start at 1)
36
- 'xmin': {'value': 1, 'type': int, 'positive': True},
37
- 'xmax': {'value': None, 'type': int, 'positive': True},
38
- 'ymin': {'value': 1, 'type': int, 'positive': True},
39
- 'ymax': {'value': None, 'type': int, 'positive': True},
37
+ "xmin": {"value": 1, "type": int, "positive": True},
38
+ "xmax": {"value": None, "type": int, "positive": True},
39
+ "ymin": {"value": 1, "type": int, "positive": True},
40
+ "ymax": {"value": None, "type": int, "positive": True},
40
41
  # Number of runs to execute L.A.Cosmic
41
- 'nruns': {'value': 1, 'type': int, 'positive': True}
42
+ "nruns": {"value": 1, "type": int, "positive": True},
42
43
  }
43
44
 
44
45
  # Default parameters for cleaning methods
45
- VALID_CLEANING_METHODS = [
46
- 'x interp.',
47
- 'y interp.',
48
- 'surface interp.',
49
- 'median',
50
- 'mean',
51
- 'lacosmic',
52
- 'auxdata'
53
- ]
46
+ VALID_CLEANING_METHODS = ["x interp.", "y interp.", "surface interp.", "median", "mean", "lacosmic", "auxdata"]
54
47
 
55
48
  # Maximum pixel distance to consider when finding closest CR pixel
56
49
  MAX_PIXEL_DISTANCE_TO_CR = 15
@@ -60,3 +53,11 @@ DEFAULT_NPOINTS_INTERP = 2
60
53
 
61
54
  # Default degree for interpolation
62
55
  DEFAULT_DEGREE_INTERP = 1
56
+
57
+ # Default Tk window size
58
+ DEFAULT_TK_WINDOW_SIZE_X = 800
59
+ DEFAULT_TK_WINDOW_SIZE_Y = 700
60
+
61
+ # Default font settings
62
+ DEFAULT_FONT_FAMILY = "Helvetica"
63
+ DEFAULT_FONT_SIZE = 14
@@ -60,6 +60,7 @@ class ImageDisplay:
60
60
  the displayed region can be determined from either the axes limits or a
61
61
  predefined region attribute.
62
62
  """
63
+
63
64
  def set_vmin(self):
64
65
  """Prompt user to set a new minimum display value (vmin)."""
65
66
  old_vmin = self.get_vmin()
@@ -72,7 +73,7 @@ class ImageDisplay:
72
73
  return
73
74
  self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
74
75
  self.image.set_clim(vmin=new_vmin)
75
- if hasattr(self, 'image_aux'):
76
+ if hasattr(self, "image_aux"):
76
77
  self.image_aux.set_clim(vmin=new_vmin)
77
78
  self.canvas.draw_idle()
78
79
 
@@ -88,7 +89,7 @@ class ImageDisplay:
88
89
  return
89
90
  self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
90
91
  self.image.set_clim(vmax=new_vmax)
91
- if hasattr(self, 'image_aux'):
92
+ if hasattr(self, "image_aux"):
92
93
  self.image_aux.set_clim(vmax=new_vmax)
93
94
  self.canvas.draw_idle()
94
95
 
@@ -102,7 +103,7 @@ class ImageDisplay:
102
103
 
103
104
  def get_displayed_region(self):
104
105
  """Get the currently displayed region of the image."""
105
- if hasattr(self, 'ax'):
106
+ if hasattr(self, "ax"):
106
107
  xmin, xmax = self.ax.get_xlim()
107
108
  xmin = int(xmin + 0.5)
108
109
  if xmin < 1:
@@ -118,10 +119,8 @@ class ImageDisplay:
118
119
  if ymax > self.data.shape[0]:
119
120
  ymax = self.data.shape[0]
120
121
  print(f"Setting min/max using axis limits: x=({xmin:.2f}, {xmax:.2f}), y=({ymin:.2f}, {ymax:.2f})")
121
- region = self.region = SliceRegion2D(
122
- f'[{xmin}:{xmax}, {ymin}:{ymax}]', mode='fits'
123
- ).python
124
- elif hasattr(self, 'region'):
122
+ region = self.region = SliceRegion2D(f"[{xmin}:{xmax}, {ymin}:{ymax}]", mode="fits").python
123
+ elif hasattr(self, "region"):
125
124
  region = self.region
126
125
  else:
127
126
  raise AttributeError("No axis or region defined for set_minmax.")
@@ -136,7 +135,7 @@ class ImageDisplay:
136
135
  self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
137
136
  self.image.set_clim(vmin=vmin_new)
138
137
  self.image.set_clim(vmax=vmax_new)
139
- if hasattr(self, 'image_aux'):
138
+ if hasattr(self, "image_aux"):
140
139
  self.image_aux.set_clim(vmin=vmin_new)
141
140
  self.image_aux.set_clim(vmax=vmax_new)
142
141
  self.canvas.draw_idle()
@@ -149,7 +148,7 @@ class ImageDisplay:
149
148
  self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
150
149
  self.image.set_clim(vmin=vmin_new)
151
150
  self.image.set_clim(vmax=vmax_new)
152
- if hasattr(self, 'image_aux'):
151
+ if hasattr(self, "image_aux"):
153
152
  self.image_aux.set_clim(vmin=vmin_new)
154
153
  self.image_aux.set_clim(vmax=vmax_new)
155
154
  self.canvas.draw_idle()
@@ -53,14 +53,10 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
53
53
  by cosmic rays are interpolated.
54
54
  """
55
55
  # Mask of CR pixels
56
- mask = (cr_labels == cr_index)
56
+ mask = cr_labels == cr_index
57
57
  # Dilate the mask to find border pixels
58
58
  # dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
59
- dilated_mask = dilatemask(
60
- mask=mask,
61
- iterations=npoints,
62
- connectivity=1
63
- )
59
+ dilated_mask = dilatemask(mask=mask, iterations=npoints, connectivity=1)
64
60
  # Border pixels are those in the dilated mask but not in the original mask
65
61
  border_mask = dilated_mask & (~mask)
66
62
  # Get coordinates of border pixels
@@ -68,7 +64,7 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
68
64
  zfit_all = data[yfit_all, xfit_all].tolist()
69
65
  # Perform interpolation
70
66
  interpolation_performed = False
71
- if method == 'surface':
67
+ if method == "surface":
72
68
  if len(xfit_all) > 3:
73
69
  # Construct the design matrix for a 2D polynomial fit to a plane,
74
70
  # where each row corresponds to a point (x, y, z) and the model
@@ -84,10 +80,10 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
84
80
  interpolation_performed = True
85
81
  else:
86
82
  print("Not enough points to fit a plane")
87
- elif method in ['median', 'mean']:
83
+ elif method in ["median", "mean"]:
88
84
  # Compute median of all surrounding points
89
85
  if len(zfit_all) > 0:
90
- if method == 'median':
86
+ if method == "median":
91
87
  zval = np.median(zfit_all)
92
88
  else:
93
89
  zval = np.mean(zfit_all)
@@ -11,13 +11,16 @@
11
11
 
12
12
  import tkinter as tk
13
13
  from tkinter import messagebox
14
+ from tkinter import ttk
14
15
 
16
+ from .centerchildparent import center_on_parent
15
17
  from .definitions import VALID_CLEANING_METHODS
16
18
 
17
19
 
18
20
  class InterpolationEditor:
19
21
  """Dialog to select interpolation cleaning parameters."""
20
- def __init__(self, root, last_dilation, last_npoints, last_degree, auxdata):
22
+
23
+ def __init__(self, root, last_dilation, last_npoints, last_degree, auxdata, xmin, xmax, ymin, ymax, imgshape):
21
24
  """Initialize the interpolation editor dialog.
22
25
 
23
26
  Parameters
@@ -32,6 +35,16 @@ class InterpolationEditor:
32
35
  The last used degree for interpolation.
33
36
  auxdata : array-like or None
34
37
  Auxiliary data for cleaning, if available.
38
+ xmin : float
39
+ Minimum x value of the data. From 1 to NAXIS1.
40
+ xmax : float
41
+ Maximum x value of the data. From 1 to NAXIS1.
42
+ ymin : float
43
+ Minimum y value of the data. From 1 to NAXIS2.
44
+ ymax : float
45
+ Maximum y value of the data. From 1 to NAXIS2.
46
+ imgshape : tuple
47
+ Shape of the image data (height, width).
35
48
 
36
49
  Methods
37
50
  -------
@@ -62,6 +75,16 @@ class InterpolationEditor:
62
75
  The number of points for interpolation.
63
76
  degree : int
64
77
  The degree for interpolation.
78
+ xmin : float
79
+ Minimum x value of the data. From 1 to NAXIS1.
80
+ xmax : float
81
+ Maximum x value of the data. From 1 to NAXIS1.
82
+ ymin : float
83
+ Minimum y value of the data. From 1 to NAXIS2.
84
+ ymax : float
85
+ Maximum y value of the data. From 1 to NAXIS2.
86
+ imgshape : tuple
87
+ Shape of the image data (height, width).
65
88
  """
66
89
  self.root = root
67
90
  self.root.title("Cleaning Parameters")
@@ -74,15 +97,23 @@ class InterpolationEditor:
74
97
  "median": "a-median",
75
98
  "mean": "a-mean",
76
99
  "lacosmic": "lacosmic",
77
- "auxdata": "auxdata"
100
+ "auxdata": "auxdata",
78
101
  }
79
102
  self.check_interp_methods()
80
103
  # Initialize parameters
81
104
  self.cleaning_method = None
82
105
  self.npoints = last_npoints
83
106
  self.degree = last_degree
107
+ self.xmin = xmin
108
+ self.xmax = xmax
109
+ self.ymin = ymin
110
+ self.ymax = ymax
111
+ self.imgshape = imgshape
112
+ # Dictionary to hold entry widgets for region parameters
113
+ self.entries = {}
84
114
  # Create the form
85
115
  self.create_widgets()
116
+ center_on_parent(child=self.root, parent=self.root.master)
86
117
 
87
118
  def create_widgets(self):
88
119
  """Create the widgets for the dialog."""
@@ -90,9 +121,15 @@ class InterpolationEditor:
90
121
  main_frame = tk.Frame(self.root, padx=10, pady=10)
91
122
  main_frame.pack()
92
123
 
93
- # Title
94
- title_label = tk.Label(main_frame, text="Select Cleaning Method", font=("Arial", 14, "bold"))
95
- title_label.grid(row=0, column=0, columnspan=2, pady=(0, 15))
124
+ row = 0
125
+
126
+ # Subtitle for cleaning method selection
127
+ default_font = tk.font.nametofont("TkDefaultFont")
128
+ bold_font = default_font.copy()
129
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
130
+ subtitle_label = tk.Label(main_frame, text="Select Cleaning Method", font=bold_font)
131
+ subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
132
+ row += 1
96
133
 
97
134
  # Create labels and entry fields for each cleaning method
98
135
  row = 1
@@ -111,35 +148,80 @@ class InterpolationEditor:
111
148
  text=interp_method,
112
149
  variable=self.cleaning_method_var,
113
150
  value=interp_method,
114
- command=self.action_on_method_change
115
- ).grid(row=row, column=0, sticky='w')
151
+ command=self.action_on_method_change,
152
+ ).grid(row=row, column=1, sticky="w")
116
153
  row += 1
117
154
 
155
+ # Separator
156
+ separator1 = ttk.Separator(main_frame, orient="horizontal")
157
+ separator1.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
158
+ row += 1
159
+
160
+ # Subtitle for additional parameters
161
+ subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
162
+ subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
163
+ row += 1
164
+
118
165
  # Create labels and entry fields for each additional parameter
119
- label = tk.Label(main_frame, text='Npoints:')
120
- label.grid(row=row, column=0, sticky='e', padx=(0, 10))
166
+ label = tk.Label(main_frame, text="Npoints:")
167
+ label.grid(row=row, column=0, sticky="e", padx=(0, 10))
121
168
  self.entry_npoints = tk.Entry(main_frame, width=10)
122
169
  self.entry_npoints.insert(0, self.npoints)
123
- self.entry_npoints.grid(row=row, column=1, sticky='w')
170
+ self.entry_npoints.grid(row=row, column=1, sticky="w")
124
171
  row += 1
125
- label = tk.Label(main_frame, text='Degree:')
126
- label.grid(row=row, column=0, sticky='e', padx=(0, 10))
172
+ label = tk.Label(main_frame, text="Degree:")
173
+ label.grid(row=row, column=0, sticky="e", padx=(0, 10))
127
174
  self.entry_degree = tk.Entry(main_frame, width=10)
128
175
  self.entry_degree.insert(0, self.degree)
129
- self.entry_degree.grid(row=row, column=1, sticky='w')
176
+ self.entry_degree.grid(row=row, column=1, sticky="w")
177
+ row += 1
178
+
179
+ # Separator
180
+ separator2 = ttk.Separator(main_frame, orient="horizontal")
181
+ separator2.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
182
+ row += 1
183
+
184
+ # Subtitle for region to be examined
185
+ subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
186
+ subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
187
+ row += 1
188
+
189
+ # Region to be examined label and entries
190
+ for key in ["xmin", "xmax", "ymin", "ymax"]:
191
+ # Parameter name label
192
+ label = tk.Label(main_frame, text=f"{key}:", anchor="e", width=15)
193
+ label.grid(row=row, column=0, sticky="w", pady=5)
194
+ # Entry field
195
+ entry = tk.Entry(main_frame, width=10)
196
+ entry.insert(0, str(self.__dict__[key]))
197
+ entry.grid(row=row, column=1, padx=10, pady=5)
198
+ self.entries[key] = entry # dictionary to hold entry widgets
199
+ # Type label
200
+ dumtext = "(int)"
201
+ if key in ["xmin", "xmax"]:
202
+ dumtext += f" --> [1, {self.imgshape[1]}]"
203
+ else:
204
+ dumtext += f" --> [1, {self.imgshape[0]}]"
205
+ type_label = tk.Label(main_frame, text=dumtext, fg="gray", anchor="w", width=15)
206
+ type_label.grid(row=row, column=2, sticky="w", pady=5)
207
+ row += 1
208
+
209
+ # Separator
210
+ separator3 = ttk.Separator(main_frame, orient="horizontal")
211
+ separator3.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
130
212
  row += 1
131
213
 
132
214
  # Button frame
133
215
  self.button_frame = tk.Frame(main_frame)
134
- self.button_frame.grid(row=row, column=0, columnspan=2, pady=(15, 0))
216
+ self.button_frame.grid(row=row, column=0, columnspan=3, pady=(15, 0))
135
217
 
136
218
  # OK button
137
219
  self.ok_button = tk.Button(self.button_frame, text="OK", width=5, command=self.on_ok)
138
- self.ok_button.pack(side='left', padx=5)
220
+ self.ok_button.pack(side="left", padx=5)
139
221
 
140
222
  # Cancel button
141
223
  self.cancel_button = tk.Button(self.button_frame, text="Cancel", width=5, command=self.on_cancel)
142
- self.cancel_button.pack(side='left', padx=5)
224
+ self.cancel_button.pack(side="left", padx=5)
143
225
 
144
226
  # Initial action depending on the default method
145
227
  self.action_on_method_change()
@@ -165,10 +247,49 @@ class InterpolationEditor:
165
247
  messagebox.showerror("Input Error", "Degree must be non-negative.")
166
248
  return
167
249
 
168
- if self.cleaning_method in ['x', 'y'] and 2 * self.npoints <= self.degree:
250
+ if self.cleaning_method in ["x", "y"] and 2 * self.npoints <= self.degree:
169
251
  messagebox.showerror("Input Error", "2*Npoints must be greater than Degree for x and y interpolation.")
170
252
  return
171
253
 
254
+ # Retrieve and validate region parameters
255
+ try:
256
+ xmin = int(self.entries["xmin"].get())
257
+ except ValueError:
258
+ messagebox.showerror("Input Error", "xmin must be an integer.")
259
+ return
260
+ try:
261
+ xmax = int(self.entries["xmax"].get())
262
+ except ValueError:
263
+ messagebox.showerror("Input Error", "xmax must be an integer.")
264
+ return
265
+ if xmin >= xmax:
266
+ messagebox.showerror("Input Error", "xmin must be less than xmax.")
267
+ return
268
+ try:
269
+ ymin = int(self.entries["ymin"].get())
270
+ except ValueError:
271
+ messagebox.showerror("Input Error", "ymin must be an integer.")
272
+ return
273
+ try:
274
+ ymax = int(self.entries["ymax"].get())
275
+ except ValueError:
276
+ messagebox.showerror("Input Error", "ymax must be an integer.")
277
+ return
278
+ if ymin >= ymax:
279
+ messagebox.showerror("Input Error", "ymin must be less than ymax.")
280
+ return
281
+ for key, entry in self.entries.items():
282
+ value = int(entry.get())
283
+ if key in ["xmin", "xmax"]:
284
+ if not (1 <= value <= self.imgshape[1]):
285
+ messagebox.showerror("Input Error", f"{key} must be in the range [1, {self.imgshape[1]}].")
286
+ return
287
+ else:
288
+ if not (1 <= value <= self.imgshape[0]):
289
+ messagebox.showerror("Input Error", f"{key} must be in the range [1, {self.imgshape[0]}].")
290
+ return
291
+ self.__dict__[key] = value
292
+
172
293
  self.root.destroy()
173
294
 
174
295
  def on_cancel(self):
@@ -182,24 +303,24 @@ class InterpolationEditor:
182
303
  """Handle changes in the selected cleaning method."""
183
304
  selected_method = self.cleaning_method_var.get()
184
305
  print(f"Selected cleaning method: {selected_method}")
185
- if selected_method in ['x interp.', 'y interp.']:
186
- self.entry_npoints.config(state='normal')
187
- self.entry_degree.config(state='normal')
188
- elif selected_method == 'surface interp.':
189
- self.entry_npoints.config(state='normal')
190
- self.entry_degree.config(state='disabled')
191
- elif selected_method == 'median':
192
- self.entry_npoints.config(state='normal')
193
- self.entry_degree.config(state='disabled')
194
- elif selected_method == 'mean':
195
- self.entry_npoints.config(state='normal')
196
- self.entry_degree.config(state='disabled')
197
- elif selected_method == 'lacosmic':
198
- self.entry_npoints.config(state='disabled')
199
- self.entry_degree.config(state='disabled')
200
- elif selected_method == 'auxdata':
201
- self.entry_npoints.config(state='disabled')
202
- self.entry_degree.config(state='disabled')
306
+ if selected_method in ["x interp.", "y interp."]:
307
+ self.entry_npoints.config(state="normal")
308
+ self.entry_degree.config(state="normal")
309
+ elif selected_method == "surface interp.":
310
+ self.entry_npoints.config(state="normal")
311
+ self.entry_degree.config(state="disabled")
312
+ elif selected_method == "median":
313
+ self.entry_npoints.config(state="normal")
314
+ self.entry_degree.config(state="disabled")
315
+ elif selected_method == "mean":
316
+ self.entry_npoints.config(state="normal")
317
+ self.entry_degree.config(state="disabled")
318
+ elif selected_method == "lacosmic":
319
+ self.entry_npoints.config(state="disabled")
320
+ self.entry_degree.config(state="disabled")
321
+ elif selected_method == "auxdata":
322
+ self.entry_npoints.config(state="disabled")
323
+ self.entry_degree.config(state="disabled")
203
324
 
204
325
  def check_interp_methods(self):
205
326
  """Check that all interpolation methods are valid."""
@@ -0,0 +1,182 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Module defining a progress bar widget for Tkinter."""
11
+
12
+ import tkinter as tk
13
+ from tkinter import ttk
14
+ import time
15
+
16
+
17
+ class ModalProgressBar:
18
+ def __init__(
19
+ self, parent, iterable=None, total=None, desc="Processing", completion_msg="Processing completed successfully!"
20
+ ):
21
+ self.parent = parent
22
+ self.iterable = iterable
23
+ self.total = total if total is not None else (len(iterable) if iterable is not None else 100)
24
+ self.current = 0
25
+ self.start_time = time.time()
26
+ self.window = None
27
+ self.desc = desc
28
+ self.completion_msg = completion_msg
29
+ self.continue_clicked = False
30
+
31
+ def __enter__(self):
32
+ # Create the modal window when entering context
33
+ self.window = tk.Toplevel(self.parent)
34
+ self.window.title("Progress")
35
+
36
+ # Make it modal
37
+ self.window.transient(self.parent)
38
+ self.window.grab_set()
39
+ self.window.protocol("WM_DELETE_WINDOW", lambda: None)
40
+
41
+ # Set geometry and force it
42
+ minwinsize_x = 400
43
+ minwinsize_y = 120
44
+ self.window.minsize(minwinsize_x, minwinsize_y)
45
+ self.window.update_idletasks()
46
+ self.window.update()
47
+
48
+ # Center on parent
49
+ self._center_on_parent()
50
+
51
+ # UI elements
52
+ default_font = tk.font.nametofont("TkDefaultFont")
53
+ bold_font = default_font.copy()
54
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
55
+ self.desc_label = tk.Label(self.window, text=self.desc, font=bold_font)
56
+ self.desc_label.pack(padx=10, pady=5)
57
+
58
+ self.progress = ttk.Progressbar(self.window, length=minwinsize_x - 20, mode="determinate", maximum=self.total)
59
+ self.progress.pack(padx=10, pady=10)
60
+
61
+ self.status_label = tk.Label(self.window, text=f"0/{self.total} (0.0%)")
62
+ self.status_label.pack(padx=10, pady=2)
63
+
64
+ self.time_label = tk.Label(self.window, text="Elapsed: 0s | ETA: --")
65
+ self.time_label.pack(padx=10, pady=2)
66
+
67
+ # Continue button (to close the dialog after completion; hidden until done)
68
+ self.continue_button = tk.Button(self.window, text="Continue", command=self._on_continue)
69
+ self.continue_button.pack(padx=10, pady=10)
70
+ self.continue_button.pack_forget() # Hide initially
71
+
72
+ # Force another update
73
+ self.window.update_idletasks()
74
+ self.window.update()
75
+
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ if self.window:
80
+ # Show completion screen instead of closing
81
+ self._show_completion()
82
+
83
+ # Wait for user to click Continue
84
+ self.window.wait_variable(self._continue_var)
85
+
86
+ # Now close
87
+ self._destroy()
88
+ return False
89
+
90
+ def __iter__(self):
91
+ """Allow iteration like tqdm"""
92
+ if self.iterable is None:
93
+ raise ValueError("No iterable provided for iteration")
94
+
95
+ for item in self.iterable:
96
+ yield item
97
+ self.update(1)
98
+
99
+ def _center_on_parent(self):
100
+ self.window.update_idletasks()
101
+ x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.window.winfo_width() // 2)
102
+ y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.window.winfo_height() // 2)
103
+ self.window.geometry(f"+{x}+{y}")
104
+
105
+ def update(self, n=1):
106
+ self.current += n
107
+ self.progress["value"] = self.current
108
+ percentage = (self.current / self.total) * 100
109
+
110
+ elapsed = time.time() - self.start_time
111
+
112
+ if self.current > 0:
113
+ rate = self.current / elapsed
114
+ remaining = self.total - self.current
115
+ eta_seconds = remaining / rate if rate > 0 else 0
116
+
117
+ elapsed_str = self._format_time(elapsed)
118
+ eta_str = self._format_time(eta_seconds)
119
+ rate_str = f"{rate:.2f} CR/s" if rate >= 1 else f"{1/rate:.2f} s/CR"
120
+
121
+ self.status_label.config(text=f"{self.current}/{self.total} ({percentage:.1f}%) | {rate_str}")
122
+ self.time_label.config(text=f"Elapsed: {elapsed_str} | ETA: {eta_str}")
123
+
124
+ self.window.update_idletasks()
125
+ self.window.update()
126
+
127
+ def _format_time(self, seconds):
128
+ if seconds < 60:
129
+ return f"{seconds:.1f} s"
130
+ elif seconds < 3600:
131
+ minutes = int(seconds // 60)
132
+ secs = int(seconds % 60)
133
+ return f"{minutes}m {secs} s"
134
+ else:
135
+ hours = int(seconds // 3600)
136
+ minutes = int((seconds % 3600) // 60)
137
+ return f"{hours}h {minutes} m"
138
+
139
+ def _show_completion(self):
140
+ """Transform the window into a completion dialog"""
141
+ elapsed = time.time() - self.start_time
142
+ elapsed_str = self._format_time(elapsed)
143
+
144
+ # Update title
145
+ self.window.title("Completed")
146
+
147
+ # Update description to show completion message
148
+ default_font = tk.font.nametofont("TkDefaultFont")
149
+ bold_font = default_font.copy()
150
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
151
+ self.desc_label.config(text=self.completion_msg, fg="green", font=bold_font)
152
+
153
+ # Hide progress bar
154
+ self.progress.pack_forget()
155
+
156
+ # Update status to show final stats
157
+ avg_rate = self.current / elapsed if elapsed > 0 else 0
158
+ rate_str = f"{avg_rate:.2f} CR/s" if avg_rate >= 1 else f"{1/avg_rate:.2f} s/CR"
159
+ self.status_label.config(text=f"Processed {self.current} CRs | {rate_str}")
160
+
161
+ # Update time label
162
+ self.time_label.config(text=f"Total time: {elapsed_str}")
163
+
164
+ # Show the Continue button
165
+ self.continue_button.pack(padx=10, pady=15)
166
+
167
+ # Create a variable to track when Continue is clicked
168
+ self._continue_var = tk.BooleanVar(value=False)
169
+
170
+ # Re-enable close button to work like Continue
171
+ self.window.protocol("WM_DELETE_WINDOW", self._on_continue)
172
+
173
+ self.window.update()
174
+ self._center_on_parent()
175
+
176
+ def _on_continue(self):
177
+ """Called when Continue button is clicked"""
178
+ self._continue_var.set(True)
179
+
180
+ def _destroy(self):
181
+ self.window.grab_release()
182
+ self.window.destroy()