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.
Files changed (89) hide show
  1. {edof-4.2.2 → edof-4.2.4}/PKG-INFO +1 -1
  2. edof-4.2.4/edof/_apps/_assoc_dialog.py +92 -0
  3. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/__init__.py +18 -0
  4. {edof-4.2.2 → edof-4.2.4}/edof/_apps/cli.py +4 -3
  5. {edof-4.2.2 → edof-4.2.4}/edof/_apps/editor.py +17 -33
  6. {edof-4.2.2 → edof-4.2.4}/edof/_apps/file_assoc.py +133 -83
  7. {edof-4.2.2 → edof-4.2.4}/edof/_apps/viewer.py +19 -35
  8. {edof-4.2.2 → edof-4.2.4}/edof/version.py +1 -1
  9. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/PKG-INFO +1 -1
  10. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/SOURCES.txt +1 -0
  11. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/entry_points.txt +2 -0
  12. {edof-4.2.2 → edof-4.2.4}/pyproject.toml +4 -2
  13. {edof-4.2.2 → edof-4.2.4}/LICENSE +0 -0
  14. {edof-4.2.2 → edof-4.2.4}/README.md +0 -0
  15. {edof-4.2.2 → edof-4.2.4}/edof/__init__.py +0 -0
  16. {edof-4.2.2 → edof-4.2.4}/edof/_apps/__init__.py +0 -0
  17. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/__init__.py +0 -0
  18. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document-512.png +0 -0
  19. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.icns +0 -0
  20. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.ico +0 -0
  21. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-document.png +0 -0
  22. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor-512.png +0 -0
  23. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.icns +0 -0
  24. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.ico +0 -0
  25. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-editor.png +0 -0
  26. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
  27. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.icns +0 -0
  28. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.ico +0 -0
  29. {edof-4.2.2 → edof-4.2.4}/edof/_apps/assets/icons/edof-viewer.png +0 -0
  30. {edof-4.2.2 → edof-4.2.4}/edof/_apps/edof_text_editor.py +0 -0
  31. {edof-4.2.2 → edof-4.2.4}/edof/_apps/shortcuts.py +0 -0
  32. {edof-4.2.2 → edof-4.2.4}/edof/api/__init__.py +0 -0
  33. {edof-4.2.2 → edof-4.2.4}/edof/api/commands.py +0 -0
  34. {edof-4.2.2 → edof-4.2.4}/edof/crypto/__init__.py +0 -0
  35. {edof-4.2.2 → edof-4.2.4}/edof/crypto/document_protection.py +0 -0
  36. {edof-4.2.2 → edof-4.2.4}/edof/crypto/encryption.py +0 -0
  37. {edof-4.2.2 → edof-4.2.4}/edof/crypto/permissions.py +0 -0
  38. {edof-4.2.2 → edof-4.2.4}/edof/editor_lang/en.json +0 -0
  39. {edof-4.2.2 → edof-4.2.4}/edof/engine/__init__.py +0 -0
  40. {edof-4.2.2 → edof-4.2.4}/edof/engine/color.py +0 -0
  41. {edof-4.2.2 → edof-4.2.4}/edof/engine/debug_log.py +0 -0
  42. {edof-4.2.2 → edof-4.2.4}/edof/engine/document_flow.py +0 -0
  43. {edof-4.2.2 → edof-4.2.4}/edof/engine/document_paginate.py +0 -0
  44. {edof-4.2.2 → edof-4.2.4}/edof/engine/renderer.py +0 -0
  45. {edof-4.2.2 → edof-4.2.4}/edof/engine/text_engine.py +0 -0
  46. {edof-4.2.2 → edof-4.2.4}/edof/engine/text_layout.py +0 -0
  47. {edof-4.2.2 → edof-4.2.4}/edof/engine/textbox_flow.py +0 -0
  48. {edof-4.2.2 → edof-4.2.4}/edof/engine/transform.py +0 -0
  49. {edof-4.2.2 → edof-4.2.4}/edof/exceptions.py +0 -0
  50. {edof-4.2.2 → edof-4.2.4}/edof/export/__init__.py +0 -0
  51. {edof-4.2.2 → edof-4.2.4}/edof/export/bitmap.py +0 -0
  52. {edof-4.2.2 → edof-4.2.4}/edof/export/legacy_v3.py +0 -0
  53. {edof-4.2.2 → edof-4.2.4}/edof/export/pdf.py +0 -0
  54. {edof-4.2.2 → edof-4.2.4}/edof/export/pdf_writer.py +0 -0
  55. {edof-4.2.2 → edof-4.2.4}/edof/export/printer.py +0 -0
  56. {edof-4.2.2 → edof-4.2.4}/edof/export/svg.py +0 -0
  57. {edof-4.2.2 → edof-4.2.4}/edof/format/__init__.py +0 -0
  58. {edof-4.2.2 → edof-4.2.4}/edof/format/document.py +0 -0
  59. {edof-4.2.2 → edof-4.2.4}/edof/format/document_body.py +0 -0
  60. {edof-4.2.2 → edof-4.2.4}/edof/format/document_boxes.py +0 -0
  61. {edof-4.2.2 → edof-4.2.4}/edof/format/objects.py +0 -0
  62. {edof-4.2.2 → edof-4.2.4}/edof/format/serializer.py +0 -0
  63. {edof-4.2.2 → edof-4.2.4}/edof/format/styles.py +0 -0
  64. {edof-4.2.2 → edof-4.2.4}/edof/format/svg_io.py +0 -0
  65. {edof-4.2.2 → edof-4.2.4}/edof/format/variables.py +0 -0
  66. {edof-4.2.2 → edof-4.2.4}/edof/gui/__init__.py +0 -0
  67. {edof-4.2.2 → edof-4.2.4}/edof/gui/pyqt6_widget.py +0 -0
  68. {edof-4.2.2 → edof-4.2.4}/edof/gui/tkinter_canvas.py +0 -0
  69. {edof-4.2.2 → edof-4.2.4}/edof/interop/__init__.py +0 -0
  70. {edof-4.2.2 → edof-4.2.4}/edof/interop/docx_io.py +0 -0
  71. {edof-4.2.2 → edof-4.2.4}/edof/py.typed +0 -0
  72. {edof-4.2.2 → edof-4.2.4}/edof/units.py +0 -0
  73. {edof-4.2.2 → edof-4.2.4}/edof/utils/__init__.py +0 -0
  74. {edof-4.2.2 → edof-4.2.4}/edof/utils/compat.py +0 -0
  75. {edof-4.2.2 → edof-4.2.4}/edof/utils/legacy_v2.py +0 -0
  76. {edof-4.2.2 → edof-4.2.4}/edof/utils/pdf_import.py +0 -0
  77. {edof-4.2.2 → edof-4.2.4}/edof/utils/qr.py +0 -0
  78. {edof-4.2.2 → edof-4.2.4}/edof/utils/rtf.py +0 -0
  79. {edof-4.2.2 → edof-4.2.4}/edof/utils/safe_eval.py +0 -0
  80. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/dependency_links.txt +0 -0
  81. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/requires.txt +0 -0
  82. {edof-4.2.2 → edof-4.2.4}/edof.egg-info/top_level.txt +0 -0
  83. {edof-4.2.2 → edof-4.2.4}/setup.cfg +0 -0
  84. {edof-4.2.2 → edof-4.2.4}/tests/test_v310.py +0 -0
  85. {edof-4.2.2 → edof-4.2.4}/tests/test_v400.py +0 -0
  86. {edof-4.2.2 → edof-4.2.4}/tests/test_v401.py +0 -0
  87. {edof-4.2.2 → edof-4.2.4}/tests/test_v402.py +0 -0
  88. {edof-4.2.2 → edof-4.2.4}/tests/test_v403.py +0 -0
  89. {edof-4.2.2 → edof-4.2.4}/tests/test_v410.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edof
