pdflinkcheck 1.1.73__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 (186) hide show
  1. pdflinkcheck/__init__.py +88 -21
  2. pdflinkcheck/__main__.py +6 -0
  3. pdflinkcheck/analysis_pdfium.py +131 -0
  4. pdflinkcheck/{analyze_pymupdf.py → analysis_pymupdf.py} +109 -145
  5. pdflinkcheck/{analyze_pypdf.py → analysis_pypdf.py} +67 -37
  6. pdflinkcheck/cli.py +111 -116
  7. pdflinkcheck/data/I Have Questions.md +51 -0
  8. pdflinkcheck/data/LICENSE +20 -654
  9. pdflinkcheck/data/README.md +65 -67
  10. pdflinkcheck/data/icons/BoxArt-1080x1080.png +0 -0
  11. pdflinkcheck/data/icons/Logo-150x150.png +0 -0
  12. pdflinkcheck/data/icons/Logo-300x300.png +0 -0
  13. pdflinkcheck/data/icons/Logo-71x71.png +0 -0
  14. pdflinkcheck/data/icons/PosterArt-720x1080.png +0 -0
  15. pdflinkcheck/data/icons/SmallLogo-44x44.png +0 -0
  16. pdflinkcheck/data/icons/SplashScreen-620x300.png +0 -0
  17. pdflinkcheck/data/icons/StoreLogo-50x50.png +0 -0
  18. pdflinkcheck/data/icons/WideLogo-310x150.png +0 -0
  19. pdflinkcheck/data/icons/red_pdf_512px.ico +0 -0
  20. pdflinkcheck/data/pyproject.toml +25 -37
  21. pdflinkcheck/data/themes/forest/forest-dark/border-accent-hover.png +0 -0
  22. pdflinkcheck/data/themes/forest/forest-dark/border-accent.png +0 -0
  23. pdflinkcheck/data/themes/forest/forest-dark/border-basic.png +0 -0
  24. pdflinkcheck/data/themes/forest/forest-dark/border-hover.png +0 -0
  25. pdflinkcheck/data/themes/forest/forest-dark/border-invalid.png +0 -0
  26. pdflinkcheck/data/themes/forest/forest-dark/card.png +0 -0
  27. pdflinkcheck/data/themes/forest/forest-dark/check-accent.png +0 -0
  28. pdflinkcheck/data/themes/forest/forest-dark/check-basic.png +0 -0
  29. pdflinkcheck/data/themes/forest/forest-dark/check-hover.png +0 -0
  30. pdflinkcheck/data/themes/forest/forest-dark/check-tri-accent.png +0 -0
  31. pdflinkcheck/data/themes/forest/forest-dark/check-tri-basic.png +0 -0
  32. pdflinkcheck/data/themes/forest/forest-dark/check-tri-hover.png +0 -0
  33. pdflinkcheck/data/themes/forest/forest-dark/check-unsel-accent.png +0 -0
  34. pdflinkcheck/data/themes/forest/forest-dark/check-unsel-basic.png +0 -0
  35. pdflinkcheck/data/themes/forest/forest-dark/check-unsel-hover.png +0 -0
  36. pdflinkcheck/data/themes/forest/forest-dark/check-unsel-pressed.png +0 -0
  37. pdflinkcheck/data/themes/forest/forest-dark/combo-button-basic.png +0 -0
  38. pdflinkcheck/data/themes/forest/forest-dark/combo-button-focus.png +0 -0
  39. pdflinkcheck/data/themes/forest/forest-dark/combo-button-hover.png +0 -0
  40. pdflinkcheck/data/themes/forest/forest-dark/down.png +0 -0
  41. pdflinkcheck/data/themes/forest/forest-dark/empty.png +0 -0
  42. pdflinkcheck/data/themes/forest/forest-dark/hor-accent.png +0 -0
  43. pdflinkcheck/data/themes/forest/forest-dark/hor-basic.png +0 -0
  44. pdflinkcheck/data/themes/forest/forest-dark/hor-hover.png +0 -0
  45. pdflinkcheck/data/themes/forest/forest-dark/notebook.png +0 -0
  46. pdflinkcheck/data/themes/forest/forest-dark/off-accent.png +0 -0
  47. pdflinkcheck/data/themes/forest/forest-dark/off-basic.png +0 -0
  48. pdflinkcheck/data/themes/forest/forest-dark/off-hover.png +0 -0
  49. pdflinkcheck/data/themes/forest/forest-dark/on-accent.png +0 -0
  50. pdflinkcheck/data/themes/forest/forest-dark/on-basic.png +0 -0
  51. pdflinkcheck/data/themes/forest/forest-dark/on-hover.png +0 -0
  52. pdflinkcheck/data/themes/forest/forest-dark/radio-accent.png +0 -0
  53. pdflinkcheck/data/themes/forest/forest-dark/radio-basic.png +0 -0
  54. pdflinkcheck/data/themes/forest/forest-dark/radio-hover.png +0 -0
  55. pdflinkcheck/data/themes/forest/forest-dark/radio-tri-accent.png +0 -0
  56. pdflinkcheck/data/themes/forest/forest-dark/radio-tri-basic.png +0 -0
  57. pdflinkcheck/data/themes/forest/forest-dark/radio-tri-hover.png +0 -0
  58. pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-accent.png +0 -0
  59. pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-basic.png +0 -0
  60. pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-hover.png +0 -0
  61. pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-pressed.png +0 -0
  62. pdflinkcheck/data/themes/forest/forest-dark/rect-accent-hover.png +0 -0
  63. pdflinkcheck/data/themes/forest/forest-dark/rect-accent.png +0 -0
  64. pdflinkcheck/data/themes/forest/forest-dark/rect-basic.png +0 -0
  65. pdflinkcheck/data/themes/forest/forest-dark/rect-hover.png +0 -0
  66. pdflinkcheck/data/themes/forest/forest-dark/right.png +0 -0
  67. pdflinkcheck/data/themes/forest/forest-dark/scale-hor.png +0 -0
  68. pdflinkcheck/data/themes/forest/forest-dark/scale-vert.png +0 -0
  69. pdflinkcheck/data/themes/forest/forest-dark/separator.png +0 -0
  70. pdflinkcheck/data/themes/forest/forest-dark/sizegrip.png +0 -0
  71. pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-basic.png +0 -0
  72. pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-focus.png +0 -0
  73. pdflinkcheck/data/themes/forest/forest-dark/spin-button-up.png +0 -0
  74. pdflinkcheck/data/themes/forest/forest-dark/tab-accent.png +0 -0
  75. pdflinkcheck/data/themes/forest/forest-dark/tab-basic.png +0 -0
  76. pdflinkcheck/data/themes/forest/forest-dark/tab-hover.png +0 -0
  77. pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-accent.png +0 -0
  78. pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-basic.png +0 -0
  79. pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-hover.png +0 -0
  80. pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-accent.png +0 -0
  81. pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-basic.png +0 -0
  82. pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-hover.png +0 -0
  83. pdflinkcheck/data/themes/forest/forest-dark/tree-basic.png +0 -0
  84. pdflinkcheck/data/themes/forest/forest-dark/tree-pressed.png +0 -0
  85. pdflinkcheck/data/themes/forest/forest-dark/up.png +0 -0
  86. pdflinkcheck/data/themes/forest/forest-dark/vert-accent.png +0 -0
  87. pdflinkcheck/data/themes/forest/forest-dark/vert-basic.png +0 -0
  88. pdflinkcheck/data/themes/forest/forest-dark/vert-hover.png +0 -0
  89. pdflinkcheck/data/themes/forest/forest-dark.tcl +536 -0
  90. pdflinkcheck/data/themes/forest/forest-light/border-accent-hover.png +0 -0
  91. pdflinkcheck/data/themes/forest/forest-light/border-accent.png +0 -0
  92. pdflinkcheck/data/themes/forest/forest-light/border-basic.png +0 -0
  93. pdflinkcheck/data/themes/forest/forest-light/border-hover.png +0 -0
  94. pdflinkcheck/data/themes/forest/forest-light/border-invalid.png +0 -0
  95. pdflinkcheck/data/themes/forest/forest-light/card.png +0 -0
  96. pdflinkcheck/data/themes/forest/forest-light/check-accent.png +0 -0
  97. pdflinkcheck/data/themes/forest/forest-light/check-basic.png +0 -0
  98. pdflinkcheck/data/themes/forest/forest-light/check-hover.png +0 -0
  99. pdflinkcheck/data/themes/forest/forest-light/check-tri-accent.png +0 -0
  100. pdflinkcheck/data/themes/forest/forest-light/check-tri-basic.png +0 -0
  101. pdflinkcheck/data/themes/forest/forest-light/check-tri-hover.png +0 -0
  102. pdflinkcheck/data/themes/forest/forest-light/check-unsel-accent.png +0 -0
  103. pdflinkcheck/data/themes/forest/forest-light/check-unsel-basic.png +0 -0
  104. pdflinkcheck/data/themes/forest/forest-light/check-unsel-hover.png +0 -0
  105. pdflinkcheck/data/themes/forest/forest-light/check-unsel-pressed.png +0 -0
  106. pdflinkcheck/data/themes/forest/forest-light/combo-button-basic.png +0 -0
  107. pdflinkcheck/data/themes/forest/forest-light/combo-button-focus.png +0 -0
  108. pdflinkcheck/data/themes/forest/forest-light/combo-button-hover.png +0 -0
  109. pdflinkcheck/data/themes/forest/forest-light/down-focus.png +0 -0
  110. pdflinkcheck/data/themes/forest/forest-light/down.png +0 -0
  111. pdflinkcheck/data/themes/forest/forest-light/empty.png +0 -0
  112. pdflinkcheck/data/themes/forest/forest-light/hor-accent.png +0 -0
  113. pdflinkcheck/data/themes/forest/forest-light/hor-basic.png +0 -0
  114. pdflinkcheck/data/themes/forest/forest-light/hor-hover.png +0 -0
  115. pdflinkcheck/data/themes/forest/forest-light/notebook.png +0 -0
  116. pdflinkcheck/data/themes/forest/forest-light/off-accent.png +0 -0
  117. pdflinkcheck/data/themes/forest/forest-light/off-basic.png +0 -0
  118. pdflinkcheck/data/themes/forest/forest-light/off-hover.png +0 -0
  119. pdflinkcheck/data/themes/forest/forest-light/on-accent.png +0 -0
  120. pdflinkcheck/data/themes/forest/forest-light/on-basic.png +0 -0
  121. pdflinkcheck/data/themes/forest/forest-light/on-hover.png +0 -0
  122. pdflinkcheck/data/themes/forest/forest-light/radio-accent.png +0 -0
  123. pdflinkcheck/data/themes/forest/forest-light/radio-basic.png +0 -0
  124. pdflinkcheck/data/themes/forest/forest-light/radio-hover.png +0 -0
  125. pdflinkcheck/data/themes/forest/forest-light/radio-tri-accent.png +0 -0
  126. pdflinkcheck/data/themes/forest/forest-light/radio-tri-basic.png +0 -0
  127. pdflinkcheck/data/themes/forest/forest-light/radio-tri-hover.png +0 -0
  128. pdflinkcheck/data/themes/forest/forest-light/radio-unsel-accent.png +0 -0
  129. pdflinkcheck/data/themes/forest/forest-light/radio-unsel-basic.png +0 -0
  130. pdflinkcheck/data/themes/forest/forest-light/radio-unsel-hover.png +0 -0
  131. pdflinkcheck/data/themes/forest/forest-light/radio-unsel-pressed.png +0 -0
  132. pdflinkcheck/data/themes/forest/forest-light/rect-accent-hover.png +0 -0
  133. pdflinkcheck/data/themes/forest/forest-light/rect-accent.png +0 -0
  134. pdflinkcheck/data/themes/forest/forest-light/rect-basic.png +0 -0
  135. pdflinkcheck/data/themes/forest/forest-light/rect-hover.png +0 -0
  136. pdflinkcheck/data/themes/forest/forest-light/right-focus.png +0 -0
  137. pdflinkcheck/data/themes/forest/forest-light/right.png +0 -0
  138. pdflinkcheck/data/themes/forest/forest-light/scale-hor.png +0 -0
  139. pdflinkcheck/data/themes/forest/forest-light/scale-vert.png +0 -0
  140. pdflinkcheck/data/themes/forest/forest-light/separator.png +0 -0
  141. pdflinkcheck/data/themes/forest/forest-light/sizegrip.png +0 -0
  142. pdflinkcheck/data/themes/forest/forest-light/spin-button-down-basic.png +0 -0
  143. pdflinkcheck/data/themes/forest/forest-light/spin-button-down-focus.png +0 -0
  144. pdflinkcheck/data/themes/forest/forest-light/spin-button-up.png +0 -0
  145. pdflinkcheck/data/themes/forest/forest-light/tab-accent.png +0 -0
  146. pdflinkcheck/data/themes/forest/forest-light/tab-basic.png +0 -0
  147. pdflinkcheck/data/themes/forest/forest-light/tab-hover.png +0 -0
  148. pdflinkcheck/data/themes/forest/forest-light/thumb-hor-accent.png +0 -0
  149. pdflinkcheck/data/themes/forest/forest-light/thumb-hor-basic.png +0 -0
  150. pdflinkcheck/data/themes/forest/forest-light/thumb-hor-hover.png +0 -0
  151. pdflinkcheck/data/themes/forest/forest-light/thumb-vert-accent.png +0 -0
  152. pdflinkcheck/data/themes/forest/forest-light/thumb-vert-basic.png +0 -0
  153. pdflinkcheck/data/themes/forest/forest-light/thumb-vert-hover.png +0 -0
  154. pdflinkcheck/data/themes/forest/forest-light/tree-basic.png +0 -0
  155. pdflinkcheck/data/themes/forest/forest-light/tree-pressed.png +0 -0
  156. pdflinkcheck/data/themes/forest/forest-light/up.png +0 -0
  157. pdflinkcheck/data/themes/forest/forest-light/vert-accent.png +0 -0
  158. pdflinkcheck/data/themes/forest/forest-light/vert-basic.png +0 -0
  159. pdflinkcheck/data/themes/forest/forest-light/vert-hover.png +0 -0
  160. pdflinkcheck/data/themes/forest/forest-light.tcl +544 -0
  161. pdflinkcheck/datacopy.py +18 -1
  162. pdflinkcheck/dev.py +12 -25
  163. pdflinkcheck/environment.py +76 -0
  164. pdflinkcheck/gui.py +366 -457
  165. pdflinkcheck/helpers.py +88 -0
  166. pdflinkcheck/io.py +27 -23
  167. pdflinkcheck/report.py +692 -121
  168. pdflinkcheck/security.py +189 -0
  169. pdflinkcheck/splash.py +38 -0
  170. pdflinkcheck/stdlib_server.py +14 -20
  171. pdflinkcheck/stdlib_server_alt.py +571 -0
  172. pdflinkcheck/tk_utils.py +188 -0
  173. pdflinkcheck/update_msix_version.py +49 -0
  174. pdflinkcheck/validate.py +129 -218
  175. pdflinkcheck/version_info.py +6 -3
  176. {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.2.29.dist-info}/METADATA +84 -81
  177. pdflinkcheck-1.2.29.dist-info/RECORD +183 -0
  178. pdflinkcheck-1.2.29.dist-info/WHEEL +5 -0
  179. {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.2.29.dist-info}/entry_points.txt +0 -1
  180. pdflinkcheck-1.2.29.dist-info/licenses/LICENSE +27 -0
  181. pdflinkcheck-1.2.29.dist-info/licenses/LICENSE-MIT +9 -0
  182. pdflinkcheck-1.2.29.dist-info/top_level.txt +1 -0
  183. pdflinkcheck/analyze_pypdf_v2.py +0 -218
  184. pdflinkcheck-1.1.73.dist-info/RECORD +0 -21
  185. pdflinkcheck-1.1.73.dist-info/WHEEL +0 -4
  186. /pdflinkcheck-1.1.73.dist-info/licenses/LICENSE → /pdflinkcheck-1.2.29.dist-info/licenses/LICENSE-AGPL3 +0 -0
