pdflinkcheck 1.1.94__py3-none-any.whl → 1.2.29__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 (35) hide show
  1. pdflinkcheck/__init__.py +88 -18
  2. pdflinkcheck/__main__.py +6 -0
  3. pdflinkcheck/analysis_pdfium.py +131 -0
  4. pdflinkcheck/{analyze_pymupdf.py → analysis_pymupdf.py} +99 -141
  5. pdflinkcheck/{analyze_pypdf.py → analysis_pypdf.py} +51 -39
  6. pdflinkcheck/cli.py +52 -48
  7. pdflinkcheck/data/LICENSE +18 -15
  8. pdflinkcheck/data/README.md +23 -25
  9. pdflinkcheck/data/pyproject.toml +17 -26
  10. pdflinkcheck/datacopy.py +16 -1
  11. pdflinkcheck/dev.py +2 -2
  12. pdflinkcheck/environment.py +14 -2
  13. pdflinkcheck/gui.py +346 -563
  14. pdflinkcheck/helpers.py +88 -0
  15. pdflinkcheck/io.py +24 -6
  16. pdflinkcheck/report.py +598 -97
  17. pdflinkcheck/security.py +189 -0
  18. pdflinkcheck/splash.py +38 -0
  19. pdflinkcheck/stdlib_server.py +7 -21
  20. pdflinkcheck/stdlib_server_alt.py +571 -0
  21. pdflinkcheck/tk_utils.py +188 -0
  22. pdflinkcheck/update_msix_version.py +2 -0
  23. pdflinkcheck/validate.py +104 -170
  24. pdflinkcheck/version_info.py +2 -2
  25. {pdflinkcheck-1.1.94.dist-info → pdflinkcheck-1.2.29.dist-info}/METADATA +41 -40
  26. {pdflinkcheck-1.1.94.dist-info → pdflinkcheck-1.2.29.dist-info}/RECORD +34 -27
  27. pdflinkcheck-1.2.29.dist-info/WHEEL +5 -0
  28. {pdflinkcheck-1.1.94.dist-info → pdflinkcheck-1.2.29.dist-info}/entry_points.txt +0 -1
  29. pdflinkcheck-1.2.29.dist-info/licenses/LICENSE +27 -0
  30. pdflinkcheck-1.2.29.dist-info/top_level.txt +1 -0
  31. pdflinkcheck/analyze_pypdf_v2.py +0 -217
  32. pdflinkcheck-1.1.94.dist-info/WHEEL +0 -4
  33. pdflinkcheck-1.1.94.dist-info/licenses/LICENSE +0 -24
  34. {pdflinkcheck-1.1.94.dist-info → pdflinkcheck-1.2.29.dist-info}/licenses/LICENSE-AGPL3 +0 -0
  35. {pdflinkcheck-1.1.94.dist-info → pdflinkcheck-1.2.29.dist-info}/licenses/LICENSE-MIT +0 -0
pdflinkcheck/gui.py CHANGED
@@ -1,23 +1,24 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # SPDX-License-Identifier: MIT
3
3
  # src/pdflinkcheck/gui.py
4
+ from __future__ import annotations
4
5
  import tkinter as tk
5
- from tkinter import filedialog, ttk, messagebox # Added messagebox
6
+ from tkinter import filedialog, ttk, messagebox, PhotoImage
6
7
  import sys
7
8
  from pathlib import Path
8
- from typing import Optional # Added Optional
9
+ from typing import Optional
9
10
  import unicodedata
10
11
  from importlib.resources import files
11
12
  import pyhabitat
12
13
  import ctypes
14
+ import threading
13
15
 
14
-
15
-
16
- # Import the core analysis function
16
+ # --- Core Imports ---
17
17
  from pdflinkcheck.report import run_report_and_call_exports
18
18
  from pdflinkcheck.version_info import get_version_from_pyproject
19
19
  from pdflinkcheck.io import get_first_pdf_in_cwd, get_friendly_path, PDFLINKCHECK_HOME
20
- from pdflinkcheck.environment import pymupdf_is_available, clear_all_caches, is_in_git_repo
20
+ from pdflinkcheck.environment import pymupdf_is_available, pdfium_is_available, clear_all_caches, is_in_git_repo
21
+ from pdflinkcheck.tk_utils import center_window_on_primary
21
22
 
22
23
  class RedirectText:
23
24
  """A class to redirect sys.stdout messages to a Tkinter Text widget."""
@@ -25,455 +26,206 @@ class RedirectText:
25
26
  self.text_widget = text_widget
26
27
 
27
28
  def write(self, string):
28
- """Insert the incoming string into the Text widget."""
29
29
  self.text_widget.insert(tk.END, string)
30
- self.text_widget.see(tk.END) # Scroll to the end
31
- self.text_widget.update_idletasks() # Refresh GUI to allow real timie updates << If suppress: The mainloop will handle updates efficiently without forcing them, , but info appears outdated when a new file is analyzed. Immediate feedback is better.
30
+ self.text_widget.see(tk.END)
31
+ self.text_widget.update_idletasks()
32
32
 
33
33
  def flush(self, *args):
34
- """Required for file-like objects, but does nothing here."""
35
34
  pass
36
35
 
36
+ class PDFLinkCheckApp:
37
37
 
38
- class PDFLinkCheckerApp(tk.Tk):
38
+ # --- Lifecycle & Initialization ---
39
39
 
40
- def _initialize_forest_theme(self):
41
- from importlib.resources import files
42
- # Path to pdflinkcheck/data/themes/forest/
43
- theme_dir = files("pdflinkcheck.data.themes.forest")
44
- # Load the theme files
45
- self.tk.call("source", str(theme_dir / f"forest-light.tcl"))
46
- self.tk.call("source", str(theme_dir / f"forest-dark.tcl"))
40
+ def __init__(self, root: tk.Tk):
41
+ self.root = root
47
42
 
48
-
43
+ # Do NOT load theme yet.
44
+ # Run the "heavy" initialization first
45
+ self._initialize_vars()
49
46
 
50
- def _toggle_theme(self):
51
- # You could instead assign the dark to light of a single theme here
52
- """
53
- Calls light/dark toggle for the forest theme with self._toggle_theme_just_forest()
54
- """
55
- return self._toggle_theme_forest()
56
-
57
- def _toggle_theme_forest(self):
58
- if ttk.Style().theme_use() == "forest-light":
59
- ttk.Style().theme_use("forest-dark")
60
- elif ttk.Style().theme_use() == "forest-dark":
61
- ttk.Style().theme_use("forest-light")
47
+ # NOW load the theme (this takes ~100-300ms)
48
+ self._initialize_forest_theme()
62
49
 