3
- Version: 4.2.2
3
+ Version: 4.2.4
4
4
  Summary: Easy Document Format – programmatic document creation, editing and export with vector PDF, rich text, tables and PDF import
5
5
  License: MIT License
6
6
 
@@ -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: .edof files now associated with edof-viewer.")
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 / remove)."""
10308
+ """Manage the .edof file association (choose app in-app, register/remove)."""
10309
10309
  try:
10310
- from edof._apps.file_assoc import (
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
- status = current_association_status()
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 _find_viewer_executable():
21
- """Locate the edof-viewer command in PATH."""
22
- exe = shutil.which("edof-viewer")
23
- if exe:
24
- return exe
25
- # Fallback: try Python module invocation
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
- return f'"{sys.executable}" -m edof._apps.viewer'
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 edof-viewer executable. "
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 command in PATH (fallback to module invocation)."""
36
- exe = shutil.which("edof-editor")
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
- # Our document type. With the v4.2.2 model the type has no
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 and r.stdout.strip():
84
- return f"associated with: {r.stdout.strip()}"
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 with edof-viewer for the current user."""
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.2: the association does NOT force a default opener. `.edof` is mapped
125
- to a document *type* progid (icon + friendly name, no open command), and the
126
- Viewer and Editor are registered as the two "Open with" choices. The first
127
- time the user double-clicks a `.edof` file Windows asks which app to use, so
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
- progid_doc = "edof.Document" # file type: icon + name, no opener
135
- progid_view = "edof.Document.Viewer" # Open-with choice: Viewer
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 document TYPE progid (gives icon + name) and the open-with
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, progid_doc)
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. Document type: friendly name + icon, deliberately NO shell\open\command.
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
- if view_icon and os.path.isfile(view_icon):
188
- _set_default(f"{progid_view}\\DefaultIcon", f'"{view_icon}",0')
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
- # 4. Editor open-with choice.
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
- if edit_icon and os.path.isfile(edit_icon):
197
- _set_default(f"{progid_edit}\\DefaultIcon", f'"{edit_icon}",0')
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 editor:
211
- return True, (".edof registered. Files now show the EDOF icon, and the "
212
- "first time you open one Windows lets you choose the "
213
- "Viewer or the Editor (no default is forced).")
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=EDOF Viewer
271
- Comment=View EDOF documents
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;Viewer;
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
- viewer = _find_viewer_executable()
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 file
302
- desktop_path = apps_dir / "edof-viewer.desktop"
303
- desktop_path.write_text(
304
- DESKTOP_ENTRY.format(exec_cmd=viewer),
305
- encoding="utf-8",
306
- )
307
- desktop_path.chmod(0o755)
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 database
356
+ # 3. Update databases + set the chosen default handler.
310
357
  for cmd in (["update-mime-database", str(home / ".local" / "share" / "mime")],
311
- ["update-desktop-database", str(apps_dir)],
312
- ["xdg-mime", "default", "edof-viewer.desktop", "application/x-edof"]):
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
- return True, ".edof associated with edof-viewer (Linux desktop entry installed)."
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
- ok, info = (unassociate_edof_files() if is_assoc
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, "Could not " + verb,
572
- f"Failed to {verb} file types:\n\n{e}\n\n"
573
- f"On Windows this is per-user and should not need admin rights.")
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
@@ -9,7 +9,7 @@ typography values in pt and are migrated on load.
9
9
  """
10
10
 
11
11
  from __future__ import annotations
12
- __version__ = "4.2.2"
12
+ __version__ = "4.2.4"
13
13
  FORMAT_MAJOR = 4
14
14
  FORMAT_MINOR = 2
15
15
  FORMAT_PATCH = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edof
3
- Version: 4.2.2
3
+ Version: 4.2.4
4
4
  Summary: Easy Document Format – programmatic document creation, editing and export with vector PDF, rich text, tables and PDF import
5
5
  License: MIT License
6
6
 
@@ -13,6 +13,7 @@ edof.egg-info/entry_points.txt
13
13
  edof.egg-info/requires.txt
14
14
  edof.egg-info/top_level.txt
15
15
  edof/_apps/__init__.py
16
+ edof/_apps/_assoc_dialog.py
16
17
  edof/_apps/cli.py
17
18
  edof/_apps/editor.py
18
19
  edof/_apps/edof_text_editor.py
@@ -1,4 +1,6 @@
1
1
  [console_scripts]
2
2
  edof-cli = edof._apps.cli:main
3
+
4
+ [gui_scripts]
3
5
  edof-editor = edof._apps.editor:main
4
6
  edof-viewer = edof._apps.viewer:main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "edof"
7
- version = "4.2.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