pdflinkcheck/gui.py CHANGED
@@ -1,26 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
1
3
  # src/pdflinkcheck/gui.py
4
+ from __future__ import annotations
2
5
  import tkinter as tk
3
- from tkinter import filedialog, ttk, messagebox # Added messagebox
6
+ from tkinter import filedialog, ttk, messagebox, PhotoImage
4
7
  import sys
5
8
  from pathlib import Path
6
- from typing import Optional # Added Optional
9
+ from typing import Optional
7
10
  import unicodedata
8
11
  from importlib.resources import files
9
12
  import pyhabitat
10
- """
11
- try:
12
- import sv_ttk
13
- # Apply Sun Valley Tk theme
14
- sv_ttk.set_theme("light")
15
- except Exception:
16
- # Theme not available in bundle — use default
17
- pass
18
- """
19
- # Import the core analysis function
20
- from pdflinkcheck.report import run_report
21
- from pdflinkcheck.validate import run_validation
13
+ import ctypes
14
+ import threading
15
+
16
+ # --- Core Imports ---
17
+ from pdflinkcheck.report import run_report_and_call_exports
22
18
  from pdflinkcheck.version_info import get_version_from_pyproject
23
19
  from pdflinkcheck.io import get_first_pdf_in_cwd, get_friendly_path, PDFLINKCHECK_HOME
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
24
22
 