63
- def _set_icon(self):
64
- from importlib.resources import files
65
- # Path to pdflinkcheck/data/icons/
66
- icon_dir = files("pdflinkcheck.data.icons")
67
- # Convert to a real filesystem path
68
- icon_path = icon_dir.joinpath("red_pdf_128px.ico")
69
- icon_path = icon_dir.joinpath("red_pdf_512px.ico")
70
-
71
- self.iconbitmap(str(icon_path))
72
-
73
- def __init__(self):
74
- super().__init__()
50
+ # Apply the theme
51
+ style = ttk.Style()
52
+ style.configure(".", padding=2) # global min padding
53
+ style.configure("TFrame", padding=2)
54
+ style.configure("TLabelFrame", padding=(4,2))
55
+ style.configure("TButton", padding=4)
56
+ style.configure("TCheckbutton", padding=2)
57
+ style.configure("TRadiobutton", padding=2)
58
+ style.theme_use("forest-dark")
75
59
 
76
-
77
- self._initialize_forest_theme() # load but do not set internally
78
-
79
- ttk.Style().theme_use("forest-dark") # but if you use _toggle_theme_just_forest(), then you had better do this
80
-
81
- if is_in_git_repo() and not pyhabitat.as_pyinstaller() and not pyhabitat.is_pyz():
82
- # Checking for PYZ is overkill, because a PYZ s not expected to carry a .git directory, which is a check that is already completed.
83
- title_suffix = ""# " [Development]"
84
- else:
85
- title_suffix = ""
86
-
87
- self.title(f"PDF Link Check v{get_version_from_pyproject()}{title_suffix}")
88
- self.geometry("800x600")
60
+ self.root.title(f"PDF Link Check v{get_version_from_pyproject()}") # Short title
61
+ self.root.geometry("700x500") # Smaller starting size
62
+ self.root.minsize(600, 400) # Prevent too-small window
89
63
 
90
64
  self._set_icon()
91
65
 
92
-
93
- # --- 1. Initialize Variables ---
66
+ # --- 2. Widget Construction ---
67
+ self._create_widgets()
68
+ self._initialize_menubar()
69
+
70
+ def _initialize_vars(self):
71
+ """Logic that takes time but doesn't need a UI yet."""
72
+
73
+ # --- 1. Variable State Management ---
94
74
  self.pdf_path = tk.StringVar(value="")
95
- self.pdf_library_var = tk.StringVar(value="PyMuPDF")
96
- self.max_links_var = tk.StringVar(value="50")
97
- self.show_all_links_var = tk.BooleanVar(value=True)
98
- self.do_export_report_json_var = tk.BooleanVar(value=True)
99
- self.do_export_report_txt_var = tk.BooleanVar(value=True)
75
+ self.pdf_library_var = tk.StringVar(value="pymupdf")
76
+ self.do_export_report_json_var = tk.BooleanVar(value=True)
77
+ self.do_export_report_txt_var = tk.BooleanVar(value=True)
100
78
  self.current_report_text = None
101
79
  self.current_report_data = None
102
80
 
103
- self.supported_export_formats = ["JSON", "MD", "TXT"]
104
- self.supported_export_formats = ["JSON"]
81
+ # Track exported file paths
82
+ self.last_json_path: Optional[Path] = None
83
+ self.last_txt_path: Optional[Path] = None
105
84
 
85
+ # Engine detection (This can take a few ms)
86
+ self.pdf_library_var = tk.StringVar(value="PDFium")
87
+ if not pdfium_is_available():
88
+ self.pdf_library_var.set("PyMuPDF")
106
89
  if not pymupdf_is_available():
107
- print(f"pymupdf_is_available: {pymupdf_is_available()}")
108
90
  self.pdf_library_var.set("pypdf")
109
91
 
110
-
111
- # --- 2. Create Widgets ---
112
- self._create_widgets()
113
-
114
- # --- 3. Set Initial Dependent Widget States ---
115
- self._toggle_max_links_entry()
116
- self._toggle_json_export()
117
- self._toggle_txt_export()
92
+
93
+ # --- Theme & Visual Initialization ---
94
+ def _initialize_forest_theme(self):
95
+ theme_dir = files("pdflinkcheck.data.themes.forest")
96
+ self.root.tk.call("source", str(theme_dir / "forest-light.tcl"))
97
+ self.root.tk.call("source", str(theme_dir / "forest-dark.tcl"))
118
98
 
119
- # --- Menubar with dropdown ---
120
- menubar = tk.Menu(self)
121
- self.config(menu=menubar)
99
+ def _toggle_theme(self):
100
+ style = ttk.Style(self.root) # Explicitly link style to our root
101
+ if style.theme_use() == "forest-light":
102
+ style.theme_use("forest-dark")
103
+ elif style.theme_use() == "forest-dark":
104
+ style.theme_use("forest-light")
105
+
106
+ def _set_icon(self):
107
+ icon_dir = files("pdflinkcheck.data.icons")
108
+ try:
109
+ png_path = icon_dir.joinpath("Logo-150x150.png")
110
+ if png_path.exists():
111
+ self.icon_img = PhotoImage(file=str(png_path))
112
+ self.root.iconphoto(True, self.icon_img)
113
+ except Exception:
114
+ pass
115
+ try:
116
+ icon_path = icon_dir.joinpath("red_pdf_512px.ico")
117
+ if icon_path.exists():
118
+ self.root.iconbitmap(str(icon_path))
119
+ except Exception:
120
+ pass
121
+
122
+
123
+ def _initialize_menubar(self):
124
+ """Builds the application menu bar."""
125
+ menubar = tk.Menu(self.root)
126
+ self.root.config(menu=menubar)
122
127
 
123
- # Tools menu
124
128
  tools_menu = tk.Menu(menubar, tearoff=0)
125
129
  menubar.add_cascade(label="Tools", menu=tools_menu)
126
130
 
127
131
  tools_menu.add_command(label="Toggle Theme", command=self._toggle_theme)
132
+ tools_menu.add_command(label="Clear Output Window", command=self._clear_output_window)
133
+ tools_menu.add_command(label="Copy Output to Clipboard", command=self._copy_output_to_clipboard)
128
134
  tools_menu.add_command(label="Clear Cache", command=self._clear_all_caches)
129
135
 
130
- # Add existing License/Readme to tools menu
131
136
  tools_menu.add_separator()
132
137
  tools_menu.add_command(label="License", command=self._show_license)
133
138
  tools_menu.add_command(label="Readme", command=self._show_readme)
134
139
  tools_menu.add_command(label="I Have Questions", command=self._show_i_have_questions)
135
- # In class PDFLinkCheckerApp:
136
140
 
