edof 4.2.2__tar.gz → 4.2.4__tar.gz
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.
- {edof-4.2.2 → edof-4.2.4}/PKG-INFO +1 -1
- edof-4.2.4/edof/_apps/_assoc_dialog.py +92 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/__init__.py +18 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/cli.py +4 -3
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/editor.py +17 -33
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/file_assoc.py +133 -83
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/viewer.py +19 -35
- {edof-4.2.2 → edof-4.2.4}/edof/version.py +1 -1
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/PKG-INFO +1 -1
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/SOURCES.txt +1 -0
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/entry_points.txt +2 -0
- {edof-4.2.2 → edof-4.2.4}/pyproject.toml +4 -2
- {edof-4.2.2 → edof-4.2.4}/LICENSE +0 -0
- {edof-4.2.2 → edof-4.2.4}/README.md +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document-512.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.icns +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.ico +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor-512.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.icns +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.ico +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.icns +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.ico +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.png +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/edof_text_editor.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/_apps/shortcuts.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/api/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/api/commands.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/crypto/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/crypto/document_protection.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/crypto/encryption.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/crypto/permissions.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/editor_lang/en.json +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/color.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/debug_log.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/document_flow.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/document_paginate.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/renderer.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/text_engine.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/text_layout.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/textbox_flow.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/engine/transform.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/exceptions.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/bitmap.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/legacy_v3.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/pdf.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/pdf_writer.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/printer.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/export/svg.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/document.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/document_body.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/document_boxes.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/objects.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/serializer.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/styles.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/svg_io.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/format/variables.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/gui/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/gui/pyqt6_widget.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/gui/tkinter_canvas.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/interop/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/interop/docx_io.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/py.typed +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/units.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/__init__.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/compat.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/legacy_v2.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/pdf_import.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/qr.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/rtf.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof/utils/safe_eval.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/dependency_links.txt +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/requires.txt +0 -0
- {edof-4.2.2 → edof-4.2.4}/edof.egg-info/top_level.txt +0 -0
- {edof-4.2.2 → edof-4.2.4}/setup.cfg +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v310.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v400.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v401.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v402.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v403.py +0 -0
- {edof-4.2.2 → edof-4.2.4}/tests/test_v410.py +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared .edof file-association dialog.
|
|
2
|
+
|
|
3
|
+
The user chooses *here* (inside the EDOF app) which app opens .edof files on
|
|
4
|
+
double-click; the choice is then registered with the OS. Used by both the
|
|
5
|
+
editor and the viewer so the experience is identical.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def manage_association(parent=None):
|
|
11
|
+
from PyQt6.QtWidgets import (
|
|
12
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QRadioButton, QButtonGroup,
|
|
13
|
+
QPushButton, QMessageBox, QFrame,
|
|
14
|
+
)
|
|
15
|
+
from edof._apps.file_assoc import (
|
|
16
|
+
associate_edof_files, unassociate_edof_files, current_association_status,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
status = current_association_status()
|
|
20
|
+
is_assoc = "associated" in status.lower() and "not " not in status.lower()
|
|
21
|
+
editor_default = "editor" in status.lower()
|
|
22
|
+
|
|
23
|
+
dlg = QDialog(parent)
|
|
24
|
+
dlg.setWindowTitle("File association (.edof)")
|
|
25
|
+
dlg.setMinimumWidth(420)
|
|
26
|
+
v = QVBoxLayout(dlg)
|
|
27
|
+
|
|
28
|
+
v.addWidget(QLabel(f"<b>Current status:</b> {status}"))
|
|
29
|
+
info = QLabel(
|
|
30
|
+
"Choose which app opens a <code>.edof</code> file when you double-click "
|
|
31
|
+
"it. You decide here, inside EDOF; the other app stays available via "
|
|
32
|
+
"right-click \u2192 Open With. Files show the EDOF icon either way."
|
|
33
|
+
)
|
|
34
|
+
info.setWordWrap(True)
|
|
35
|
+
v.addWidget(info)
|
|
36
|
+
|
|
37
|
+
rb_view = QRadioButton("Open with EDOF Viewer (read-only)")
|
|
38
|
+
rb_edit = QRadioButton("Open with EDOF Editor")
|
|
39
|
+
grp = QButtonGroup(dlg)
|
|
40
|
+
grp.addButton(rb_view)
|
|
41
|
+
grp.addButton(rb_edit)
|
|
42
|
+
(rb_edit if editor_default else rb_view).setChecked(True)
|
|
43
|
+
v.addWidget(rb_view)
|
|
44
|
+
v.addWidget(rb_edit)
|
|
45
|
+
|
|
46
|
+
line = QFrame()
|
|
47
|
+
line.setFrameShape(QFrame.Shape.HLine)
|
|
48
|
+
v.addWidget(line)
|
|
49
|
+
|
|
50
|
+
row = QHBoxLayout()
|
|
51
|
+
btn_reg = QPushButton("Register / Update")
|
|
52
|
+
btn_rm = QPushButton("Remove association")
|
|
53
|
+
btn_rm.setEnabled(is_assoc)
|
|
54
|
+
btn_close = QPushButton("Close")
|
|
55
|
+
row.addWidget(btn_reg)
|
|
56
|
+
row.addWidget(btn_rm)
|
|
57
|
+
row.addStretch(1)
|
|
58
|
+
row.addWidget(btn_close)
|
|
59
|
+
v.addLayout(row)
|
|
60
|
+
|
|
61
|
+
note = QLabel(
|
|
62
|
+
"<small>On Windows this is per-user (no admin needed); you may need to "
|
|
63
|
+
"log out and back in for the Explorer icon to refresh.</small>"
|
|
64
|
+
)
|
|
65
|
+
note.setWordWrap(True)
|
|
66
|
+
v.addWidget(note)
|
|
67
|
+
|
|
68
|
+
def _do_register():
|
|
69
|
+
app = "editor" if rb_edit.isChecked() else "viewer"
|
|
70
|
+
try:
|
|
71
|
+
ok, msg = associate_edof_files(default_app=app)
|
|
72
|
+
(QMessageBox.information if ok else QMessageBox.warning)(
|
|
73
|
+
dlg, "File association", msg)
|
|
74
|
+
if ok:
|
|
75
|
+
dlg.accept()
|
|
76
|
+
except Exception as e: # noqa: BLE001
|
|
77
|
+
QMessageBox.critical(dlg, "File association", str(e))
|
|
78
|
+
|
|
79
|
+
def _do_remove():
|
|
80
|
+
try:
|
|
81
|
+
ok, msg = unassociate_edof_files()
|
|
82
|
+
(QMessageBox.information if ok else QMessageBox.warning)(
|
|
83
|
+
dlg, "File association", msg)
|
|
84
|
+
if ok:
|
|
85
|
+
dlg.accept()
|
|
86
|
+
except Exception as e: # noqa: BLE001
|
|
87
|
+
QMessageBox.critical(dlg, "File association", str(e))
|
|
88
|
+
|
|
89
|
+
btn_reg.clicked.connect(_do_register)
|
|
90
|
+
btn_rm.clicked.connect(_do_remove)
|
|
91
|
+
btn_close.clicked.connect(dlg.reject)
|
|
92
|
+
dlg.exec()
|
|
@@ -32,3 +32,21 @@ def app_icon_name(kind: str, ext: str = "ico") -> str:
|
|
|
32
32
|
"""Map a logical kind to an icon file name. kind in {editor,viewer,document}."""
|
|
33
33
|
kind = kind if kind in ("editor", "viewer", "document") else "document"
|
|
34
34
|
return f"edof-{kind}.{ext}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_windows_app_id(app_id: str) -> None:
|
|
38
|
+
"""Give this process its own Windows taskbar identity.
|
|
39
|
+
|
|
40
|
+
Without this, a GUI launched under python/pythonw is grouped under the host
|
|
41
|
+
interpreter and the taskbar shows the interpreter's icon instead of the
|
|
42
|
+
app's window icon. Must be called early, before any window is shown. No-op
|
|
43
|
+
off Windows.
|
|
44
|
+
"""
|
|
45
|
+
import sys
|
|
46
|
+
if sys.platform != "win32":
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
import ctypes
|
|
50
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(str(app_id))
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
@@ -796,6 +796,8 @@ Examples:
|
|
|
796
796
|
help="Remove the .edof file association instead of adding it")
|
|
797
797
|
pa.add_argument("--status", action="store_true",
|
|
798
798
|
help="Show current association status and exit")
|
|
799
|
+
pa.add_argument("--app", choices=["viewer", "editor"], default="viewer",
|
|
800
|
+
help="Which app opens .edof on double-click (default: viewer)")
|
|
799
801
|
|
|
800
802
|
return p
|
|
801
803
|
|
|
@@ -817,9 +819,8 @@ def cmd_associate_files(args):
|
|
|
817
819
|
sys.exit(1)
|
|
818
820
|
else:
|
|
819
821
|
try:
|
|
820
|
-
associate_edof_files()
|
|
821
|
-
print("OK:
|
|
822
|
-
print(" Double-clicking a .edof file will now open it in edof-viewer.")
|
|
822
|
+
ok, info = associate_edof_files(default_app=getattr(args, "app", "viewer"))
|
|
823
|
+
print(("OK: " if ok else "WARN: ") + info)
|
|
823
824
|
print(" On Windows, you may need to log out and back in for icons to refresh.")
|
|
824
825
|
except Exception as e:
|
|
825
826
|
print(f"ERROR: {e}")
|
|
@@ -10305,44 +10305,14 @@ class EdofEditor(QMainWindow):
|
|
|
10305
10305
|
dlg.exec()
|
|
10306
10306
|
|
|
10307
10307
|
def _open_file_assoc_dialog(self):
|
|
10308
|
-
"""Manage the .edof file association (register
|
|
10308
|
+
"""Manage the .edof file association (choose app in-app, register/remove)."""
|
|
10309
10309
|
try:
|
|
10310
|
-
from edof._apps.
|
|
10311
|
-
associate_edof_files, unassociate_edof_files,
|
|
10312
|
-
current_association_status,
|
|
10313
|
-
)
|
|
10310
|
+
from edof._apps._assoc_dialog import manage_association
|
|
10314
10311
|
except Exception as e:
|
|
10315
10312
|
QMessageBox.warning(self, "File association",
|
|
10316
10313
|
f"Could not load file association module: {e}")
|
|
10317
10314
|
return
|
|
10318
|
-
|
|
10319
|
-
is_assoc = "associated" in status.lower() and "not " not in status.lower()
|
|
10320
|
-
msg = (f"<h3>File association</h3>"
|
|
10321
|
-
f"<p>Current status: <b>{status}</b></p>"
|
|
10322
|
-
f"<p>When .edof files are registered, they show the EDOF icon in "
|
|
10323
|
-
f"your file manager and the first time you open one the system "
|
|
10324
|
-
f"lets you choose <b>EDOF Viewer</b> or <b>EDOF Editor</b>. No "
|
|
10325
|
-
f"default is forced, so you stay in control of which app opens "
|
|
10326
|
-
f"them.</p>"
|
|
10327
|
-
f"<p>{'<b>Currently registered.</b> Click OK to remove the association.' if is_assoc else 'Click OK to register .edof files.'}</p>")
|
|
10328
|
-
bb_text = "Unassociate" if is_assoc else "Associate"
|
|
10329
|
-
ret = QMessageBox.question(self, "File association", msg,
|
|
10330
|
-
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
|
|
10331
|
-
if ret != QMessageBox.StandardButton.Ok: return
|
|
10332
|
-
try:
|
|
10333
|
-
if is_assoc:
|
|
10334
|
-
ok, info = unassociate_edof_files()
|
|
10335
|
-
else:
|
|
10336
|
-
ok, info = associate_edof_files()
|
|
10337
|
-
if ok:
|
|
10338
|
-
QMessageBox.information(self, "File association",
|
|
10339
|
-
f"<p><b>Success.</b></p><p>{info}</p>")
|
|
10340
|
-
else:
|
|
10341
|
-
QMessageBox.warning(self, "File association",
|
|
10342
|
-
f"<p><b>Failed.</b></p><p>{info}</p>")
|
|
10343
|
-
except Exception as e:
|
|
10344
|
-
QMessageBox.critical(self, "File association",
|
|
10345
|
-
f"Error: {e}")
|
|
10315
|
+
manage_association(self)
|
|
10346
10316
|
|
|
10347
10317
|
# ─────────────────────────────────────────────────────────────────────
|
|
10348
10318
|
# v4.1.2: Multi-window support for embedded sub-documents
|
|
@@ -12310,6 +12280,13 @@ def _tpl_invoice():
|
|
|
12310
12280
|
|
|
12311
12281
|
|
|
12312
12282
|
def main():
|
|
12283
|
+
# v4.2.4: own taskbar identity so Windows shows the EDOF icon, not Python's.
|
|
12284
|
+
try:
|
|
12285
|
+
from edof._apps.assets import set_windows_app_id
|
|
12286
|
+
set_windows_app_id("DavidSchobl.EDOF.Editor")
|
|
12287
|
+
except Exception:
|
|
12288
|
+
pass
|
|
12289
|
+
|
|
12313
12290
|
# Remove Qt image allocation limit (default 256 MB blocks large print jobs)
|
|
12314
12291
|
try:
|
|
12315
12292
|
from PyQt6.QtGui import QImageReader
|
|
@@ -12351,6 +12328,13 @@ def main():
|
|
|
12351
12328
|
|
|
12352
12329
|
app=QApplication(sys.argv); app.setApplicationName("EDOF Editor")
|
|
12353
12330
|
app.setApplicationVersion(edof.__version__)
|
|
12331
|
+
try:
|
|
12332
|
+
from edof._apps.assets import icon_path
|
|
12333
|
+
_ip = icon_path("edof-editor.ico") or icon_path("edof-editor.png")
|
|
12334
|
+
if _ip:
|
|
12335
|
+
app.setWindowIcon(QIcon(_ip))
|
|
12336
|
+
except Exception:
|
|
12337
|
+
pass
|
|
12354
12338
|
try:
|
|
12355
12339
|
win=EdofEditor(sys.argv[1] if len(sys.argv)>1 else None)
|
|
12356
12340
|
win.show()
|
|
@@ -17,31 +17,46 @@ import subprocess
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
"""Locate
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
def _find_app_executable(cmd_name: str, module: str) -> str:
|
|
21
|
+
"""Locate a GUI launcher as an ABSOLUTE path, never a .bat/.cmd.
|
|
22
|
+
|
|
23
|
+
Order: (1) the generated exe next to the interpreter (Scripts/ for pip
|
|
24
|
+
installs), (2) PATH via shutil.which but rejecting .bat/.cmd and forced to
|
|
25
|
+
absolute, (3) fall back to running the module with the interpreter
|
|
26
|
+
(pythonw on Windows so no console appears). This avoids picking up a
|
|
27
|
+
launcher .bat from the current directory, which has no icon and breaks when
|
|
28
|
+
invoked with a relative path.
|
|
29
|
+
"""
|
|
30
|
+
exe_dir = os.path.dirname(os.path.abspath(sys.executable)) if sys.executable else ""
|
|
31
|
+
names = [cmd_name + ".exe", cmd_name] if os.name == "nt" else [cmd_name]
|
|
32
|
+
for d in ([os.path.join(exe_dir, "Scripts"), exe_dir] if exe_dir else []):
|
|
33
|
+
for n in names:
|
|
34
|
+
p = os.path.join(d, n)
|
|
35
|
+
if os.path.isfile(p) and not p.lower().endswith((".bat", ".cmd")):
|
|
36
|
+
return os.path.abspath(p)
|
|
37
|
+
found = shutil.which(cmd_name)
|
|
38
|
+
if found and not found.lower().endswith((".bat", ".cmd")):
|
|
39
|
+
return os.path.abspath(found)
|
|
26
40
|
if sys.executable:
|
|
27
|
-
|
|
41
|
+
py = os.path.abspath(sys.executable)
|
|
42
|
+
if os.name == "nt":
|
|
43
|
+
pyw = os.path.join(os.path.dirname(py), "pythonw.exe")
|
|
44
|
+
if os.path.isfile(pyw):
|
|
45
|
+
py = pyw
|
|
46
|
+
return f'"{py}" -m {module}'
|
|
28
47
|
raise RuntimeError(
|
|
29
|
-
"Could not locate
|
|
30
|
-
"Make sure edof[viewer] is installed: pip install 'edof[viewer]'"
|
|
48
|
+
f"Could not locate {cmd_name}. Install with: pip install 'edof[all]'"
|
|
31
49
|
)
|
|
32
50
|
|
|
33
51
|
|
|
52
|
+
def _find_viewer_executable():
|
|
53
|
+
"""Locate the edof-viewer launcher (absolute path, never a .bat)."""
|
|
54
|
+
return _find_app_executable("edof-viewer", "edof._apps.viewer")
|
|
55
|
+
|
|
56
|
+
|
|
34
57
|
def _find_editor_executable():
|
|
35
|
-
"""Locate the edof-editor
|
|
36
|
-
|
|
37
|
-
if exe:
|
|
38
|
-
return exe
|
|
39
|
-
if sys.executable:
|
|
40
|
-
return f'"{sys.executable}" -m edof._apps.editor'
|
|
41
|
-
raise RuntimeError(
|
|
42
|
-
"Could not locate edof-editor executable. "
|
|
43
|
-
"Make sure edof[editor] is installed: pip install 'edof[all]'"
|
|
44
|
-
)
|
|
58
|
+
"""Locate the edof-editor launcher (absolute path, never a .bat)."""
|
|
59
|
+
return _find_app_executable("edof-editor", "edof._apps.editor")
|
|
45
60
|
|
|
46
61
|
|
|
47
62
|
def _cmd_with_arg(exe: str) -> str:
|
|
@@ -58,12 +73,12 @@ def current_association_status() -> str:
|
|
|
58
73
|
import winreg
|
|
59
74
|
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".edof") as k:
|
|
60
75
|
cls, _ = winreg.QueryValueEx(k, "")
|
|
76
|
+
if cls == "edof.Document.Editor":
|
|
77
|
+
return "associated (opens in EDOF Editor on double-click)"
|
|
78
|
+
if cls == "edof.Document.Viewer":
|
|
79
|
+
return "associated (opens in EDOF Viewer on double-click)"
|
|
61
80
|
if cls and cls.startswith("edof.Document"):
|
|
62
|
-
|
|
63
|
-
# default opener, so check whether the open-with choices exist.
|
|
64
|
-
return ("associated (EDOF icon set; Windows asks which app "
|
|
65
|
-
"to open with)")
|
|
66
|
-
# Some other progid claims .edof
|
|
81
|
+
return "associated (no default app set yet)"
|
|
67
82
|
try:
|
|
68
83
|
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT,
|
|
69
84
|
f"{cls}\\shell\\open\\command") as k:
|
|
@@ -74,14 +89,18 @@ def current_association_status() -> str:
|
|
|
74
89
|
except OSError:
|
|
75
90
|
return "not associated"
|
|
76
91
|
elif sys.platform.startswith("linux"):
|
|
77
|
-
# Check xdg-mime
|
|
78
92
|
try:
|
|
79
93
|
r = subprocess.run(
|
|
80
94
|
["xdg-mime", "query", "default", "application/x-edof"],
|
|
81
95
|
capture_output=True, text=True, timeout=5
|
|
82
96
|
)
|
|
83
|
-
if r.returncode == 0
|
|
84
|
-
|
|
97
|
+
d = r.stdout.strip() if r.returncode == 0 else ""
|
|
98
|
+
if d == "edof-editor.desktop":
|
|
99
|
+
return "associated (opens in EDOF Editor on double-click)"
|
|
100
|
+
if d == "edof-viewer.desktop":
|
|
101
|
+
return "associated (opens in EDOF Viewer on double-click)"
|
|
102
|
+
if d:
|
|
103
|
+
return f"associated with: {d}"
|
|
85
104
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
86
105
|
pass
|
|
87
106
|
return "not associated"
|
|
@@ -90,12 +109,18 @@ def current_association_status() -> str:
|
|
|
90
109
|
return "unknown platform"
|
|
91
110
|
|
|
92
111
|
|
|
93
|
-
def associate_edof_files():
|
|
94
|
-
"""Register .edof files
|
|
112
|
+
def associate_edof_files(default_app: str = "viewer"):
|
|
113
|
+
"""Register .edof files for the current user.
|
|
114
|
+
|
|
115
|
+
`default_app` selects which app opens a .edof file on double-click and is
|
|
116
|
+
chosen *inside the EDOF app*, not by the OS picker. Accepts "viewer" or
|
|
117
|
+
"editor"; falls back to the viewer if the editor is not found.
|
|
118
|
+
"""
|
|
119
|
+
default_app = "editor" if str(default_app).lower().startswith("e") else "viewer"
|
|
95
120
|
if sys.platform == "win32":
|
|
96
|
-
return _associate_windows()
|
|
121
|
+
return _associate_windows(default_app)
|
|
97
122
|
if sys.platform.startswith("linux"):
|
|
98
|
-
return _associate_linux()
|
|
123
|
+
return _associate_linux(default_app)
|
|
99
124
|
if sys.platform == "darwin":
|
|
100
125
|
raise NotImplementedError(
|
|
101
126
|
"macOS file association requires building edof-viewer as an .app "
|
|
@@ -118,22 +143,19 @@ def unassociate_edof_files():
|
|
|
118
143
|
# Windows
|
|
119
144
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
120
145
|
|
|
121
|
-
def _associate_windows():
|
|
146
|
+
def _associate_windows(default_app: str = "viewer"):
|
|
122
147
|
"""Register .edof in HKCU\\Software\\Classes (per-user, no admin needed).
|
|
123
148
|
|
|
124
|
-
v4.2.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
the user picks their own default (and can change it any time). Files still
|
|
129
|
-
show the document icon in Explorer.
|
|
149
|
+
v4.2.3: the default opener is whatever the user chose *in the EDOF app*
|
|
150
|
+
(`default_app` = "viewer" or "editor"). That progid is set as the `.edof`
|
|
151
|
+
default so double-click opens it directly, no OS prompt. Both apps stay in
|
|
152
|
+
"Open with", and `.edof` files show the EDOF document icon either way.
|
|
130
153
|
"""
|
|
131
154
|
import winreg
|
|
132
155
|
|
|
133
156
|
classes = r"Software\Classes"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
progid_edit = "edof.Document.Editor" # Open-with choice: Editor
|
|
157
|
+
progid_view = "edof.Document.Viewer"
|
|
158
|
+
progid_edit = "edof.Document.Editor"
|
|
137
159
|
|
|
138
160
|
viewer = _find_viewer_executable()
|
|
139
161
|
cmd_view = _cmd_with_arg(viewer)
|
|
@@ -144,6 +166,11 @@ def _associate_windows():
|
|
|
144
166
|
editor = None
|
|
145
167
|
cmd_edit = None
|
|
146
168
|
|
|
169
|
+
# If the editor was requested but is unavailable, fall back to the viewer.
|
|
170
|
+
if default_app == "editor" and not editor:
|
|
171
|
+
default_app = "viewer"
|
|
172
|
+
default_progid = progid_edit if default_app == "editor" else progid_view
|
|
173
|
+
|
|
147
174
|
try:
|
|
148
175
|
from edof._apps.assets import icon_path
|
|
149
176
|
except Exception:
|
|
@@ -157,12 +184,10 @@ def _associate_windows():
|
|
|
157
184
|
0, winreg.KEY_WRITE) as k:
|
|
158
185
|
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, value)
|
|
159
186
|
|
|
160
|
-
# 1. .edof → the
|
|
161
|
-
# list. No "default opener" is set on the type, so Windows prompts the
|
|
162
|
-
# user to choose between the Viewer and the Editor.
|
|
187
|
+
# 1. .edof → the chosen default progid + the open-with list (both apps).
|
|
163
188
|
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\.edof",
|
|
164
189
|
0, winreg.KEY_WRITE) as k:
|
|
165
|
-
winreg.SetValueEx(k, "", 0, winreg.REG_SZ,
|
|
190
|
+
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, default_progid)
|
|
166
191
|
winreg.SetValueEx(k, "Content Type", 0, winreg.REG_SZ, "application/x-edof")
|
|
167
192
|
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER,
|
|
168
193
|
f"{classes}\\.edof\\OpenWithProgids",
|
|
@@ -171,32 +196,32 @@ def _associate_windows():
|
|
|
171
196
|
if editor:
|
|
172
197
|
winreg.SetValueEx(k, progid_edit, 0, winreg.REG_NONE, b"")
|
|
173
198
|
|
|
174
|
-
# 2.
|
|
175
|
-
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{progid_doc}",
|
|
176
|
-
0, winreg.KEY_WRITE) as k:
|
|
177
|
-
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, "EDOF Document")
|
|
178
|
-
winreg.SetValueEx(k, "FriendlyTypeName", 0, winreg.REG_SZ,
|
|
179
|
-
"Easy Document Format File")
|
|
180
|
-
if doc_icon and os.path.isfile(doc_icon):
|
|
181
|
-
_set_default(f"{progid_doc}\\DefaultIcon", f'"{doc_icon}",0')
|
|
182
|
-
|
|
183
|
-
# 3. Viewer open-with choice (own name + icon + open command).
|
|
199
|
+
# 2. Viewer progid: name + open command.
|
|
184
200
|
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{progid_view}",
|
|
185
201
|
0, winreg.KEY_WRITE) as k:
|
|
186
202
|
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, "EDOF Viewer")
|
|
187
|
-
|
|
188
|
-
|
|
203
|
+
winreg.SetValueEx(k, "FriendlyTypeName", 0, winreg.REG_SZ,
|
|
204
|
+
"Easy Document Format File")
|
|
189
205
|
_set_default(f"{progid_view}\\shell\\open\\command", cmd_view)
|
|
190
206
|
|
|
191
|
-
#
|
|
207
|
+
# 3. Editor progid: name + open command.
|
|
192
208
|
if cmd_edit:
|
|
193
209
|
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{progid_edit}",
|
|
194
210
|
0, winreg.KEY_WRITE) as k:
|
|
195
211
|
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, "EDOF Editor")
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
winreg.SetValueEx(k, "FriendlyTypeName", 0, winreg.REG_SZ,
|
|
213
|
+
"Easy Document Format File")
|
|
198
214
|
_set_default(f"{progid_edit}\\shell\\open\\command", cmd_edit)
|
|
199
215
|
|
|
216
|
+
# 4. Icons. The DEFAULT progid drives the Explorer file icon, so it gets the
|
|
217
|
+
# document icon; the other progid shows its own app icon under "Open with".
|
|
218
|
+
if doc_icon and os.path.isfile(doc_icon):
|
|
219
|
+
_set_default(f"{default_progid}\\DefaultIcon", f'"{doc_icon}",0')
|
|
220
|
+
if default_progid != progid_view and view_icon and os.path.isfile(view_icon):
|
|
221
|
+
_set_default(f"{progid_view}\\DefaultIcon", f'"{view_icon}",0')
|
|
222
|
+
if editor and default_progid != progid_edit and edit_icon and os.path.isfile(edit_icon):
|
|
223
|
+
_set_default(f"{progid_edit}\\DefaultIcon", f'"{edit_icon}",0')
|
|
224
|
+
|
|
200
225
|
# 5. Notify the shell that associations changed
|
|
201
226
|
try:
|
|
202
227
|
import ctypes
|
|
@@ -207,12 +232,10 @@ def _associate_windows():
|
|
|
207
232
|
except Exception:
|
|
208
233
|
pass
|
|
209
234
|
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return True, (".edof registered with the Viewer as an open-with choice. "
|
|
215
|
-
"(Editor not found, so only the Viewer was registered.)")
|
|
235
|
+
chosen = "Editor" if default_progid == progid_edit else "Viewer"
|
|
236
|
+
return True, (f".edof registered. Double-click opens the {chosen}; the other "
|
|
237
|
+
f"app stays available under right-click \u2192 Open with. "
|
|
238
|
+
f"Files show the EDOF icon.")
|
|
216
239
|
|
|
217
240
|
|
|
218
241
|
def _unassociate_windows():
|
|
@@ -267,12 +290,12 @@ def _unassociate_windows():
|
|
|
267
290
|
DESKTOP_ENTRY = """\
|
|
268
291
|
[Desktop Entry]
|
|
269
292
|
Type=Application
|
|
270
|
-
Name=
|
|
271
|
-
Comment=
|
|
293
|
+
Name={name}
|
|
294
|
+
Comment={comment}
|
|
272
295
|
Exec={exec_cmd} %f
|
|
273
|
-
Terminal=false
|
|
296
|
+
{icon_line}Terminal=false
|
|
274
297
|
MimeType=application/x-edof;
|
|
275
|
-
Categories=Office;
|
|
298
|
+
Categories=Office;
|
|
276
299
|
"""
|
|
277
300
|
|
|
278
301
|
MIME_XML = """\
|
|
@@ -286,40 +309,67 @@ MIME_XML = """\
|
|
|
286
309
|
"""
|
|
287
310
|
|
|
288
311
|
|
|
289
|
-
def _associate_linux():
|
|
312
|
+
def _associate_linux(default_app: str = "viewer"):
|
|
290
313
|
home = Path.home()
|
|
291
314
|
apps_dir = home / ".local" / "share" / "applications"
|
|
292
315
|
mime_dir = home / ".local" / "share" / "mime" / "packages"
|
|
293
316
|
apps_dir.mkdir(parents=True, exist_ok=True)
|
|
294
317
|
mime_dir.mkdir(parents=True, exist_ok=True)
|
|
295
318
|
|
|
296
|
-
|
|
319
|
+
try:
|
|
320
|
+
from edof._apps.assets import icon_path
|
|
321
|
+
except Exception:
|
|
322
|
+
icon_path = lambda *_a, **_k: None # noqa: E731
|
|
323
|
+
|
|
324
|
+
def _icon_line(name):
|
|
325
|
+
p = icon_path(name)
|
|
326
|
+
return f"Icon={p}\n" if p and os.path.isfile(p) else ""
|
|
297
327
|
|
|
298
328
|
# 1. MIME type
|
|
299
329
|
(mime_dir / "edof.xml").write_text(MIME_XML, encoding="utf-8")
|
|
300
330
|
|
|
301
|
-
# 2. .desktop
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
DESKTOP_ENTRY.format(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
331
|
+
# 2. .desktop files for BOTH apps (so either can be the default).
|
|
332
|
+
viewer = _find_viewer_executable()
|
|
333
|
+
(apps_dir / "edof-viewer.desktop").write_text(
|
|
334
|
+
DESKTOP_ENTRY.format(name="EDOF Viewer", comment="View EDOF documents",
|
|
335
|
+
exec_cmd=viewer, icon_line=_icon_line("edof-viewer.png")),
|
|
336
|
+
encoding="utf-8")
|
|
337
|
+
(apps_dir / "edof-viewer.desktop").chmod(0o755)
|
|
338
|
+
|
|
339
|
+
editor_ok = False
|
|
340
|
+
try:
|
|
341
|
+
editor = _find_editor_executable()
|
|
342
|
+
(apps_dir / "edof-editor.desktop").write_text(
|
|
343
|
+
DESKTOP_ENTRY.format(name="EDOF Editor", comment="Edit EDOF documents",
|
|
344
|
+
exec_cmd=editor, icon_line=_icon_line("edof-editor.png")),
|
|
345
|
+
encoding="utf-8")
|
|
346
|
+
(apps_dir / "edof-editor.desktop").chmod(0o755)
|
|
347
|
+
editor_ok = True
|
|
348
|
+
except RuntimeError:
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
if default_app == "editor" and not editor_ok:
|
|
352
|
+
default_app = "viewer"
|
|
353
|
+
default_desktop = ("edof-editor.desktop" if default_app == "editor"
|
|
354
|
+
else "edof-viewer.desktop")
|
|
308
355
|
|
|
309
|
-
# 3. Update
|
|
356
|
+
# 3. Update databases + set the chosen default handler.
|
|
310
357
|
for cmd in (["update-mime-database", str(home / ".local" / "share" / "mime")],
|
|
311
|
-
|
|
312
|
-
|
|
358
|
+
["update-desktop-database", str(apps_dir)],
|
|
359
|
+
["xdg-mime", "default", default_desktop, "application/x-edof"]):
|
|
313
360
|
try:
|
|
314
361
|
subprocess.run(cmd, check=False, capture_output=True, timeout=10)
|
|
315
362
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
316
363
|
pass
|
|
317
|
-
|
|
364
|
+
chosen = "Editor" if default_desktop.endswith("editor.desktop") else "Viewer"
|
|
365
|
+
return True, (f".edof associated; double-click opens the {chosen}. "
|
|
366
|
+
f"Both apps are available via right-click \u2192 Open With.")
|
|
318
367
|
|
|
319
368
|
|
|
320
369
|
def _unassociate_linux():
|
|
321
370
|
home = Path.home()
|
|
322
371
|
for f in (home / ".local" / "share" / "applications" / "edof-viewer.desktop",
|
|
372
|
+
home / ".local" / "share" / "applications" / "edof-editor.desktop",
|
|
323
373
|
home / ".local" / "share" / "mime" / "packages" / "edof.xml"):
|
|
324
374
|
try:
|
|
325
375
|
f.unlink(missing_ok=True)
|
|
@@ -534,43 +534,13 @@ class EdofViewer(QMainWindow):
|
|
|
534
534
|
)
|
|
535
535
|
|
|
536
536
|
def _on_associate(self):
|
|
537
|
-
from edof._apps.file_assoc import (
|
|
538
|
-
associate_edof_files, unassociate_edof_files,
|
|
539
|
-
current_association_status,
|
|
540
|
-
)
|
|
541
|
-
status = current_association_status()
|
|
542
|
-
is_assoc = "associated" in status.lower() and "not " not in status.lower()
|
|
543
|
-
if is_assoc:
|
|
544
|
-
msg = ("Remove the .edof file association?\n\n"
|
|
545
|
-
"Explorer will stop showing the EDOF icon for .edof files "
|
|
546
|
-
"and will no longer offer EDOF apps to open them.\n\n"
|
|
547
|
-
f"Current status: {status}")
|
|
548
|
-
title, verb = "Remove association", "remove"
|
|
549
|
-
else:
|
|
550
|
-
msg = ("Register .edof files?\n\n"
|
|
551
|
-
"Files will show the EDOF icon, and the first time you open "
|
|
552
|
-
"one your system will let you choose the Viewer or the "
|
|
553
|
-
"Editor (no default is forced).\n\n"
|
|
554
|
-
f"Current status: {status}")
|
|
555
|
-
title, verb = "Register .edof files", "register"
|
|
556
|
-
reply = QMessageBox.question(self, title, msg,
|
|
557
|
-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
558
|
-
if reply != QMessageBox.StandardButton.Yes:
|
|
559
|
-
return
|
|
560
537
|
try:
|
|
561
|
-
|
|
562
|
-
else associate_edof_files())
|
|
563
|
-
if ok:
|
|
564
|
-
QMessageBox.information(self, "Done",
|
|
565
|
-
f"{info}\n\n"
|
|
566
|
-
"On Windows you may need to log out and back in for the "
|
|
567
|
-
"icons in Explorer to refresh.")
|
|
568
|
-
else:
|
|
569
|
-
QMessageBox.warning(self, "Could not " + verb, info)
|
|
538
|
+
from edof._apps._assoc_dialog import manage_association
|
|
570
539
|
except Exception as e:
|
|
571
|
-
QMessageBox.warning(self, "
|
|
572
|
-
|
|
573
|
-
|
|
540
|
+
QMessageBox.warning(self, "File association",
|
|
541
|
+
f"Could not load file association module: {e}")
|
|
542
|
+
return
|
|
543
|
+
manage_association(self)
|
|
574
544
|
|
|
575
545
|
def _on_donate(self):
|
|
576
546
|
try:
|
|
@@ -611,9 +581,23 @@ def main():
|
|
|
611
581
|
print("Install with: pip install edof[viewer]")
|
|
612
582
|
sys.exit(1)
|
|
613
583
|
|
|
584
|
+
# v4.2.4: own taskbar identity so Windows shows the EDOF icon, not Python's.
|
|
585
|
+
try:
|
|
586
|
+
from edof._apps.assets import set_windows_app_id
|
|
587
|
+
set_windows_app_id("DavidSchobl.EDOF.Viewer")
|
|
588
|
+
except Exception:
|
|
589
|
+
pass
|
|
590
|
+
|
|
614
591
|
app = QApplication(sys.argv)
|
|
615
592
|
app.setApplicationName("EDOF Viewer")
|
|
616
593
|
app.setApplicationVersion(edof.__version__)
|
|
594
|
+
try:
|
|
595
|
+
from edof._apps.assets import icon_path
|
|
596
|
+
_ip = icon_path("edof-viewer.ico") or icon_path("edof-viewer.png")
|
|
597
|
+
if _ip:
|
|
598
|
+
app.setWindowIcon(QIcon(_ip))
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
617
601
|
|
|
618
602
|
# Optional file argument
|
|
619
603
|
filepath = None
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "edof"
|
|
7
|
-
version = "4.2.
|
|
7
|
+
version = "4.2.4"
|
|
8
8
|
description = "Easy Document Format – programmatic document creation, editing and export with vector PDF, rich text, tables and PDF import"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -89,6 +89,8 @@ addopts = "-v --tb=short"
|
|
|
89
89
|
source = ["edof"]
|
|
90
90
|
|
|
91
91
|
[project.scripts]
|
|
92
|
+
edof-cli = "edof._apps.cli:main"
|
|
93
|
+
|
|
94
|
+
[project.gui-scripts]
|
|
92
95
|
edof-editor = "edof._apps.editor:main"
|
|
93
96
|
edof-viewer = "edof._apps.viewer:main"
|
|
94
|
-
edof-cli = "edof._apps.cli:main"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|