25
23
  class RedirectText:
26
24
  """A class to redirect sys.stdout messages to a Tkinter Text widget."""
@@ -28,319 +26,204 @@ class RedirectText:
28
26
  self.text_widget = text_widget
29
27
 
30
28
  def write(self, string):
31
- """Insert the incoming string into the Text widget."""
32
29
  self.text_widget.insert(tk.END, string)
33
- self.text_widget.see(tk.END) # Scroll to the end
34
- 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()
35
32
 
36
33
  def flush(self, *args):
37
- """Required for file-like objects, but does nothing here."""
38
34
  pass
39
35
 
40
- class PDFLinkCheckerApp(tk.Tk):
41
- def __init__(self):
42
- super().__init__()
43
- self.title(f"PDF Link Check v{get_version_from_pyproject()}")
44
- self.geometry("800x600")
45
-
46
- # Style for the application
47
- style = ttk.Style(self)
48
- style.theme_use('clam')
49
-
50
- # --- 1. Initialize Variables ---
36
+ class PDFLinkCheckApp:
37
+
38
+ # --- Lifecycle & Initialization ---
39
+
40
+ def __init__(self, root: tk.Tk):
41
+ self.root = root
42
+
43
+ # Do NOT load theme yet.
44
+ # Run the "heavy" initialization first
45
+ self._initialize_vars()
46
+
47
+ # NOW load the theme (this takes ~100-300ms)
48
+ self._initialize_forest_theme()
49
+
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")
59
+
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
63
+
64
+ self._set_icon()
65
+
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 ---
51
74
  self.pdf_path = tk.StringVar(value="")