137
- def _copy_pdf_path(self):
138
- """Copies the current PDF path from the Entry widget to the system clipboard."""
139
- path_to_copy = self.pdf_path.get()
140
-
141
- if path_to_copy:
142
- try:
143
- # Clear the clipboard
144
- self.clipboard_clear()
145
- # Append the path string to the clipboard
146
- self.clipboard_append(path_to_copy)
147
- # Notify the user (optional, but good UX)
148
- messagebox.showinfo("Copied", "PDF Path copied to clipboard.")
149
- except tk.TclError as e:
150
- # Handle cases where clipboard access might be blocked
151
- messagebox.showerror("Copy Error", f"Failed to access the system clipboard: {e}")
152
- else:
153
- messagebox.showwarning("Copy Failed", "The PDF Path field is empty.")
154
-
155
- def _scroll_to_top(self):
156
- """Scrolls the output text widget to the top."""
157
- self.output_text.see('1.0') # '1.0' is the index for the very first character
141
+ # --- UI Component Building ---
158
142
 
159
- def _scroll_to_bottom(self):
160
- """Scrolls the output text widget to the bottom."""
161
- self.output_text.see(tk.END) # tk.END is the index for the position just after the last character
143
+ def _create_widgets(self):
144
+ """Compact layout with reduced padding."""
162
145
 
163
- def _clear_all_caches(self):
164
- """Clear caches and show confirmation."""
165
- clear_all_caches()
166
- messagebox.showinfo("Caches Cleared", f"All caches have been cleared.\nPyMuPDF available: {pymupdf_is_available()}")
146
+ # --- Control Frame (Top) ---
147
+ control_frame = ttk.Frame(self.root, padding=(4, 2, 4, 2))
148
+ control_frame.pack(fill='x', pady=(2, 2))
167
149
 
168
- def _show_license(self):
169
- """
170
- Reads the embedded LICENSE file (AGPLv3) and displays its content in a new modal window.
171
- """
172
- try:
173
- # Use the Traversable object's read_text() method.
174
- # This handles files located inside zip archives (.pyz, pipx venvs) correctly.
175
- license_path_traversable = files("pdflinkcheck.data") / "LICENSE"
176
- license_content = license_path_traversable.read_text(encoding="utf-8")
177
-
178
- except FileNotFoundError:
179
- if is_in_git_repo():
180
- messagebox.showinfo(
181
- "Local Development Mode",
182
- "Embedded data files not found – copying from root..."
183
- )
184
- try:
185
- from pdflinkcheck.datacopy import ensure_package_license, ensure_data_files_for_build
186
- #ensure_package_license()
187
- ensure_data_files_for_build()
188
- # Retry display
189
- self._show_license()
190
- return
191
- except Exception as e:
192
- messagebox.showerror("Copy Failed", f"Could not copy file: {e}")
193
- else:
194
- messagebox.showerror(
195
- "Packaging Error",
196
- "Embedded file not found. This indicates a problem with the package build/installation."
197
- )
198
- return
199
-
200
- except Exception as e:
201
- messagebox.showerror("Read Error", f"Failed to read embedded LICENSE file: {e}")
202
- return
150
+ # === Row 0: File Selection ===
151
+ file_selection_frame = ttk.Frame(control_frame)
152
+ file_selection_frame.grid(row=0, column=0, columnspan=3, padx=0, pady=(2, 4), sticky='ew')
203
153
 
