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.
- pdflinkcheck/__init__.py +88 -21
- pdflinkcheck/__main__.py +6 -0
- pdflinkcheck/analysis_pdfium.py +131 -0
- pdflinkcheck/{analyze_pymupdf.py → analysis_pymupdf.py} +109 -145
- pdflinkcheck/{analyze_pypdf.py → analysis_pypdf.py} +67 -37
- pdflinkcheck/cli.py +111 -116
- pdflinkcheck/data/I Have Questions.md +51 -0
- pdflinkcheck/data/LICENSE +20 -654
- pdflinkcheck/data/README.md +65 -67
- pdflinkcheck/data/icons/BoxArt-1080x1080.png +0 -0
- pdflinkcheck/data/icons/Logo-150x150.png +0 -0
- pdflinkcheck/data/icons/Logo-300x300.png +0 -0
- pdflinkcheck/data/icons/Logo-71x71.png +0 -0
- pdflinkcheck/data/icons/PosterArt-720x1080.png +0 -0
- pdflinkcheck/data/icons/SmallLogo-44x44.png +0 -0
- pdflinkcheck/data/icons/SplashScreen-620x300.png +0 -0
- pdflinkcheck/data/icons/StoreLogo-50x50.png +0 -0
- pdflinkcheck/data/icons/WideLogo-310x150.png +0 -0
- pdflinkcheck/data/icons/red_pdf_512px.ico +0 -0
- pdflinkcheck/data/pyproject.toml +25 -37
- pdflinkcheck/data/themes/forest/forest-dark/border-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/border-invalid.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/card.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/check-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/combo-button-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/down.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/empty.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/notebook.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/off-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/on-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/radio-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/rect-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/right.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/scale-hor.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/scale-vert.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/separator.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/sizegrip.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/spin-button-up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tab-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/thumb-vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tree-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/tree-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark/vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-dark.tcl +536 -0
- pdflinkcheck/data/themes/forest/forest-light/border-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/border-invalid.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/card.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/check-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/combo-button-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/down.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/empty.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/notebook.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/off-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/on-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-tri-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/radio-unsel-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-accent-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/rect-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/right-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/right.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/scale-hor.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/scale-vert.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/separator.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/sizegrip.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-down-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-down-focus.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/spin-button-up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tab-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-hor-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/thumb-vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tree-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/tree-pressed.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/up.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-accent.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-basic.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light/vert-hover.png +0 -0
- pdflinkcheck/data/themes/forest/forest-light.tcl +544 -0
- pdflinkcheck/datacopy.py +18 -1
- pdflinkcheck/dev.py +12 -25
- pdflinkcheck/environment.py +76 -0
- pdflinkcheck/gui.py +366 -457
- pdflinkcheck/helpers.py +88 -0
- pdflinkcheck/io.py +27 -23
- pdflinkcheck/report.py +692 -121
- pdflinkcheck/security.py +189 -0
- pdflinkcheck/splash.py +38 -0
- pdflinkcheck/stdlib_server.py +14 -20
- pdflinkcheck/stdlib_server_alt.py +571 -0
- pdflinkcheck/tk_utils.py +188 -0
- pdflinkcheck/update_msix_version.py +49 -0
- pdflinkcheck/validate.py +129 -218
- pdflinkcheck/version_info.py +6 -3
- {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.2.29.dist-info}/METADATA +84 -81
- pdflinkcheck-1.2.29.dist-info/RECORD +183 -0
- pdflinkcheck-1.2.29.dist-info/WHEEL +5 -0
- {pdflinkcheck-1.1.73.dist-info → pdflinkcheck-1.2.29.dist-info}/entry_points.txt +0 -1
- pdflinkcheck-1.2.29.dist-info/licenses/LICENSE +27 -0
- pdflinkcheck-1.2.29.dist-info/licenses/LICENSE-MIT +9 -0
- pdflinkcheck-1.2.29.dist-info/top_level.txt +1 -0
- pdflinkcheck/analyze_pypdf_v2.py +0 -218
- pdflinkcheck-1.1.73.dist-info/RECORD +0 -21
- pdflinkcheck-1.1.73.dist-info/WHEEL +0 -4
- /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
|
|
6
|
+
from tkinter import filedialog, ttk, messagebox, PhotoImage
|
|
4
7
|
import sys
|
|
5
8
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
9
|
+
from typing import Optional
|
|
7
10
|
import unicodedata
|
|
8
11
|
from importlib.resources import files
|
|
9
12
|
import pyhabitat
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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)
|
|
34
|
-
self.text_widget.update_idletasks()
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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="
|
|
53
|
-
|
|
54
|
-
self.
|
|
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
|
-
|
|
62
|
-
self.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
self.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
102
|
-
"""
|
|
103
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
145
|
-
""
|
|
146
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
190
|
-
control_frame = ttk.Frame(self, padding="10")
|
|
191
|
-
control_frame.pack(fill='x')
|
|
144
|
+
"""Compact layout with reduced padding."""
|
|
192
145
|
|
|
193
|
-
#
|
|
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
|
|
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=
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
text="
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
ttk.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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=(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
#
|
|
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=
|
|
332
|
-
|
|
333
|
-
self.output_text = tk.Text(text_scroll_frame, wrap=tk.WORD, state=tk.DISABLED, bg='#
|
|
334
|
-
self.output_text.pack(side=tk.LEFT, fill='both', expand=True)
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
257
|
+
export_format += "JSON"
|
|
413
258
|
if self.do_export_report_txt_var.get():
|
|
414
|
-
export_format
|
|
259
|
+
export_format += "TXT"
|
|
415
260
|
|
|
416
|
-
pdf_library = self.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
316
|
+
return str(p)
|
|
317
|
+
|
|
318
|
+
# --- Utility Methods ---
|
|
457
319
|
|
|
458
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
331
|
+
def _scroll_to_top(self):
|
|
332
|
+
self.output_text.see('1.0')
|
|
484
333
|
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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("
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
#
|
|
555
|
-
|
|
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
|
-
#
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
563
|
-
|
|
564
|
-
return sanitized
|
|
432
|
+
# 3. Clean up double spaces created by the stripping
|
|
433
|
+
return sanitized.replace(' ', ' ')
|
|
565
434
|
|
|
566
|
-
def
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
491
|
+
if time_auto_close > 0:
|
|
492
|
+
root.after(time_auto_close, root.destroy)
|
|
585
493
|
|
|
586
|
-
|
|
494
|
+
root.focus_force()
|
|
495
|
+
root.mainloop()
|
|
587
496
|
print("pdflinkcheck: gui closed.")
|
|
588
497
|
|
|
589
498
|
if __name__ == "__main__":
|