52
- self.pdf_library_var = tk.StringVar(value="PyMuPDF")
53
- #self.pdf_library_var.set("PyMuPDF")
54
- self.max_links_var = tk.StringVar(value="50")
55
- self.show_all_links_var = tk.BooleanVar(value=True)
56
- self.do_export_report_json_var = tk.BooleanVar(value=True)
57
- self.do_export_report_txt_var = tk.BooleanVar(value=False)
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)
58
78
  self.current_report_text = None
59
79
  self.current_report_data = None
60
80
 
61
- self.supported_export_formats = ["JSON", "MD", "TXT"]
62
- self.supported_export_formats = ["JSON"]
63
-
64
-
65
- # --- 2. Create Widgets ---
66
- self._create_widgets()
67
-
68
- # --- 3. Set Initial Dependent Widget States ---
69
- self._toggle_max_links_entry()
70
- self._toggle_json_export()
71
- self._toggle_txt_export()
72
-
73
- # In class PDFLinkCheckerApp:
81
+ # Track exported file paths
82
+ self.last_json_path: Optional[Path] = None
83
+ self.last_txt_path: Optional[Path] = None
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")
89
+ if not pymupdf_is_available():
90
+ self.pdf_library_var.set("pypdf")
74
91
 
75
- def _copy_pdf_path(self):
76
- """Copies the current PDF path from the Entry widget to the system clipboard."""
77
- path_to_copy = self.pdf_path.get()
78
-
79
- if path_to_copy:
80
- try:
81
- # Clear the clipboard
82
- self.clipboard_clear()
83
- # Append the path string to the clipboard
84
- self.clipboard_append(path_to_copy)
85
- # Notify the user (optional, but good UX)
86
- messagebox.showinfo("Copied", "PDF Path copied to clipboard.")
87
- except tk.TclError as e:
88
- # Handle cases where clipboard access might be blocked
89
- messagebox.showerror("Copy Error", f"Failed to access the system clipboard: {e}")
90
- else:
91
- messagebox.showwarning("Copy Failed", "The PDF Path field is empty.")
92
92
 
93
- def _scroll_to_top(self):
94
- """Scrolls the output text widget to the top."""
95
- self.output_text.see('1.0') # '1.0' is the index for the very first character
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"))
98
+
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
96
121
 
97
- def _scroll_to_bottom(self):
98
- """Scrolls the output text widget to the bottom."""
99
- self.output_text.see(tk.END) # tk.END is the index for the position just after the last character
100
122
 
101
- def _show_license(self):
102
- """
103
- Reads the embedded LICENSE file (AGPLv3) and displays its content in a new modal window.
104
- """
105
- try:
106
- # CORRECT WAY: Use the Traversable object's read_text() method.
107
- # This handles files located inside zip archives (.pyz, pipx venvs) correctly.
108
- license_path_traversable = files("pdflinkcheck.data") / "LICENSE"
109
- license_content = license_path_traversable.read_text(encoding="utf-8")
110
-
111
- except FileNotFoundError:
112
- messagebox.showerror(
113
- "License Error",
114
- "LICENSE file not found within the installation package (pdflinkcheck.data/LICENSE). Check build process."
115
- )
116
- return
117
- except Exception as e:
118
- messagebox.showerror("Read Error", f"Failed to read embedded LICENSE file: {e}")
119
- return
123
+ def _initialize_menubar(self):
124
+ """Builds the application menu bar."""
125
+ menubar = tk.Menu(self.root)
126
+ self.root.config(menu=menubar)
120
127
 