204
- # --- Display in a New Toplevel Window ---
205
- license_window = tk.Toplevel(self)
206
- license_window.title("Software License")
207
- license_window.geometry("600x400")
208
-
209
- # Text widget for content
210
- text_widget = tk.Text(license_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
211
- text_widget.insert(tk.END, license_content)
212
- text_widget.config(state=tk.DISABLED)
213
-
214
- # Scrollbar
215
- scrollbar = ttk.Scrollbar(license_window, command=text_widget.yview)
216
- text_widget['yscrollcommand'] = scrollbar.set
217
-
218
- # Layout
219
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
220
- text_widget.pack(fill='both', expand=True)
221
-
222
- # Make the window modal (optional, but good practice for notices)
223
- license_window.transient(self)
224
- license_window.grab_set()
225
- self.wait_window(license_window)
154
+ ttk.Label(file_selection_frame, text="PDF Path:").pack(side=tk.LEFT, padx=(0, 3))
155
+ entry = ttk.Entry(file_selection_frame, textvariable=self.pdf_path)
156
+ entry.pack(side=tk.LEFT, fill='x', expand=True, padx=3)
157
+ ttk.Button(file_selection_frame, text="Browse...", command=self._select_pdf, width=10).pack(side=tk.LEFT, padx=(3, 3))
158
+ ttk.Button(file_selection_frame, text="Copy Path", command=self._copy_pdf_path, width=10).pack(side=tk.LEFT, padx=(0, 0))
226
159
 
227
- def _show_readme(self):
228
- """
229
- Reads the embedded README.md file and displays its content in a new modal window.
230
- """
231
- try:
232
- # Use the Traversable object's read_text() method.
233
- # This handles files located inside zip archives (.pyz, pipx venvs) correctly.
234
- readme_path_traversable = files("pdflinkcheck.data") / "README.md"
235
- readme_content = readme_path_traversable.read_text(encoding="utf-8")
236
- readme_content = sanitize_glyphs_for_tkinter(readme_content)
237
- except FileNotFoundError:
238
- if is_in_git_repo():
239
- messagebox.showinfo(
240
- "Local Development Mode",
241
- "Embedded data files not found – copying from root..."
242
- )
243
- try:
244
- from pdflinkcheck.datacopy import ensure_package_readme, ensure_data_files_for_build
245
- #ensure_package_readme()
246
- ensure_data_files_for_build()
247
- # Retry display
248
- self._show_readme()
249
- return
250
- except Exception as e:
251
- messagebox.showerror("Copy Failed", f"Could not copy file: {e}")
252
- else:
253
- messagebox.showerror(
254
- "Packaging Error",
255
- "Embedded file not found. This indicates a problem with the package build/installation."
256
- )
257
- return
258
- except Exception as e:
259
- messagebox.showerror("Read Error", f"Failed to read embedded README.md file: {e}")
260
- return
160
+ # === Row 1: Configuration & Export Jumps ===
161
+ pdf_library_frame = ttk.LabelFrame(control_frame, text="Backend Engine:")
162
+ pdf_library_frame.grid(row=1, column=0, padx=3, pady=3, sticky='nsew')
261
163
 
262
- # --- Display in a New Toplevel Window ---
263
- readme_window = tk.Toplevel(self)
264
- readme_window.title("pdflinkcheck README.md")
265
- readme_window.geometry("600x400")
266
-
267
- # Text widget for content
268
- text_widget = tk.Text(readme_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
269
- text_widget.insert(tk.END, readme_content)
270
- text_widget.config(state=tk.DISABLED)
271
-
272
- # Scrollbar
273
- scrollbar = ttk.Scrollbar(readme_window, command=text_widget.yview)
274
- text_widget['yscrollcommand'] = scrollbar.set
275
-
276
- # Layout
277
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
278
- text_widget.pack(fill='both', expand=True)
279
-
280
- # Make the window modal (optional, but good practice for notices)
281
- readme_window.transient(self)
282
- readme_window.grab_set()
283
- self.wait_window(readme_window)
164
+ if pdfium_is_available():
165
+ ttk.Radiobutton(pdf_library_frame, text="PDFium", variable=self.pdf_library_var, value="PDFium").pack(side='left', padx=5, pady=1)
166
+ if pymupdf_is_available():
167
+ ttk.Radiobutton(pdf_library_frame, text="PyMuPDF", variable=self.pdf_library_var, value="PyMuPDF").pack(side='left', padx=3, pady=1)
168
+ ttk.Radiobutton(pdf_library_frame, text="pypdf", variable=self.pdf_library_var, value="pypdf").pack(side='left', padx=3, pady=1)
284
169
 
285
- def _show_i_have_questions(self):
286
- """
287
- Reads the embedded I Have Questions.md file and displays its content in a new modal window.
288
- """
289
- try:
290
- # Use the Traversable object's read_text() method.
291
- # This handles files located inside zip archives (.pyz, pipx venvs) correctly.
292
- i_have_questions_path_traversable = files("pdflinkcheck.data") / "I Have Questions.md"
293
- i_have_questions_content = i_have_questions_path_traversable.read_text(encoding="utf-8")
294
- i_have_questions_content = sanitize_glyphs_for_tkinter(i_have_questions_content)
295
- except FileNotFoundError:
296
- messagebox.showerror("Read Error", f"Failed to read embedded 'I Have Questions.md' file.")
297
- return
170
+ export_config_frame = ttk.LabelFrame(control_frame, text="Export Enabled:")
171
+ export_config_frame.grid(row=1, column=1, padx=3, pady=3, sticky='nsew')
298
172
 
299
- # --- Display in a New Toplevel Window ---
300
- i_have_questions_window = tk.Toplevel(self)
301
- i_have_questions_window.title("I Have Questions.md")
302
- i_have_questions_window.geometry("600x400")
303
-
304
- # Text widget for content
305
- text_widget = tk.Text(i_have_questions_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
306
- text_widget.insert(tk.END, i_have_questions_content)
307
- text_widget.config(state=tk.DISABLED)
308
-
309
- # Scrollbar
310
- scrollbar = ttk.Scrollbar(i_have_questions_window, command=text_widget.yview)
311
- text_widget['yscrollcommand'] = scrollbar.set
312
-
313
- # Layout
314
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
315
- text_widget.pack(fill='both', expand=True)
316
-
317
- # Make the window modal (optional, but good practice for notices)
318
- i_have_questions_window.transient(self)
319
- i_have_questions_window.grab_set()
320
- self.wait_window(i_have_questions_window)
173
+ ttk.Checkbutton(export_config_frame, text="JSON", variable=self.do_export_report_json_var).pack(side=tk.LEFT, padx=4)
174
+ ttk.Checkbutton(export_config_frame, text="TXT", variable=self.do_export_report_txt_var).pack(side=tk.LEFT, padx=4)
321
175
 
322
- def _create_widgets(self):
323
- # --- Control Frame (Top) ---
324
- control_frame = ttk.Frame(self, padding="10")
325
- control_frame.pack(fill='x')
176
+ self.export_actions_frame = ttk.LabelFrame(control_frame, text="Open Report Files:")
177
+ self.export_actions_frame.grid(row=1, column=2, padx=3, pady=3, sticky='nsew')
326
178
 
327
- # Row 0: File Selection
179
+ self.btn_open_json = ttk.Button(self.export_actions_frame, text="Open JSON", command=lambda: self._open_export_file("json"), width=10)
180
+ self.btn_open_json.pack(side=tk.LEFT, padx=3, pady=1)
328
181
 
329
- # === File Selection Frame (Row 0) ===
330
- file_selection_frame = ttk.Frame(control_frame)
331
- file_selection_frame.grid(row=0, column=0, columnspan=3, padx=0, pady=5, sticky='ew')
332
-
333
- # Elements are now packed/gridded within file_selection_frame
334
-
335
- # Label
336
- ttk.Label(file_selection_frame, text="PDF Path:").pack(side=tk.LEFT, padx=(0, 5))
337
-
338
- # Entry (Path Display)
339
- ttk.Entry(file_selection_frame, textvariable=self.pdf_path, width=50).pack(side=tk.LEFT, fill='x', expand=True, padx=5)
340
- # The Entry field (column 1) must expand horizontally within its frame
341
- # Since we are using PACK for this frame, we use fill='x', expand=True on the Entry.
342
-
343
- # Browse Button
344
- ttk.Button(file_selection_frame, text="Browse...", command=self._select_pdf).pack(side=tk.LEFT, padx=(5, 5))
182
+ self.btn_open_txt = ttk.Button(self.export_actions_frame, text="Open TXT", command=lambda: self._open_export_file("txt"), width=10)
183
+ self.btn_open_txt.pack(side=tk.LEFT, padx=3, pady=1)
345
184
 
346
- # Copy Button
347
- # NOTE: Removed leading spaces from " Copy Path"
348
- ttk.Button(file_selection_frame, text="Copy Path", command=self._copy_pdf_path).pack(side=tk.LEFT, padx=(0, 0))
349
-
350
- # === END: File Selection Frame ===
351
-
352
- # --- Report brevity options ----
353
- report_brevity_frame = ttk.LabelFrame(control_frame, text="Report Brevity Options:")
354
- #report_brevity_frame.grid(row=1, column=0, columnspan=2, padx=5, pady=1, sticky='nsew')
355
- report_brevity_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nsew')
356
- #
357
- ttk.Checkbutton(
358
- report_brevity_frame,
359
- text="Show All Links.",
360
- variable=self.show_all_links_var,
361
- command=self._toggle_max_links_entry
362
- ).pack(side='left', padx=5, pady=1)
363
-
364
- ttk.Label(report_brevity_frame, text="Max Links to Display:").pack(side='left', padx=5, pady=1)
365
- self.max_links_entry = ttk.Entry(report_brevity_frame, textvariable=self.max_links_var, width=7)
366
- self.max_links_entry.pack(side='left', padx=5, pady=5)
367
-
368
- # --- PDF Library Selection ---
369
- # Create a labeled group for the PDF options
370
- pdf_library_frame = ttk.LabelFrame(control_frame, text="Select PDF Library:")
371
- pdf_library_frame.grid(row=1, column=1, padx=5, pady=5, sticky='nsew')
372
-
373
- # Radio options inside the frame
374
- ttk.Radiobutton(
375
- pdf_library_frame,
376
- text="PyMuPDF",
377
- variable=self.pdf_library_var,
378
- value="PyMuPDF",
379
-
380
- ).pack(side='left', padx=5, pady=1)
381
-
382
- ttk.Radiobutton(
383
- pdf_library_frame,
384
- text="pypdf",
385
- variable=self.pdf_library_var,
386
- value="pypdf",
387
- ).pack(side='left', padx=5, pady=1)
388
-
389
- export_group_frame = ttk.LabelFrame(control_frame, text="Export Format:")
390
- #export_group_frame = ttk.LabelFrame(control_frame, text = "Export Filetype Selection:")
391
- export_group_frame.grid(row=1, column=2, padx=5, pady=5, sticky='nseew') # Placed in the original Checkbutton's column
392
-
393
- ttk.Checkbutton(
394
- export_group_frame,
395
- #text="Export Report",
396
- text = "JSON" ,
397
- variable=self.do_export_report_json_var
398
- ).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
399
- ttk.Checkbutton(
400
- export_group_frame,
401
- text = "TXT" ,
402
- #state=tk.DISABLED,
403
- variable=self.do_export_report_txt_var,
404
- ).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
405
-
406
- # Row 3: Run Button, Export Filetype selection, License Button, and readme button
407
- # 1. Run Button (Spans columns 0 and 1)
408
- run_analysis_btn = ttk.Button(control_frame, text="▶ Run Analysis", command=self._run_report_gui, style='Accent.TButton')
409
- run_analysis_btn.grid(row=3, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
410
-
411
- """
412
- # 2. Create a Frame to hold the two file link buttons (This frame goes into column 2)
413
- info_btn_frame = ttk.Frame(control_frame)
414
- info_btn_frame.grid(row=3, column=2, columnspan=1, pady=10, sticky='ew', padx=(5, 0))
415
- # Ensure the info button frame expands to fill its column
416
- info_btn_frame.grid_columnconfigure(0, weight=1)
417
- info_btn_frame.grid_columnconfigure(1, weight=1)
418
-
419
- # 3. Placeholder buttons inside the info button frame
420
- info_1_btn = ttk.Button(info_btn_frame, text="Empty1", command=self._do_stuff_1)
421
- # Use PACK or a 2-column GRID inside the info_btn_frame. GRID is cleaner here.
422
- info_1_btn.grid(row=0, column=0, sticky='ew', padx=(0, 2)) # Left side of the frame
423
-
424
- info_2_btn = ttk.Button(info_btn_frame, text="Empty2", command=self._do_stuff_2)
425
- info_2_btn.grid(row=0, column=1, sticky='ew', padx=(2, 0)) # Right side of the frame
426
- """
427
-
428
- # Force the columns to distribute space evenly
429
- control_frame.grid_columnconfigure(0, weight=2)
185
+ # === Row 3: Action Buttons ===
186
+ run_analysis_btn = ttk.Button(control_frame, text="▶ Run Analysis", command=self._run_report_gui, style='Accent.TButton', width=16)
187
+ run_analysis_btn.grid(row=3, column=0, columnspan=2, pady=6, sticky='ew', padx=(0, 3))
188
+
189
+ clear_window_btn = ttk.Button(control_frame, text="Clear Output Window", command=self._clear_output_window, width=18)
190
+ clear_window_btn.grid(row=3, column=2, pady=6, sticky='ew', padx=3)
191
+
192
+ # Grid configuration
193
+ control_frame.grid_columnconfigure(0, weight=1)
430
194
  control_frame.grid_columnconfigure(1, weight=1)
431
195
  control_frame.grid_columnconfigure(2, weight=1)
432
196
 
433
197
  # --- Output Frame (Bottom) ---
434
- output_frame = ttk.Frame(self, padding=(10, 2, 10, 10)) # Left, Top, Right, Bottom
198
+ output_frame = ttk.Frame(self.root, padding=(4, 2, 4, 4))
435
199
  output_frame.pack(fill='both', expand=True)
436
200
 
437
201
  output_header_frame = ttk.Frame(output_frame)
438
- output_header_frame.pack(fill='x', pady=(0, 5))
439
-
440
- # Label
441
- ttk.Label(output_header_frame, text="Analysis Report Output:").pack(side=tk.LEFT, fill='x', expand=True)
202
+ output_header_frame.pack(fill='x', pady=(0, 1))
442
203
 
443
- # Scroll to Bottom Button # put this first so that it on the right when the Top button is added on the left.
444
- bottom_btn = ttk.Button(output_header_frame, text="▼ Bottom", command=self._scroll_to_bottom, width=8)
445
- bottom_btn.pack(side=tk.RIGHT, padx=(0, 5))
204
+ ttk.Label(output_header_frame, text="Analysis Report Logs:").pack(side=tk.LEFT, fill='x', expand=True)
446
205
 
447
- # Scroll to Top Button
448
- top_btn = ttk.Button(output_header_frame, text="▲ Top", command=self._scroll_to_top, width=6)
449
- top_btn.pack(side=tk.RIGHT, padx=(5, 5))
206
+ ttk.Button(output_header_frame, text="▼ Bottom", command=self._scroll_to_bottom, width=10).pack(side=tk.RIGHT, padx=(0, 2))
207
+ ttk.Button(output_header_frame, text="▲ Top", command=self._scroll_to_top, width=6).pack(side=tk.RIGHT, padx=2)
450
208
 
451
- # Open Report Button
452
- self.open_report_btn = ttk.Button(output_header_frame, text="Open Report", command=self._open_report_text)
453
- self.open_report_btn.pack(side=tk.RIGHT, padx=(5, 5))
454
-
455
-
456
- # ----------------------------------------------------
457
-
458
-
459
- # Scrollable Text Widget for output
460
- # Use an internal frame for text and scrollbar to ensure correct packing
209
+ # Scrollable Text Area
461
210
  text_scroll_frame = ttk.Frame(output_frame)
462
- text_scroll_frame.pack(fill='both', expand=True, padx=5, pady=5)
463
-
464
- self.output_text = tk.Text(text_scroll_frame, wrap=tk.WORD, state=tk.DISABLED, bg='#333333', fg='white', font=('Monospace', 10))
465
- self.output_text.pack(side=tk.LEFT, fill='both', expand=True) # Text fills and expands
211
+ text_scroll_frame.pack(fill='both', expand=True, padx=3, pady=3)
212
+
213
+ self.output_text = tk.Text(text_scroll_frame, wrap=tk.WORD, state=tk.DISABLED, bg='#2b2b2b', fg='#ffffff', font=('Monospace', 10))
214
+ self.output_text.pack(side=tk.LEFT, fill='both', expand=True)
466
215
 
467
- # Scrollbar (Scrollbar must be packed AFTER the text widget)
468
216
  scrollbar = ttk.Scrollbar(text_scroll_frame, command=self.output_text.yview)
469
217
  scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
470
- self.output_text['yscrollcommand'] = scrollbar.set # Link text widget back to scrollbar
218
+ self.output_text['yscrollcommand'] = scrollbar.set
219
+
220
+ # --- Event Handlers & Business Logic ---
471
221
 
472
222
  def _select_pdf(self):
473
223
  if self.pdf_path.get():
474
224
  initialdir = str(Path(self.pdf_path.get()).parent)
475
- else:
225
+ elif pyhabitat.is_msix():
476
226
  initialdir = str(Path.home())
227
+ else:
228
+ initialdir = str(Path.cwd())
477
229
 
478
230
  file_path = filedialog.askopenfilename(
479
231
  initialdir=initialdir,
@@ -483,233 +235,264 @@ class PDFLinkCheckerApp(tk.Tk):
483
235
  if file_path:
484
236
  self.pdf_path.set(get_friendly_path(file_path))
485
237
 
486
-
487
- def _toggle_max_links_entry(self):
488
- """Disables/enables the max_links entry based on show_all_links_var."""
489
- if self.show_all_links_var.get():
490
- self.max_links_entry.config(state=tk.DISABLED)
238
+ def _copy_pdf_path(self):
239
+ path_to_copy = self.pdf_path.get()
240
+ if path_to_copy:
241
+ try:
242
+ self.root.clipboard_clear()
243
+ self.root.clipboard_append(path_to_copy)
244
+ messagebox.showinfo("Copied", "PDF Path copied to clipboard.")
245
+ except tk.TclError as e:
246
+ messagebox.showerror("Copy Error", f"Clipboard access blocked: {e}")
491
247
  else:
492
- self.max_links_entry.config(state=tk.NORMAL)
493
-
494
- def _toggle_json_export(self):
495
- """Checkbox toggle for json filetype report."""
496
- if self.do_export_report_json_var.get():
497
- pass # placeholder # no side effects
498
-
499
- def _toggle_txt_export(self):
500
- """Checkbox toggle for TXT filetype report."""
501
- if self.do_export_report_txt_var.get():
502
- pass # placeholder # no side effects
503
-
504
-
505
-
506
- def _toggle_pdf_library(self):
507
- selected_lib = self.pdf_library_var.get().lower()
508
- try:
509
- self.pdf_library_var.set("pypdf" if selected_lib == "pymupdf" else "pymupdf")
510
- except Exception:
511
- pass
512
-
513
- def _assess_pdf_path_str(self):
514
- pdf_path_str = self.pdf_path.get().strip()
515
- if not pdf_path_str:
516
- pdf_path_str = get_first_pdf_in_cwd()
517
- if not pdf_path_str:
518
- self._display_error("Error: No PDF found in current directory.")
519
- return
520
-
521
- p = Path(pdf_path_str).expanduser().resolve()
522
-
523
- if not p.exists():
524
- self._display_error(f"Error: PDF file not found at: {p}")
525
- return
526
-
527
- # Use the resolved string version for the rest of the function
528
- pdf_path_str_assessed = str(p)
529
- return pdf_path_str_assessed
248
+ messagebox.showwarning("Copy Failed", "PDF Path field is empty.")
530
249
 
531
250
  def _run_report_gui(self):
532
-
533
251
  pdf_path_str = self._assess_pdf_path_str()
534
252
  if not pdf_path_str:
535
253
  return
536
254
 
537
- if self.show_all_links_var.get():
538
- max_links_to_pass = 0
539
- else:
540
- try:
541
- max_links_to_pass = int(self.max_links_var.get())
542
- if max_links_to_pass < 0:
543
- self._display_error("Error: Max Links must be a positive number (or use 'Show All').")
544
- return
545
- except ValueError:
546
- self._display_error("Error: Max Links must be an integer.")
547
- return
548
-
549
- export_format = None # default value, if selection is not made (if selection is not active)
550
255
  export_format = ""
551
256
  if self.do_export_report_json_var.get():
552
- export_format = export_format + "JSON"
257
+ export_format += "JSON"
553
258
  if self.do_export_report_txt_var.get():
554
- export_format = export_format + "TXT"
259
+ export_format += "TXT"
555
260
 
556
- pdf_library = self._discern_pdf_library()
261
+ pdf_library = self.pdf_library_var.get().lower()
557
262
 
558
-
559
- # 1. Clear previous output and enable editing
560
263
  self.output_text.config(state=tk.NORMAL)
561
264
  self.output_text.delete('1.0', tk.END)
562
265
 
563
- # 2. Redirect standard output to the Text widget
564
266
  original_stdout = sys.stdout
565
267
  sys.stdout = RedirectText(self.output_text)
566
-
268
+
567
269
  try:
568
- # 3. Call the core logic function
569
- #self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
570
270
  report_results = run_report_and_call_exports(
571
271
  pdf_path=pdf_path_str,
572
- max_links=max_links_to_pass,
573
272
  export_format=export_format,
574
- pdf_library = pdf_library,
273
+ pdf_library=pdf_library,
575
274
  )
576
275
  self.current_report_text = report_results.get("text", "")
577
276
  self.current_report_data = report_results.get("data", {})
578
277
 
579
- #self.output_text.insert(tk.END, "\n--- Analysis Complete ---\n")
278
+ self.last_json_path = report_results.get("files", {}).get("export_path_json")
279
+ self.last_txt_path = report_results.get("files", {}).get("export_path_txt")
580
280
 
581
281
  except Exception as e:
582
- # Inform the user in the GUI with a clean message
583
- messagebox.showinfo( # Changed from showwarning – less alarming
584
- "PyMuPDF Not Available",
585
- "PyMuPDF is not installed or not working.\n"
586
- "Switched to pypdf engine automatically.\n\n"
587
- "To use the faster PyMuPDF engine:\n"
588
- "1. Install it: pip install pymupdf\n"
589
- "2. Go to Tools → Clear Cache\n"
590
- "3. Run analysis again"
282
+ messagebox.showinfo(
283
+ "Engine Fallback",
284
+ f"Error encountered with {pdf_library}: {e}\n\nFalling back to pypdf."
591
285
  )
592
- self._toggle_pdf_library()
593
-
594
-
286
+ self.pdf_library_var.set("pypdf")
595
287
  finally:
596
- # 4. Restore standard output and disable editing
597
288
  sys.stdout = original_stdout
598
289
  self.output_text.config(state=tk.DISABLED)
599
- # -- This call to _toggle_pdf_library() fails currently --
600
-
601
- def _discern_pdf_library(self):
602
- selected_lib = self.pdf_library_var.get().lower()
603
-
604
- if selected_lib == "pymupdf":
605
- self._display_msg("Using high-speed PyMuPDF engine.")
606
- elif selected_lib == "pypdf":
607
- self._display_msg("Using pure-python pypdf engine.")
608
- return selected_lib
609
-
610
- def _display_msg(self, message):
611
- # Ensure output is in normal state to write
612
- original_state = self.output_text.cget('state')
613
- if original_state == tk.DISABLED:
614
- self.output_text.config(state=tk.NORMAL)
615
-
616
- #self.output_text.delete('1.0', tk.END)
617
- self.output_text.insert(tk.END, f"{message}\n", 'msg')
618
- self.output_text.tag_config('msg')#, foreground='red')
290
+
291
+ def _open_export_file(self, file_type: str):
292
+ target_path = self.last_json_path if file_type == "json" else self.last_txt_path
293
+
294
+ if target_path and Path(target_path).exists():
295
+ try:
296
+ #pyhabitat.edit_textfile(target_path)
297
+ threading.Thread(target=lambda: pyhabitat.edit_textfile(target_path), daemon=True).start()
298
+ except Exception as e:
299
+ messagebox.showerror("Open Error", f"Failed to open {file_type.upper()} report: {e}")
300
+ else:
301
+ messagebox.showwarning("File Not Found", f"The {file_type.upper()} report file does not exist.\n\nPlease click 'Run Analysis' to generate one.")
302
+
303
+ def _assess_pdf_path_str(self):
304
+ pdf_path_str = self.pdf_path.get().strip()
305
+ if not pdf_path_str:
306
+ pdf_path_str = get_first_pdf_in_cwd()
307
+ if not pdf_path_str:
308
+ self._display_error("No PDF found in current directory.")
309
+ return
310
+
311
+ p = Path(pdf_path_str).expanduser().resolve()
312
+ if not p.exists():
313
+ self._display_error(f"PDF file not found at: {p}")
314
+ return
315
+
316
+ return str(p)
317
+
318
+ # --- Utility Methods ---
319
+
320
+ def _clear_output_window(self):
321
+ self.output_text.config(state=tk.NORMAL)
322
+ self.output_text.delete('1.0', tk.END)
323
+ self.output_text.config(state=tk.DISABLED)
324
+
325
+ def _copy_output_to_clipboard(self):
326
+ content = self.output_text.get('1.0', tk.END)
327
+ self.root.clipboard_clear()
328
+ self.root.clipboard_append(content)
329
+ messagebox.showinfo("Clipboard", "Output buffer copied to clipboard.")
330
+
331
+ def _scroll_to_top(self):
332
+ self.output_text.see('1.0')
333
+
334
+ def _scroll_to_bottom(self):
619
335
  self.output_text.see(tk.END)
620
336
 
337
+ def _clear_all_caches(self):
338
+ clear_all_caches()
339
+ messagebox.showinfo("Caches Cleared", f"All caches have been cleared.\nPyMuPDF available: {pymupdf_is_available()}\nPDFium available: {pdfium_is_available()}")
340
+
621
341
  def _display_error(self, message):
622
- # Ensure output is in normal state to write
623
- original_state = self.output_text.cget('state')
624
- if original_state == tk.DISABLED:
625
- self.output_text.config(state=tk.NORMAL)
626
-
627
- #self.output_text.delete('1.0', tk.END)
342
+ self.output_text.config(state=tk.NORMAL)
628
343
  self.output_text.insert(tk.END, f"[ERROR] {message}\n", 'error')
629
344
  self.output_text.tag_config('error', foreground='red')
630
345
  self.output_text.see(tk.END)
631
-
632
- # Restore state
633
346
  self.output_text.config(state=tk.DISABLED)
634
347
 
635
- def _open_report_text(self):
636
- """Opens the LATEST analysis text in an editor, regardless of export settings."""
637
- # 1. Check our internal buffer, not the window or the disk
638
- if not self.current_report_text:
639
- messagebox.showwarning("Open Failed", "No analysis data available. Please run an analysis first.")
640
- return
348
+ # --- Modal Documentation Windows ---
349
+
350
+ def _show_license(self):
351
+ self._display_resource_window("LICENSE", "Software License")
641
352
 
353
+ def _show_readme(self):
354
+ self._display_resource_window("README.md", "pdflinkcheck README.md")
355
+
356
+ def _show_i_have_questions(self):
357
+ self._display_resource_window("I Have Questions.md", "I Have Questions.md")
358
+
359
+ def _display_resource_window(self, filename: str, title: str):
360
+ content = None
642
361
  try:
643
- # 2. Always create a 'viewing' file in a temp directory or .tmp folder
644
- # This prevents clobbering an actual user-saved report.
645
- pdf_name = Path(self.pdf_path.get()).stem if self.pdf_path.get() else "report"
646
- view_path = PDFLINKCHECK_HOME / f"LAST_REPORT_{pdf_name}.txt"
647
-
648
- # 3. Write our buffer to this 'View' file
649
- view_path.write_text(self.current_report_text, encoding="utf-8")
650
-
651
- # 4. Open with pyhabitat
652
- pyhabitat.edit_textfile(view_path)
653
-
362
+ content = (files("pdflinkcheck.data") / filename).read_text(encoding="utf-8")
363
+ except FileNotFoundError:
364
+ if is_in_git_repo():
365
+ messagebox.showinfo("Development Mode", f"Embedded {filename} not found.\nTrying to copy from project root...")
366
+ try:
367
+ from pdflinkcheck.datacopy import ensure_data_files_for_build
368
+ ensure_data_files_for_build()
369
+ content = (files("pdflinkcheck.data") / filename).read_text(encoding="utf-8")
370
+ except ImportError:
371
+ messagebox.showerror("Fallback Failed", "Cannot import datacopy module.")
372
+ return
373
+ except Exception as e:
374
+ messagebox.showerror("Copy Failed", f"Failed to copy {filename}: {e}")
375
+ return
376
+ else:
377
+ messagebox.showerror("Resource Missing", f"Embedded file '{filename}' not found.")
378
+ return
654
379
  except Exception as e:
655
- messagebox.showerror("View Error", f"Could not launch editor: {e}")
656
-
380
+ messagebox.showerror("Read Error", f"Failed to read {filename}: {e}")
381
+ return
382
+
383
+ content = sanitize_glyphs_for_tkinter(content)
384
+ #content = sanitize_glyphs_for_compatibility(content)
385
+
386
+ win = tk.Toplevel(self.root)
387
+ win.title(title)
388
+ win.geometry("700x500")
389
+
390
+ txt = tk.Text(win, wrap=tk.WORD, font=('Monospace', 10), padx=6, pady=6)
391
+ txt.insert(tk.END, content)
392
+ txt.config(state=tk.DISABLED)
393
+
394
+ sb = ttk.Scrollbar(win, command=txt.yview)
395
+ txt['yscrollcommand'] = sb.set
396
+
397
+ sb.pack(side=tk.RIGHT, fill=tk.Y)
398
+ txt.pack(fill='both', expand=True)
399
+
400
+ win.transient(self.root)
401
+ win.grab_set()
402
+
403
+ # --- Helper Functions ---
404
+
657
405
  def sanitize_glyphs_for_tkinter(text: str) -> str:
406
+ normalized = unicodedata.normalize('NFKD', text)
407
+ sanitized = normalized.encode('ascii', 'ignore').decode('utf-8')
408
+ return sanitized.replace(' ', ' ')
409
+
410
+ def sanitize_glyphs_for_compatibility(text: str) -> str:
658
411
  """
659
- Converts complex Unicode characters (like emojis and symbols)
660
- into their closest ASCII representation, ignoring those that
661
- cannot be mapped. This prevents the 'empty square' issue in Tkinter.
412
+ Replaces problematic glyphs with ASCII equivalents for WSL2/gedit compatibility.
413
+ Does not require external libraries like unidecode.
662
414
  """
663
- # 1. Normalize the text (NFKD converts composite characters to their base parts)
664
- normalized = unicodedata.normalize('NFKD', text)
415
+ # Define a explicit mapping for your validation glyphs
416
+ glyph_mapping = {
417
+ '✅': '[PASS]',
418
+ '🌐': '[WEB]',
419
+ '⚠️': '[WARN]',
420
+ '❌': '[FAIL]',
421
+ 'ℹ️': '[INFO]'
422
+ }
423
+
424
+ # 1. Manual replacement for known report glyphs
425
+ for glyph, replacement in glyph_mapping.items():
426
+ text = text.replace(glyph, replacement)
665
427
 
666
- # 2. Encode to ASCII and decode back.
667
- # The 'ignore' flag is crucial: it removes any characters
668
- # that don't have an ASCII representation.
428
+ # 2. Normalize and strip remaining non-ASCII (NFKD decomposes characters)
429
+ normalized = unicodedata.normalize('NFKD', text)
669
430
  sanitized = normalized.encode('ascii', 'ignore').decode('utf-8')
670
431
 
671
- # 3. Clean up any resulting double spaces or artifacts
672
- sanitized = sanitized.replace(' ', ' ')
673
- return sanitized
432
+ # 3. Clean up double spaces created by the stripping
433
+ return sanitized.replace(' ', ' ')
674
434
 
675
- def auto_close_window(root, delay_ms:int = 0):
676
- """
677
- Schedules the Tkinter window to be destroyed after a specified delay.
678
- """
679
- if delay_ms > 0:
680
- print(f"Window is set to automatically close in {delay_ms/1000} seconds.")
681
- root.after(delay_ms, root.destroy)
682
- else:
435
+ def start_gui(time_auto_close: int = 0):
436
+ # 1. Initialize Root and Splash instantly
437
+ root = tk.Tk()
438
+ root.withdraw() # Hide the ugly default window for a split second
439
+
440
+ from pdflinkcheck.splash import SplashFrame
441
+ splash = SplashFrame(root)
442
+ root.update() # Force drawing the splash screen
443
+
444
+ # app = PDFLinkCheckApp(root=root)
445
+ # App Initialization
446
+ print("pdflinkcheck: Initializing PDF Link Check Engine...")
447
+ try:
448
+ app = PDFLinkCheckApp(root=root)
449
+ except Exception as e:
450
+ print(f"Startup Error: {e}")
451
+ root.destroy()
683
452
  return
453
+
454
+ # === Artificial Loading Delay ===4
455
+ DEV_DELAY = False
456
+ if DEV_DELAY:
457
+ import time
458
+ for _ in range(40):
459
+ if not root.winfo_exists(): return
460
+ time.sleep(0.05)
461
+ root.update()
462
+ # ====================================
463
+
464
+ # Handover
465
+ if root.winfo_exists():
466
+ splash.teardown() # The Splash cleans itself up
467
+
468
+ # Re-center the MAIN app window before showing it
469
+ app_w, app_h = 700, 500
470
+ # Center and then reveal
471
+ center_window_on_primary(root, app_w, app_h)
472
+
473
+ root.deiconify()
474
+ # Restore window borders/decorations
475
+ #root.overrideredirect(False)
684
476
 
477
+ # Force a title update to kick the window manager
478
+ root.title(f"PDF Link Check v{get_version_from_pyproject()}")
685
479
 
480
+ root.lift()
481
+ root.wm_attributes("-topmost", True)
482
+ root.after(200, lambda: root.wm_attributes("-topmost", False))
686
483
 
687
- def start_gui(time_auto_close:int=0):
688
- """
689
- Entry point function to launch the application.
690
- """
691
- print("pdflinkcheck: start_gui ...")
692
-
693
- tk_app = PDFLinkCheckerApp()
694
-
695
- # Bring window to front
696
- tk_app.lift()
697
- #tk_app.attributes('-topmost', True)
698
- #tk_app.after(100, lambda: tk_app.attributes('-topmost', False))
699
- tk_app.wm_attributes("-topmost", True)
700
- tk_app.after(200, lambda: tk_app.wm_attributes("-topmost", False))
701
- tk_app.deiconify()
702
- tk_app.focus_force()
703
-
704
- # Win32 nudge (optional but helpful)
705
- if pyhabitat.on_windows():
706
- hwnd = tk_app.winfo_id()
707
- ctypes.windll.user32.SetForegroundWindow(hwnd)
708
-
709
- # Ths is called in the CLI by the --auto-close flag value, for CI scripted testing purposes (like in .github/workflows/build.yml)
710
- auto_close_window(tk_app, time_auto_close)
484
+ if pyhabitat.on_windows():
485
+ try:
486
+ hwnd = root.winfo_id()
487
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
488
+ except:
489
+ pass
490
+
491
+ if time_auto_close > 0:
492
+ root.after(time_auto_close, root.destroy)
711
493
 
712
- tk_app.mainloop()
494
+ root.focus_force()
495
+ root.mainloop()
713
496
  print("pdflinkcheck: gui closed.")
714
497
 
715
498
  if __name__ == "__main__":