121
- # --- Display in a New Toplevel Window ---
122
- license_window = tk.Toplevel(self)
123
- license_window.title("Software License")
124
- license_window.geometry("600x400")
125
-
126
- # Text widget for content
127
- text_widget = tk.Text(license_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
128
- text_widget.insert(tk.END, license_content)
129
- text_widget.config(state=tk.DISABLED)
130
-
131
- # Scrollbar
132
- scrollbar = ttk.Scrollbar(license_window, command=text_widget.yview)
133
- text_widget['yscrollcommand'] = scrollbar.set
134
-
135
- # Layout
136
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
137
- text_widget.pack(fill='both', expand=True)
138
-
139
- # Make the window modal (optional, but good practice for notices)
140
- license_window.transient(self)
141
- license_window.grab_set()
142
- self.wait_window(license_window)
128
+ tools_menu = tk.Menu(menubar, tearoff=0)
129
+ menubar.add_cascade(label="Tools", menu=tools_menu)
143
130
 
144
- def _show_readme(self):
145
- """
146
- Reads the embedded README.md file and displays its content in a new modal window.
147
- """
148
- try:
149
- # CORRECT WAY: Use the Traversable object's read_text() method.
150
- # This handles files located inside zip archives (.pyz, pipx venvs) correctly.
151
- readme_path_traversable = files("pdflinkcheck.data") / "README.md"
152
- readme_content = readme_path_traversable.read_text(encoding="utf-8")
153
- readme_content = sanitize_glyphs_for_tkinter(readme_content)
154
-
155
- except FileNotFoundError:
156
- messagebox.showerror(
157
- "Readme Error",
158
- "README.md file not found within the installation package (pdflinkcheck.data/README.md). Check build process."
159
- )
160
- return
161
- except Exception as e:
162
- messagebox.showerror("Read Error", f"Failed to read embedded README.md file: {e}")
163
- return
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)
134
+ tools_menu.add_command(label="Clear Cache", command=self._clear_all_caches)
164
135
 
165
- # --- Display in a New Toplevel Window ---
166
- readme_window = tk.Toplevel(self)
167
- readme_window.title("pdflinkcheck README.md")
168
- readme_window.geometry("600x400")
169
-
170
- # Text widget for content
171
- text_widget = tk.Text(readme_window, wrap=tk.WORD, font=('Monospace', 10), padx=10, pady=10)
172
- text_widget.insert(tk.END, readme_content)
173
- text_widget.config(state=tk.DISABLED)
174
-
175
- # Scrollbar
176
- scrollbar = ttk.Scrollbar(readme_window, command=text_widget.yview)
177
- text_widget['yscrollcommand'] = scrollbar.set
178
-
179
- # Layout
180
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
181
- text_widget.pack(fill='both', expand=True)
182
-
183
- # Make the window modal (optional, but good practice for notices)
184
- readme_window.transient(self)
185
- readme_window.grab_set()
186
- self.wait_window(readme_window)
136
+ tools_menu.add_separator()
137
+ tools_menu.add_command(label="License", command=self._show_license)
138
+ tools_menu.add_command(label="Readme", command=self._show_readme)
139
+ tools_menu.add_command(label="I Have Questions", command=self._show_i_have_questions)
140
+
141
+ # --- UI Component Building ---
187
142
 
188
143
  def _create_widgets(self):
189
- # --- Control Frame (Top) ---
190
- control_frame = ttk.Frame(self, padding="10")
191
- control_frame.pack(fill='x')
144
+ """Compact layout with reduced padding."""
192
145
 
193
- # Row 0: File Selection
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))
194
149
 
195
- # === File Selection Frame (Row 0) ===
150
+ # === Row 0: File Selection ===
196
151
  file_selection_frame = ttk.Frame(control_frame)
197
- file_selection_frame.grid(row=0, column=0, columnspan=3, padx=0, pady=5, sticky='ew')
198
-
199
- # Elements are now packed/gridded within file_selection_frame
200
-
201
- # Label
202
- ttk.Label(file_selection_frame, text="PDF Path:").pack(side=tk.LEFT, padx=(0, 5))
203
-
204
- # Entry (Path Display)
205
- ttk.Entry(file_selection_frame, textvariable=self.pdf_path, width=50).pack(side=tk.LEFT, fill='x', expand=True, padx=5)
206
- # The Entry field (column 1) must expand horizontally within its frame
207
- # Since we are using PACK for this frame, we use fill='x', expand=True on the Entry.
208
-
209
- # Browse Button
210
- ttk.Button(file_selection_frame, text="Browse...", command=self._select_pdf).pack(side=tk.LEFT, padx=(5, 5))
152
+ file_selection_frame.grid(row=0, column=0, columnspan=3, padx=0, pady=(2, 4), sticky='ew')
211
153
 
212
- # Copy Button
213
- # NOTE: Removed leading spaces from " Copy Path"
214
- ttk.Button(file_selection_frame, text="Copy Path", command=self._copy_pdf_path).pack(side=tk.LEFT, padx=(0, 0))
215
-
216
- # === END: File Selection Frame ===
217
-
218
- # --- Report brevity options ----
219
- report_brevity_frame = ttk.LabelFrame(control_frame, text="Report Brevity Options:")
220
- #report_brevity_frame.grid(row=1, column=0, columnspan=2, padx=5, pady=1, sticky='nsew')
221
- report_brevity_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nsew')
222
- #
223
- ttk.Checkbutton(
224
- report_brevity_frame,
225
- text="Show All Links.",
226
- variable=self.show_all_links_var,
227
- command=self._toggle_max_links_entry
228
- ).pack(side='left', padx=5, pady=1)
229
-
230
- ttk.Label(report_brevity_frame, text="Max Links to Display:").pack(side='left', padx=5, pady=1)
231
- self.max_links_entry = ttk.Entry(report_brevity_frame, textvariable=self.max_links_var, width=4)
232
- self.max_links_entry.pack(side='left', padx=5, pady=5)
233
-
234
- # --- PDF Library Selection ---
235
- # Create a labeled group for the PDF options
236
- pdf_library_frame = ttk.LabelFrame(control_frame, text="Select PDF Library:")
237
- pdf_library_frame.grid(row=1, column=1, padx=5, pady=5, sticky='nsew')
238
-
239
- # Radio options inside the frame
240
- ttk.Radiobutton(
241
- pdf_library_frame,
242
- text="PyMuPDF",
243
- variable=self.pdf_library_var,
244
- value="PyMuPDF",
245
-
246
- ).pack(side='left', padx=5, pady=1)
247
-
248
- ttk.Radiobutton(
249
- pdf_library_frame,
250
- text="pypdf",
251
- variable=self.pdf_library_var,
252
- value="pypdf",
253
- ).pack(side='left', padx=5, pady=1)
254
-
255
- export_group_frame = ttk.LabelFrame(control_frame, text="Export Format:")
256
- #export_group_frame = ttk.LabelFrame(control_frame, text = "Export Filetype Selection:")
257
- export_group_frame.grid(row=1, column=2, padx=5, pady=5, sticky='nseew') # Placed in the original Checkbutton's column
258
-
259
- ttk.Checkbutton(
260
- export_group_frame,
261
- #text="Export Report",
262
- text = "JSON" ,
263
- variable=self.do_export_report_json_var
264
- ).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
265
- ttk.Checkbutton(
266
- export_group_frame,
267
- text = "TXT" ,
268
- #state=tk.DISABLED,
269
- variable=self.do_export_report_txt_var,
270
- ).pack(side=tk.LEFT, padx=(0, 5)) # Pack Checkbutton to the left with small internal padding
271
-
272
- # Row 3: Run Button, Export Filetype selection, License Button, and readme button
273
- # 1. Run Button (Spans columns 0 and 1)
274
- run_analysis_btn = ttk.Button(control_frame, text="▶ Run Analysis", command=self._run_report_gui, style='Accent.TButton')
275
- run_analysis_btn.grid(row=3, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
276
-
277
- run_validation_btn = ttk.Button(control_frame, text="▶ Run Validation", command=self._run_validation_gui, style='Accent.TButton')
278
- run_validation_btn.grid(row=4, column=0, columnspan=2, pady=10, sticky='ew', padx=(0, 5))
279
- # Ensure the run button frame expands to fill its column
280
- #run_analysis_btn.grid_columnconfigure(0, weight=1)
281
-
282
- # 2. Create a Frame to hold the two file link buttons (This frame goes into column 2)
283
- info_btn_frame = ttk.Frame(control_frame)
284
- info_btn_frame.grid(row=3, column=2, columnspan=1, pady=10, sticky='ew', padx=(5, 0))
285
- # Ensure the info button frame expands to fill its column
286
- info_btn_frame.grid_columnconfigure(0, weight=1)
287
- info_btn_frame.grid_columnconfigure(1, weight=1)
288
-
289
- # 3. Place License and Readme buttons inside the new frame
290
- license_btn = ttk.Button(info_btn_frame, text="License", command=self._show_license)
291
- # Use PACK or a 2-column GRID inside the info_btn_frame. GRID is cleaner here.
292
- license_btn.grid(row=0, column=0, sticky='ew', padx=(0, 2)) # Left side of the frame
293
-
294
- readme_btn = ttk.Button(info_btn_frame, text="Readme", command=self._show_readme)
295
- readme_btn.grid(row=0, column=1, sticky='ew', padx=(2, 0)) # Right side of the frame
296
-
297
- # Force the columns to distribute space evenly
298
- control_frame.grid_columnconfigure(0, weight=2)
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))
159
+
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')
163
+
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)
169
+
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')
172
+
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)
175
+
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')
178
+
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)
181
+
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)
184
+
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)
299
194
  control_frame.grid_columnconfigure(1, weight=1)
300
195
  control_frame.grid_columnconfigure(2, weight=1)
301
196
 
302
197
  # --- Output Frame (Bottom) ---
303
- 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))
304
199
  output_frame.pack(fill='both', expand=True)
305
200
 
306
201
  output_header_frame = ttk.Frame(output_frame)
307
- output_header_frame.pack(fill='x', pady=(0, 5))
308
-
309
- # Label
310
- 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))
311
203
 
312
- # Scroll to Bottom Button # put this first so that it on the right when the Top button is added on the left.
313
- bottom_btn = ttk.Button(output_header_frame, text="▼ Bottom", command=self._scroll_to_bottom, width=8)
314
- 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)
315
205
 
316
- # Scroll to Top Button
317
- top_btn = ttk.Button(output_header_frame, text="▲ Top", command=self._scroll_to_top, width=6)
318
- 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)
319
208
 
320
- # Open Report Button
321
- self.open_report_btn = ttk.Button(output_header_frame, text="Open Report", command=self._open_report_text)
322
- self.open_report_btn.pack(side=tk.RIGHT, padx=(5, 5))
323
-
324
-
325
- # ----------------------------------------------------
326
-
327
-
328
- # Scrollable Text Widget for output
329
- # Use an internal frame for text and scrollbar to ensure correct packing
209
+ # Scrollable Text Area
330
210
  text_scroll_frame = ttk.Frame(output_frame)
331
- text_scroll_frame.pack(fill='both', expand=True, padx=5, pady=5)
332
-
333
- self.output_text = tk.Text(text_scroll_frame, wrap=tk.WORD, state=tk.DISABLED, bg='#333333', fg='white', font=('Monospace', 10))
334
- 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)
335
215
 
336
- # Scrollbar (Scrollbar must be packed AFTER the text widget)
337
216
  scrollbar = ttk.Scrollbar(text_scroll_frame, command=self.output_text.yview)
338
217
  scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
339
- 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 ---
340
221
 
341
222
  def _select_pdf(self):
342
223
  if self.pdf_path.get():
343
224
  initialdir = str(Path(self.pdf_path.get()).parent)
225
+ elif pyhabitat.is_msix():
226
+ initialdir = str(Path.home())
344
227
  else:
345
228
  initialdir = str(Path.cwd())
346
229
 
@@ -352,238 +235,264 @@ class PDFLinkCheckerApp(tk.Tk):
352
235
  if file_path:
353
236
  self.pdf_path.set(get_friendly_path(file_path))
354
237
 
355
-
356
- def _toggle_max_links_entry(self):
357
- """Disables/enables the max_links entry based on show_all_links_var."""
358
- if self.show_all_links_var.get():
359
- 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}")
360
247
  else:
361
- self.max_links_entry.config(state=tk.NORMAL)
362
-
363
- def _toggle_json_export(self):
364
- """Checkbox toggle for json filetype report."""
365
- if self.do_export_report_json_var.get():
366
- pass # placeholder # no side effects
367
-
368
- def _toggle_txt_export(self):
369
- """Checkbox toggle for TXT filetype report."""
370
- if self.do_export_report_txt_var.get():
371
- pass # placeholder # no side effects
372
-
373
- def _assess_pdf_path_str(self):
374
- pdf_path_str = self.pdf_path.get().strip()
375
- if not pdf_path_str:
376
- pdf_path_str = get_first_pdf_in_cwd()
377
- if not pdf_path_str:
378
- self._display_error("Error: No PDF found in current directory.")
379
- return
380
-
381
- p = Path(pdf_path_str).expanduser().resolve()
382
-
383
- if not p.exists():
384
- self._display_error(f"Error: PDF file not found at: {p}")
385
- return
386
-
387
- # Use the resolved string version for the rest of the function
388
- pdf_path_str_assessed = str(p)
389
- return pdf_path_str_assessed
248
+ messagebox.showwarning("Copy Failed", "PDF Path field is empty.")
390
249
 
391
250
  def _run_report_gui(self):
392
-
393
251
  pdf_path_str = self._assess_pdf_path_str()
394
252
  if not pdf_path_str:
395
253
  return
396
254
 
397
- if self.show_all_links_var.get():
398
- max_links_to_pass = 0
399
- else:
400
- try:
401
- max_links_to_pass = int(self.max_links_var.get())
402
- if max_links_to_pass < 1:
403
- self._display_error("Error: Max Links must be a positive number (or use 'Show All').")
404
- return
405
- except ValueError:
406
- self._display_error("Error: Max Links must be an integer.")
407
- return
408
-
409
- export_format = None # default value, if selection is not made (if selection is not active)
410
255
  export_format = ""
411
256
  if self.do_export_report_json_var.get():
412
- export_format = export_format + "JSON"
257
+ export_format += "JSON"
413
258
  if self.do_export_report_txt_var.get():
414
- export_format = export_format + "TXT"
259
+ export_format += "TXT"
415
260
 
416
- pdf_library = self._discern_pdf_library()
261
+ pdf_library = self.pdf_library_var.get().lower()
417
262
 
418
-
419
- # 1. Clear previous output and enable editing
420
263
  self.output_text.config(state=tk.NORMAL)
421
264
  self.output_text.delete('1.0', tk.END)
422
265
 
423
- # 2. Redirect standard output to the Text widget
424
266
  original_stdout = sys.stdout
425
267
  sys.stdout = RedirectText(self.output_text)
426
-
268
+
427
269
  try:
428
- # 3. Call the core logic function
429
- #self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
430
- report_results = run_report(
270
+ report_results = run_report_and_call_exports(
431
271
  pdf_path=pdf_path_str,
432
- max_links=max_links_to_pass,
433
272
  export_format=export_format,
434
- pdf_library = pdf_library,
273
+ pdf_library=pdf_library,
435
274
  )
436
275
  self.current_report_text = report_results.get("text", "")
437
276
  self.current_report_data = report_results.get("data", {})
438
277
 
439
- #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")
440
280
 
441
281
  except Exception as e:
442
- # Inform the user in the GUI with a clean message
443
- self._display_error(f"An unexpected error occurred during analysis: {e}")
444
-
282
+ messagebox.showinfo(
283
+ "Engine Fallback",
284
+ f"Error encountered with {pdf_library}: {e}\n\nFalling back to pypdf."
285
+ )
286
+ self.pdf_library_var.set("pypdf")
445
287
  finally:
446
- # 4. Restore standard output and disable editing
447
288
  sys.stdout = original_stdout
448
289
  self.output_text.config(state=tk.DISABLED)
449
-
450
- def _run_validation_gui(self):
451
-
452
- pdf_path_str = self._assess_pdf_path_str()
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()
453
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}")
454
314
  return
455
315
 
456
- pdf_library = self._discern_pdf_library()
316
+ return str(p)
317
+
318
+ # --- Utility Methods ---
457
319
 
458
- # 1. Clear previous output and enable editing
320
+ def _clear_output_window(self):
459
321
  self.output_text.config(state=tk.NORMAL)
460
322
  self.output_text.delete('1.0', tk.END)
323
+ self.output_text.config(state=tk.DISABLED)
461
324
 
462
- # 2. Redirect standard output to the Text widget
463
- original_stdout = sys.stdout
464
- sys.stdout = RedirectText(self.output_text)
465
-
466
- if not self.current_report_data:
467
- self._run_report_gui()
468
- report_results = self.current_report_data
469
-
470
- try:
471
- # 3. Call the core logic function
472
- #self.output_text.insert(tk.END, "--- Starting Analysis ---\n")
473
- validation_results = run_validation(
474
- report_results=report_results,
475
- pdf_path=pdf_path_str,
476
- pdf_library=pdf_library,
477
- export_json=True,
478
- print_bool=True
479
- )
480
- self.current_report_text = report_results.get("text", "")
481
- self.current_report_data = report_results.get("data", {})
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.")
482
330
 
483
- #self.output_text.insert(tk.END, "\n--- Analysis Complete ---\n")
331
+ def _scroll_to_top(self):
332
+ self.output_text.see('1.0')
484
333
 
485
- except Exception as e:
486
- # Inform the user in the GUI with a clean message
487
- self._display_error(f"An unexpected error occurred during analysis: {e}")
334
+ def _scroll_to_bottom(self):
335
+ self.output_text.see(tk.END)
488
336
 
489
- finally:
490
- # 4. Restore standard output and disable editing
491
- sys.stdout = original_stdout
492
- self.output_text.config(state=tk.DISABLED)
493
-
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()}")
494
340
 
495
- def _discern_pdf_library(self):
496
- selected_lib = self.pdf_library_var.get().lower()
497
-
498
- if selected_lib == "pymupdf":
499
- print("Using high-speed PyMuPDF engine.")
500
- elif selected_lib == "pypdf":
501
- print("Using pure-python pypdf engine.")
502
- return selected_lib
503
-
504
341
  def _display_error(self, message):
505
- # Ensure output is in normal state to write
506
- original_state = self.output_text.cget('state')
507
- if original_state == tk.DISABLED:
508
- self.output_text.config(state=tk.NORMAL)
509
-
510
- #self.output_text.delete('1.0', tk.END)
342
+ self.output_text.config(state=tk.NORMAL)
511
343
  self.output_text.insert(tk.END, f"[ERROR] {message}\n", 'error')
512
344
  self.output_text.tag_config('error', foreground='red')
513
345
  self.output_text.see(tk.END)
514
-
515
- # Restore state
516
346
  self.output_text.config(state=tk.DISABLED)
517
347
 
518
- def _open_report_text(self):
519
- """Opens the LATEST analysis text in an editor, regardless of export settings."""
520
- # 1. Check our internal buffer, not the window or the disk
521
- if not self.current_report_text:
522
- messagebox.showwarning("Open Failed", "No analysis data available. Please run an analysis first.")
523
- return
348
+ # --- Modal Documentation Windows ---
524
349
 
350
+ def _show_license(self):
351
+ self._display_resource_window("LICENSE", "Software License")
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
525
361
  try:
526
- # 2. Always create a 'viewing' file in a temp directory or .tmp folder
527
- # This prevents clobbering an actual user-saved report.
528
- pdf_name = Path(self.pdf_path.get()).stem if self.pdf_path.get() else "report"
529
- view_path = PDFLINKCHECK_HOME / f"LAST_REPORT_{pdf_name}.txt"
530
-
531
- # 3. Write our buffer to this 'View' file
532
- view_path.write_text(self.current_report_text, encoding="utf-8")
533
-
534
- # 4. Open with pyhabitat
535
- pyhabitat.edit_textfile(view_path)
536
-
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
537
379
  except Exception as e:
538
- messagebox.showerror("View Error", f"Could not launch editor: {e}")
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 ---
539
404
 
540
- """
541
- def toggle_theme():
542
- try:
543
- current = sv_ttk.get_theme()
544
- sv_ttk.set_theme("dark" if current == "light" else "light")
545
- except Exception:
546
- pass
547
- """
548
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:
549
411
  """
550
- Converts complex Unicode characters (like emojis and symbols)
551
- into their closest ASCII representation, ignoring those that
552
- 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.
553
414
  """
554
- # 1. Normalize the text (NFKD converts composite characters to their base parts)
555
- 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
+ }
556
423
 
557
- # 2. Encode to ASCII and decode back.
558
- # The 'ignore' flag is crucial: it removes any characters
559
- # that don't have an ASCII representation.
424
+ # 1. Manual replacement for known report glyphs
425
+ for glyph, replacement in glyph_mapping.items():
426
+ text = text.replace(glyph, replacement)
427
+
428
+ # 2. Normalize and strip remaining non-ASCII (NFKD decomposes characters)
429
+ normalized = unicodedata.normalize('NFKD', text)
560
430
  sanitized = normalized.encode('ascii', 'ignore').decode('utf-8')
561
431
 
562
- # 3. Clean up any resulting double spaces or artifacts
563
- sanitized = sanitized.replace(' ', ' ')
564
- return sanitized
432
+ # 3. Clean up double spaces created by the stripping
433
+ return sanitized.replace(' ', ' ')
565
434
 
566
- def auto_close_window(root, delay_ms:int = 0):
567
- """
568
- Schedules the Tkinter window to be destroyed after a specified delay.
569
- """
570
- if delay_ms > 0:
571
- print(f"Window is set to automatically close in {delay_ms/1000} seconds.")
572
- root.after(delay_ms, root.destroy)
573
- 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()
574
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
575
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)
576
476
 
577
- def start_gui(time_auto_close:int=0):
578
- """
579
- Entry point function to launch the application.
580
- """
581
- print("pdflinkcheck: start_gui ...")
582
- tk_app = PDFLinkCheckerApp()
477
+ # Force a title update to kick the window manager
478
+ root.title(f"PDF Link Check v{get_version_from_pyproject()}")
479
+
480
+ root.lift()
481
+ root.wm_attributes("-topmost", True)
482
+ root.after(200, lambda: root.wm_attributes("-topmost", False))
483
+
484
+ if pyhabitat.on_windows():
485
+ try:
486
+ hwnd = root.winfo_id()
487
+ ctypes.windll.user32.SetForegroundWindow(hwnd)
488
+ except:
489
+ pass
583
490
 
584
- auto_close_window(tk_app, time_auto_close)
491
+ if time_auto_close > 0:
492
+ root.after(time_auto_close, root.destroy)
585
493
 
586
- tk_app.mainloop()
494
+ root.focus_force()
495
+ root.mainloop()
587
496
  print("pdflinkcheck: gui closed.")
588
497
 
589
498
  if __name__ == "__main__":