edof 4.2.1__tar.gz → 4.2.2__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.1 → edof-4.2.2}/PKG-INFO +8 -8
- {edof-4.2.1 → edof-4.2.2}/README.md +7 -7
- {edof-4.2.1 → edof-4.2.2}/edof/__init__.py +2 -1
- edof-4.2.2/edof/_apps/assets/__init__.py +34 -0
- edof-4.2.2/edof/_apps/assets/icons/__init__.py +1 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-document-512.png +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-document.icns +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-document.ico +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-document.png +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-editor-512.png +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-editor.icns +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-editor.ico +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-editor.png +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-viewer.icns +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-viewer.ico +0 -0
- edof-4.2.2/edof/_apps/assets/icons/edof-viewer.png +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/editor.py +26 -8
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/edof_text_editor.py +22 -0
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/file_assoc.py +142 -39
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/viewer.py +66 -17
- {edof-4.2.1 → edof-4.2.2}/edof/engine/document_paginate.py +17 -2
- {edof-4.2.1 → edof-4.2.2}/edof/format/styles.py +3 -1
- {edof-4.2.1 → edof-4.2.2}/edof/version.py +1 -1
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/PKG-INFO +8 -8
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/SOURCES.txt +14 -0
- {edof-4.2.1 → edof-4.2.2}/pyproject.toml +2 -1
- {edof-4.2.1 → edof-4.2.2}/LICENSE +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/cli.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/_apps/shortcuts.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/api/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/api/commands.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/crypto/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/crypto/document_protection.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/crypto/encryption.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/crypto/permissions.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/editor_lang/en.json +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/color.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/debug_log.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/document_flow.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/renderer.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/text_engine.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/text_layout.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/textbox_flow.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/engine/transform.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/exceptions.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/bitmap.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/legacy_v3.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/pdf.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/pdf_writer.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/printer.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/export/svg.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/document.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/document_body.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/document_boxes.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/objects.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/serializer.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/svg_io.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/format/variables.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/gui/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/gui/pyqt6_widget.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/gui/tkinter_canvas.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/interop/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/interop/docx_io.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/py.typed +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/units.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/compat.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/legacy_v2.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/pdf_import.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/qr.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/rtf.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof/utils/safe_eval.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/dependency_links.txt +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/entry_points.txt +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/requires.txt +0 -0
- {edof-4.2.1 → edof-4.2.2}/edof.egg-info/top_level.txt +0 -0
- {edof-4.2.1 → edof-4.2.2}/setup.cfg +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v310.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v400.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v401.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v402.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v403.py +0 -0
- {edof-4.2.1 → edof-4.2.2}/tests/test_v410.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edof
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
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
|
|
|
@@ -185,7 +185,7 @@ A `Document` contains pages; each `Page` contains objects. All measurements are
|
|
|
185
185
|
| `Table` | Formatted table with per-cell styling | `cells`, `col_widths`, `row_heights`, `table_border` |
|
|
186
186
|
| `Group` | Container with optional clip | `children` |
|
|
187
187
|
|
|
188
|
-
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.
|
|
188
|
+
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.2.0+).
|
|
189
189
|
|
|
190
190
|
#### Transform fields
|
|
191
191
|
|
|
@@ -207,7 +207,7 @@ All color fields accept tuples in 0–255 range. Both RGB (3-tuple) and RGBA (4-
|
|
|
207
207
|
obj.style.color = (74, 144, 226) # RGB → alpha 255 implied
|
|
208
208
|
obj.fill.color = (245, 245, 248, 255) # RGBA — explicit
|
|
209
209
|
obj.stroke.color = (0, 0, 0, 200) # RGBA — translucent black
|
|
210
|
-
shape.fill.color = "#4a90e2" # v4.
|
|
210
|
+
shape.fill.color = "#4a90e2" # v4.2.0+: hex string accepted
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
When read back, colors come as tuples (`obj.style.color` returns `(74, 144, 226)`, `obj.fill.color` returns `(245, 245, 248, 255)`).
|
|
@@ -469,7 +469,7 @@ heading.lock_text = True # text never editable until lock_text is cleared
|
|
|
469
469
|
### Headings with auto-computed height (no more silent text loss)
|
|
470
470
|
|
|
471
471
|
```python
|
|
472
|
-
# v4.
|
|
472
|
+
# v4.2.0+: add_textbox_auto computes height from content and accepts style kwargs
|
|
473
473
|
heading = page.add_textbox_auto(
|
|
474
474
|
20, 20, 170, "MY HEADING",
|
|
475
475
|
font_size=24, bold=True, alignment="center",
|
|
@@ -481,7 +481,7 @@ next_y = heading.transform.y + heading.transform.height + 5 # 5 mm gap
|
|
|
481
481
|
### Tables with explicit sizing
|
|
482
482
|
|
|
483
483
|
```python
|
|
484
|
-
# v4.
|
|
484
|
+
# v4.2.0+: make_table accepts position + size and computes total height
|
|
485
485
|
tbl = edof.make_table(
|
|
486
486
|
rows=[
|
|
487
487
|
["Product", "Qty", "Price"],
|
|
@@ -509,7 +509,7 @@ tb.style.color = edof.as_color("#4a90e2") # hex via helper
|
|
|
509
509
|
tb.style.color = edof.as_color("#4a90e2cc") # 8-digit hex with alpha
|
|
510
510
|
```
|
|
511
511
|
|
|
512
|
-
### Per-side padding (v4.
|
|
512
|
+
### Per-side padding (v4.2.0+)
|
|
513
513
|
|
|
514
514
|
```python
|
|
515
515
|
tb.style.padding = 2.0 # uniform 2 mm on all sides
|
|
@@ -556,7 +556,7 @@ A PyQt6 desktop editor (`edof-editor`) ships with the library. It is a working e
|
|
|
556
556
|
|
|
557
557
|
## Viewer
|
|
558
558
|
|
|
559
|
-
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.
|
|
559
|
+
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.2.0+). It is designed for the case where someone receives a `.edof` file and just wants to view, print, or convert it — without launching the full editor.
|
|
560
560
|
|
|
561
561
|
```bash
|
|
562
562
|
pip install 'edof[viewer]'
|
|
@@ -698,7 +698,7 @@ A small `.bat` makes switching painless:
|
|
|
698
698
|
@echo off
|
|
699
699
|
call D:\apps\Edof_V401\edof-python\.venv\Scripts\activate.bat
|
|
700
700
|
cd /d D:\apps\Edof_V401\edof-python
|
|
701
|
-
cmd /k prompt [edof v4.
|
|
701
|
+
cmd /k prompt [edof v4.2.2] $P$G
|
|
702
702
|
```
|
|
703
703
|
|
|
704
704
|
Each version's venv is independent. Removing a version is `rmdir /s /q <folder>`; nothing else needs cleanup.
|
|
@@ -103,7 +103,7 @@ A `Document` contains pages; each `Page` contains objects. All measurements are
|
|
|
103
103
|
| `Table` | Formatted table with per-cell styling | `cells`, `col_widths`, `row_heights`, `table_border` |
|
|
104
104
|
| `Group` | Container with optional clip | `children` |
|
|
105
105
|
|
|
106
|
-
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.
|
|
106
|
+
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.2.0+).
|
|
107
107
|
|
|
108
108
|
#### Transform fields
|
|
109
109
|
|
|
@@ -125,7 +125,7 @@ All color fields accept tuples in 0–255 range. Both RGB (3-tuple) and RGBA (4-
|
|
|
125
125
|
obj.style.color = (74, 144, 226) # RGB → alpha 255 implied
|
|
126
126
|
obj.fill.color = (245, 245, 248, 255) # RGBA — explicit
|
|
127
127
|
obj.stroke.color = (0, 0, 0, 200) # RGBA — translucent black
|
|
128
|
-
shape.fill.color = "#4a90e2" # v4.
|
|
128
|
+
shape.fill.color = "#4a90e2" # v4.2.0+: hex string accepted
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
When read back, colors come as tuples (`obj.style.color` returns `(74, 144, 226)`, `obj.fill.color` returns `(245, 245, 248, 255)`).
|
|
@@ -387,7 +387,7 @@ heading.lock_text = True # text never editable until lock_text is cleared
|
|
|
387
387
|
### Headings with auto-computed height (no more silent text loss)
|
|
388
388
|
|
|
389
389
|
```python
|
|
390
|
-
# v4.
|
|
390
|
+
# v4.2.0+: add_textbox_auto computes height from content and accepts style kwargs
|
|
391
391
|
heading = page.add_textbox_auto(
|
|
392
392
|
20, 20, 170, "MY HEADING",
|
|
393
393
|
font_size=24, bold=True, alignment="center",
|
|
@@ -399,7 +399,7 @@ next_y = heading.transform.y + heading.transform.height + 5 # 5 mm gap
|
|
|
399
399
|
### Tables with explicit sizing
|
|
400
400
|
|
|
401
401
|
```python
|
|
402
|
-
# v4.
|
|
402
|
+
# v4.2.0+: make_table accepts position + size and computes total height
|
|
403
403
|
tbl = edof.make_table(
|
|
404
404
|
rows=[
|
|
405
405
|
["Product", "Qty", "Price"],
|
|
@@ -427,7 +427,7 @@ tb.style.color = edof.as_color("#4a90e2") # hex via helper
|
|
|
427
427
|
tb.style.color = edof.as_color("#4a90e2cc") # 8-digit hex with alpha
|
|
428
428
|
```
|
|
429
429
|
|
|
430
|
-
### Per-side padding (v4.
|
|
430
|
+
### Per-side padding (v4.2.0+)
|
|
431
431
|
|
|
432
432
|
```python
|
|
433
433
|
tb.style.padding = 2.0 # uniform 2 mm on all sides
|
|
@@ -474,7 +474,7 @@ A PyQt6 desktop editor (`edof-editor`) ships with the library. It is a working e
|
|
|
474
474
|
|
|
475
475
|
## Viewer
|
|
476
476
|
|
|
477
|
-
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.
|
|
477
|
+
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.2.0+). It is designed for the case where someone receives a `.edof` file and just wants to view, print, or convert it — without launching the full editor.
|
|
478
478
|
|
|
479
479
|
```bash
|
|
480
480
|
pip install 'edof[viewer]'
|
|
@@ -616,7 +616,7 @@ A small `.bat` makes switching painless:
|
|
|
616
616
|
@echo off
|
|
617
617
|
call D:\apps\Edof_V401\edof-python\.venv\Scripts\activate.bat
|
|
618
618
|
cd /d D:\apps\Edof_V401\edof-python
|
|
619
|
-
cmd /k prompt [edof v4.
|
|
619
|
+
cmd /k prompt [edof v4.2.2] $P$G
|
|
620
620
|
```
|
|
621
621
|
|
|
622
622
|
Each version's venv is independent. Removing a version is `rmdir /s /q <folder>`; nothing else needs cleanup.
|
|
@@ -85,7 +85,8 @@ def new(width: float = 210.0, height: float = 297.0, **kwargs) -> Document:
|
|
|
85
85
|
Example:
|
|
86
86
|
>>> import edof
|
|
87
87
|
>>> doc = edof.new(210, 297, title="Hello", dpi=300)
|
|
88
|
-
>>> page = doc.
|
|
88
|
+
>>> page = doc.add_page()
|
|
89
|
+
>>> page.add_textbox(15, 15, 180, 12, "Hello world!")
|
|
89
90
|
>>> doc.save("hello.edof")
|
|
90
91
|
"""
|
|
91
92
|
return Document(width=width, height=height, **kwargs)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Bundled application assets (icons).
|
|
2
|
+
|
|
3
|
+
`icon_path(name)` returns the absolute on-disk path to a packaged icon, working
|
|
4
|
+
both from a source checkout and from an installed wheel. Kept Qt-free so it can
|
|
5
|
+
also be used by the file-association registration code.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def icon_path(name: str):
|
|
13
|
+
"""Return the absolute path to a bundled icon file, or None if missing.
|
|
14
|
+
|
|
15
|
+
`name` is the file name, e.g. "edof-editor.ico" or "edof-document.png".
|
|
16
|
+
"""
|
|
17
|
+
# Preferred: importlib.resources (works for installed packages).
|
|
18
|
+
try:
|
|
19
|
+
from importlib.resources import files
|
|
20
|
+
p = files("edof._apps.assets.icons").joinpath(name)
|
|
21
|
+
sp = str(p)
|
|
22
|
+
if os.path.isfile(sp):
|
|
23
|
+
return sp
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
# Fallback: relative to this file (source checkout).
|
|
27
|
+
here = os.path.join(os.path.dirname(__file__), "icons", name)
|
|
28
|
+
return here if os.path.isfile(here) else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def app_icon_name(kind: str, ext: str = "ico") -> str:
|
|
32
|
+
"""Map a logical kind to an icon file name. kind in {editor,viewer,document}."""
|
|
33
|
+
kind = kind if kind in ("editor", "viewer", "document") else "document"
|
|
34
|
+
return f"edof-{kind}.{ext}"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Packaged icon files (.ico/.png/.icns)."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,7 +23,7 @@ try:
|
|
|
23
23
|
from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer, QSize, pyqtSignal, QObject, QSettings
|
|
24
24
|
from PyQt6.QtGui import (
|
|
25
25
|
QAction, QPainter, QPen, QBrush, QColor, QPixmap, QImage,
|
|
26
|
-
QPolygonF, QFont, QTransform, QCursor, QKeySequence, QPalette,
|
|
26
|
+
QPolygonF, QFont, QTransform, QCursor, QKeySequence, QPalette, QIcon,
|
|
27
27
|
)
|
|
28
28
|
except ImportError:
|
|
29
29
|
print("PyQt6 required: pip install PyQt6"); sys.exit(1)
|
|
@@ -233,7 +233,7 @@ QCheckBox::indicator:checked{{background:{ACC};border-color:{ACC}}}
|
|
|
233
233
|
QRadioButton::indicator{{border-radius:8px}} QRadioButton::indicator:checked{{background:{ACC};border-color:{ACC}}}
|
|
234
234
|
QComboBox::drop-down{{border:none;width:18px}}
|
|
235
235
|
QComboBox QAbstractItemView{{background:{PBG2};color:{FG};selection-background-color:{ACC};font:10pt 'Segoe UI'}}
|
|
236
|
-
QListWidget{{background:#1a1a2e;color:{FG};border:none;
|
|
236
|
+
QListWidget{{background:#1a1a2e;color:{FG};border:none;font:10pt 'Segoe UI'}}
|
|
237
237
|
QListWidget::item{{padding:4px 6px}}
|
|
238
238
|
QListWidget::item:selected{{background:{ACC}}}
|
|
239
239
|
QTableWidget{{background:#1a1a2e;color:{FG};gridline-color:#444;font:10pt 'Segoe UI'}}
|
|
@@ -1081,6 +1081,14 @@ class EdofCanvas(QGraphicsView):
|
|
|
1081
1081
|
ed._host_undo = mw._undo
|
|
1082
1082
|
ed._host_redo = mw._redo
|
|
1083
1083
|
ed._on_body_edit = mw._on_body_touched
|
|
1084
|
+
# v4.2.1: route Ctrl+S / Ctrl+Shift+S to the window's save so
|
|
1085
|
+
# they work while the caret is inside the body text editor
|
|
1086
|
+
# (previously Ctrl+S did nothing and Ctrl+Shift+S toggled
|
|
1087
|
+
# strikethrough instead of Save As).
|
|
1088
|
+
if hasattr(mw, '_save'):
|
|
1089
|
+
ed._host_save = mw._save
|
|
1090
|
+
if hasattr(mw, '_save_as'):
|
|
1091
|
+
ed._host_save_as = mw._save_as
|
|
1084
1092
|
# v4.1.16.4: signals trigger UI cleanup AFTER editor has written
|
|
1085
1093
|
# data back. Editor.py drives commit/cancel directly; signals are
|
|
1086
1094
|
# for after-the-fact cleanup only — no recursion possible.
|
|
@@ -8200,6 +8208,14 @@ class EdofEditor(QMainWindow):
|
|
|
8200
8208
|
_patch_button_font(btn)
|
|
8201
8209
|
except Exception: pass
|
|
8202
8210
|
self.setWindowTitle(f"{t('app_title')} {edof.__version__}")
|
|
8211
|
+
# v4.2.2: window / taskbar icon
|
|
8212
|
+
try:
|
|
8213
|
+
from edof._apps.assets import icon_path
|
|
8214
|
+
_ip = icon_path("edof-editor.ico") or icon_path("edof-editor.png")
|
|
8215
|
+
if _ip:
|
|
8216
|
+
self.setWindowIcon(QIcon(_ip))
|
|
8217
|
+
except Exception:
|
|
8218
|
+
pass
|
|
8203
8219
|
# v4.0.2/4.1.9.1: restore window geometry, default to maximized on first run
|
|
8204
8220
|
geom = self._settings.value("geometry")
|
|
8205
8221
|
if geom is not None:
|
|
@@ -8680,7 +8696,7 @@ class EdofEditor(QMainWindow):
|
|
|
8680
8696
|
hm.addSeparator()
|
|
8681
8697
|
# v4.1.0: donate
|
|
8682
8698
|
# v4.1.1: file association management
|
|
8683
|
-
assoc_act = QAction("
|
|
8699
|
+
assoc_act = QAction("File association (.edof)…", self)
|
|
8684
8700
|
assoc_act.triggered.connect(self._open_file_assoc_dialog)
|
|
8685
8701
|
hm.addAction(assoc_act)
|
|
8686
8702
|
hm.addSeparator()
|
|
@@ -10289,7 +10305,7 @@ class EdofEditor(QMainWindow):
|
|
|
10289
10305
|
dlg.exec()
|
|
10290
10306
|
|
|
10291
10307
|
def _open_file_assoc_dialog(self):
|
|
10292
|
-
"""
|
|
10308
|
+
"""Manage the .edof file association (register / remove)."""
|
|
10293
10309
|
try:
|
|
10294
10310
|
from edof._apps.file_assoc import (
|
|
10295
10311
|
associate_edof_files, unassociate_edof_files,
|
|
@@ -10303,10 +10319,12 @@ class EdofEditor(QMainWindow):
|
|
|
10303
10319
|
is_assoc = "associated" in status.lower() and "not " not in status.lower()
|
|
10304
10320
|
msg = (f"<h3>File association</h3>"
|
|
10305
10321
|
f"<p>Current status: <b>{status}</b></p>"
|
|
10306
|
-
f"<p>When .edof files are
|
|
10307
|
-
f"
|
|
10308
|
-
f"<b>
|
|
10309
|
-
f"
|
|
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>")
|
|
10310
10328
|
bb_text = "Unassociate" if is_assoc else "Associate"
|
|
10311
10329
|
ret = QMessageBox.question(self, "File association", msg,
|
|
10312
10330
|
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
|
|
@@ -532,6 +532,8 @@ class EdofTextEditor(QWidget):
|
|
|
532
532
|
# local stacks. Set by the canvas in _start_inline for body editors.
|
|
533
533
|
self._host_undo = None # callable() -> document-level undo
|
|
534
534
|
self._host_redo = None # callable() -> document-level redo
|
|
535
|
+
self._host_save = None # callable() -> document-level Save (Ctrl+S)
|
|
536
|
+
self._host_save_as = None # callable() -> document-level Save As (Ctrl+Shift+S)
|
|
535
537
|
self._on_body_edit = None # callable() -> checkpoint before an edit burst
|
|
536
538
|
# Idle-overflow auto-commit timer (doc body only)
|
|
537
539
|
self._idle_overflow_timer = QTimer(self)
|
|
@@ -1775,6 +1777,16 @@ class EdofTextEditor(QWidget):
|
|
|
1775
1777
|
if k in claim and not ctrl:
|
|
1776
1778
|
ev.accept()
|
|
1777
1779
|
return True
|
|
1780
|
+
# v4.2.1: in the document body, claim Ctrl+S / Ctrl+Shift+S so
|
|
1781
|
+
# they reach our keyPressEvent (which forwards them to the
|
|
1782
|
+
# window's Save / Save As) instead of being swallowed or, for
|
|
1783
|
+
# Ctrl+Shift+S, triggering the editor's strikethrough action.
|
|
1784
|
+
if (getattr(self, '_is_doc_body', False)
|
|
1785
|
+
and ctrl and k == Qt.Key.Key_S):
|
|
1786
|
+
if (self._host_save is not None
|
|
1787
|
+
or self._host_save_as is not None):
|
|
1788
|
+
ev.accept()
|
|
1789
|
+
return True
|
|
1778
1790
|
# v4.1.23.38: also claim any Ctrl/Alt combo that maps to a
|
|
1779
1791
|
# configured editor action (undo/redo/copy/paste/format/…) so a
|
|
1780
1792
|
# window-level QAction (e.g. global Undo on Ctrl+Z) can't eat it
|
|
@@ -1861,6 +1873,16 @@ class EdofTextEditor(QWidget):
|
|
|
1861
1873
|
self._push_undo("type"); self._insert_text(' '); self._invalidate()
|
|
1862
1874
|
return
|
|
1863
1875
|
|
|
1876
|
+
# v4.2.1: document-level Save / Save As take precedence inside the body
|
|
1877
|
+
# editor, before the configurable dispatch (which maps Ctrl+Shift+S to
|
|
1878
|
+
# strikethrough). Forward to the host window so the standard shortcuts
|
|
1879
|
+
# work while typing.
|
|
1880
|
+
if ctrl and key == Qt.Key.Key_S and getattr(self, '_is_doc_body', False):
|
|
1881
|
+
if shift and self._host_save_as is not None:
|
|
1882
|
+
self._host_save_as(); ev.accept(); return
|
|
1883
|
+
if not shift and self._host_save is not None:
|
|
1884
|
+
self._host_save(); ev.accept(); return
|
|
1885
|
+
|
|
1864
1886
|
# v4.1.23.37: configurable shortcuts (formatting / clipboard /
|
|
1865
1887
|
# alignment). Resolved via the user-editable map; unmapped combos
|
|
1866
1888
|
# (e.g. ctrl+arrow word navigation) fall through to the handlers below.
|
|
@@ -31,6 +31,26 @@ def _find_viewer_executable():
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
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
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cmd_with_arg(exe: str) -> str:
|
|
48
|
+
"""Build a 'command' value: quote the exe (unless already quoted) + "%1"."""
|
|
49
|
+
if exe.startswith('"') or exe.startswith("'"):
|
|
50
|
+
return f'{exe} "%1"'
|
|
51
|
+
return f'"{exe}" "%1"'
|
|
52
|
+
|
|
53
|
+
|
|
34
54
|
def current_association_status() -> str:
|
|
35
55
|
"""Return human-readable description of current .edof file association."""
|
|
36
56
|
if sys.platform == "win32":
|
|
@@ -38,10 +58,19 @@ def current_association_status() -> str:
|
|
|
38
58
|
import winreg
|
|
39
59
|
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".edof") as k:
|
|
40
60
|
cls, _ = winreg.QueryValueEx(k, "")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
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
|
|
67
|
+
try:
|
|
68
|
+
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT,
|
|
69
|
+
f"{cls}\\shell\\open\\command") as k:
|
|
70
|
+
cmd, _ = winreg.QueryValueEx(k, "")
|
|
71
|
+
return f"associated with another app: {cmd}"
|
|
72
|
+
except OSError:
|
|
73
|
+
return f"associated with: {cls}"
|
|
45
74
|
except OSError:
|
|
46
75
|
return "not associated"
|
|
47
76
|
elif sys.platform.startswith("linux"):
|
|
@@ -90,73 +119,145 @@ def unassociate_edof_files():
|
|
|
90
119
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
91
120
|
|
|
92
121
|
def _associate_windows():
|
|
93
|
-
"""Register .edof in HKCU\\Software\\Classes (per-user, no admin needed).
|
|
122
|
+
"""Register .edof in HKCU\\Software\\Classes (per-user, no admin needed).
|
|
123
|
+
|
|
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.
|
|
130
|
+
"""
|
|
94
131
|
import winreg
|
|
95
132
|
|
|
96
|
-
viewer = _find_viewer_executable()
|
|
97
|
-
# Build command line. Use double-quotes around the executable path.
|
|
98
|
-
if viewer.startswith('"') or viewer.startswith("'"):
|
|
99
|
-
cmd = f'{viewer} "%1"'
|
|
100
|
-
else:
|
|
101
|
-
cmd = f'"{viewer}" "%1"'
|
|
102
|
-
|
|
103
|
-
progid = "edof.Document.1"
|
|
104
133
|
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
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
viewer = _find_viewer_executable()
|
|
139
|
+
cmd_view = _cmd_with_arg(viewer)
|
|
140
|
+
try:
|
|
141
|
+
editor = _find_editor_executable()
|
|
142
|
+
cmd_edit = _cmd_with_arg(editor)
|
|
143
|
+
except RuntimeError:
|
|
144
|
+
editor = None
|
|
145
|
+
cmd_edit = None
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
try:
|
|
148
|
+
from edof._apps.assets import icon_path
|
|
149
|
+
except Exception:
|
|
150
|
+
icon_path = lambda *_a, **_k: None # noqa: E731
|
|
151
|
+
doc_icon = icon_path("edof-document.ico")
|
|
152
|
+
view_icon = icon_path("edof-viewer.ico")
|
|
153
|
+
edit_icon = icon_path("edof-editor.ico")
|
|
154
|
+
|
|
155
|
+
def _set_default(path, value):
|
|
156
|
+
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{path}",
|
|
157
|
+
0, winreg.KEY_WRITE) as k:
|
|
158
|
+
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, value)
|
|
159
|
+
|
|
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.
|
|
163
|
+
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\.edof",
|
|
164
|
+
0, winreg.KEY_WRITE) as k:
|
|
165
|
+
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, progid_doc)
|
|
166
|
+
winreg.SetValueEx(k, "Content Type", 0, winreg.REG_SZ, "application/x-edof")
|
|
114
167
|
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER,
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
f"{classes}\\.edof\\OpenWithProgids",
|
|
169
|
+
0, winreg.KEY_WRITE) as k:
|
|
170
|
+
winreg.SetValueEx(k, progid_view, 0, winreg.REG_NONE, b"")
|
|
171
|
+
if editor:
|
|
172
|
+
winreg.SetValueEx(k, progid_edit, 0, winreg.REG_NONE, b"")
|
|
173
|
+
|
|
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:
|
|
117
177
|
winreg.SetValueEx(k, "", 0, winreg.REG_SZ, "EDOF Document")
|
|
118
178
|
winreg.SetValueEx(k, "FriendlyTypeName", 0, winreg.REG_SZ,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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).
|
|
184
|
+
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{progid_view}",
|
|
185
|
+
0, winreg.KEY_WRITE) as k:
|
|
186
|
+
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')
|
|
189
|
+
_set_default(f"{progid_view}\\shell\\open\\command", cmd_view)
|
|
190
|
+
|
|
191
|
+
# 4. Editor open-with choice.
|
|
192
|
+
if cmd_edit:
|
|
193
|
+
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, f"{classes}\\{progid_edit}",
|
|
194
|
+
0, winreg.KEY_WRITE) as k:
|
|
195
|
+
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')
|
|
198
|
+
_set_default(f"{progid_edit}\\shell\\open\\command", cmd_edit)
|
|
199
|
+
|
|
200
|
+
# 5. Notify the shell that associations changed
|
|
135
201
|
try:
|
|
136
202
|
import ctypes
|
|
137
203
|
SHCNE_ASSOCCHANGED = 0x08000000
|
|
138
204
|
SHCNF_IDLIST = 0x0000
|
|
139
205
|
ctypes.windll.shell32.SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST,
|
|
140
|
-
|
|
206
|
+
None, None)
|
|
141
207
|
except Exception:
|
|
142
208
|
pass
|
|
143
209
|
|
|
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.)")
|
|
216
|
+
|
|
144
217
|
|
|
145
218
|
def _unassociate_windows():
|
|
146
219
|
import winreg
|
|
147
220
|
classes = r"Software\Classes"
|
|
148
221
|
for sub in [
|
|
222
|
+
r".edof\OpenWithProgids",
|
|
149
223
|
r".edof",
|
|
224
|
+
# new (v4.2.2) keys
|
|
225
|
+
r"edof.Document.Viewer\shell\open\command",
|
|
226
|
+
r"edof.Document.Viewer\shell\open",
|
|
227
|
+
r"edof.Document.Viewer\shell",
|
|
228
|
+
r"edof.Document.Viewer\DefaultIcon",
|
|
229
|
+
r"edof.Document.Viewer",
|
|
230
|
+
r"edof.Document.Editor\shell\open\command",
|
|
231
|
+
r"edof.Document.Editor\shell\open",
|
|
232
|
+
r"edof.Document.Editor\shell",
|
|
233
|
+
r"edof.Document.Editor\DefaultIcon",
|
|
234
|
+
r"edof.Document.Editor",
|
|
235
|
+
r"edof.Document\DefaultIcon",
|
|
236
|
+
r"edof.Document",
|
|
237
|
+
# old (<=4.2.1) keys, cleaned up for migration
|
|
238
|
+
r"edof.Document.1\shell\edit\command",
|
|
239
|
+
r"edof.Document.1\shell\edit",
|
|
150
240
|
r"edof.Document.1\shell\open\command",
|
|
151
241
|
r"edof.Document.1\shell\open",
|
|
152
242
|
r"edof.Document.1\shell",
|
|
153
243
|
r"edof.Document.1\DefaultIcon",
|
|
154
244
|
r"edof.Document.1",
|
|
245
|
+
r"edof.Document.Editor.1\shell\open\command",
|
|
246
|
+
r"edof.Document.Editor.1\shell\open",
|
|
247
|
+
r"edof.Document.Editor.1\shell",
|
|
248
|
+
r"edof.Document.Editor.1\DefaultIcon",
|
|
249
|
+
r"edof.Document.Editor.1",
|
|
155
250
|
]:
|
|
156
251
|
try:
|
|
157
252
|
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, f"{classes}\\{sub}")
|
|
158
253
|
except OSError:
|
|
159
254
|
pass
|
|
255
|
+
try:
|
|
256
|
+
import ctypes
|
|
257
|
+
ctypes.windll.shell32.SHChangeNotify(0x08000000, 0x0000, None, None)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
return True, ".edof association removed."
|
|
160
261
|
|
|
161
262
|
|
|
162
263
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -213,6 +314,7 @@ def _associate_linux():
|
|
|
213
314
|
subprocess.run(cmd, check=False, capture_output=True, timeout=10)
|
|
214
315
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
215
316
|
pass
|
|
317
|
+
return True, ".edof associated with edof-viewer (Linux desktop entry installed)."
|
|
216
318
|
|
|
217
319
|
|
|
218
320
|
def _unassociate_linux():
|
|
@@ -230,6 +332,7 @@ def _unassociate_linux():
|
|
|
230
332
|
)
|
|
231
333
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
232
334
|
pass
|
|
335
|
+
return True, ".edof association removed."
|
|
233
336
|
|
|
234
337
|
|
|
235
338
|
if __name__ == "__main__":
|
|
@@ -90,6 +90,14 @@ class EdofViewer(QMainWindow):
|
|
|
90
90
|
def __init__(self, filepath: Optional[str] = None):
|
|
91
91
|
super().__init__()
|
|
92
92
|
self.setWindowTitle(f"EDOF Viewer {edof.__version__}")
|
|
93
|
+
# v4.2.2: window / taskbar icon
|
|
94
|
+
try:
|
|
95
|
+
from edof._apps.assets import icon_path
|
|
96
|
+
_ip = icon_path("edof-viewer.ico") or icon_path("edof-viewer.png")
|
|
97
|
+
if _ip:
|
|
98
|
+
self.setWindowIcon(QIcon(_ip))
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
93
101
|
self.resize(1100, 800)
|
|
94
102
|
self.setStyleSheet(VIEWER_QSS)
|
|
95
103
|
|
|
@@ -143,6 +151,12 @@ class EdofViewer(QMainWindow):
|
|
|
143
151
|
self._act_open.triggered.connect(self._on_open)
|
|
144
152
|
fm.addAction(self._act_open)
|
|
145
153
|
|
|
154
|
+
# v4.2.1: open the current document in the editor
|
|
155
|
+
self._act_edit = QAction("Open in &Editor", self)
|
|
156
|
+
self._act_edit.setShortcut(QKeySequence("Ctrl+E"))
|
|
157
|
+
self._act_edit.triggered.connect(self._open_in_editor)
|
|
158
|
+
fm.addAction(self._act_edit)
|
|
159
|
+
|
|
146
160
|
self._act_export_pdf = QAction("Export as &PDF…", self)
|
|
147
161
|
self._act_export_pdf.triggered.connect(self._on_export_pdf)
|
|
148
162
|
fm.addAction(self._act_export_pdf)
|
|
@@ -214,7 +228,7 @@ class EdofViewer(QMainWindow):
|
|
|
214
228
|
act_about.triggered.connect(self._on_about)
|
|
215
229
|
hm.addAction(act_about)
|
|
216
230
|
|
|
217
|
-
act_associate = QAction("
|
|
231
|
+
act_associate = QAction("File association (.edof)…", self)
|
|
218
232
|
act_associate.triggered.connect(self._on_associate)
|
|
219
233
|
hm.addAction(act_associate)
|
|
220
234
|
|
|
@@ -520,28 +534,43 @@ class EdofViewer(QMainWindow):
|
|
|
520
534
|
)
|
|
521
535
|
|
|
522
536
|
def _on_associate(self):
|
|
523
|
-
from edof._apps.file_assoc import
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
"Associate .edof files with edof Viewer?\n\n"
|
|
527
|
-
"After this, double-clicking any .edof file in your file manager\n"
|
|
528
|
-
"will open it directly in this viewer.\n\n"
|
|
529
|
-
f"Current status: {status}"
|
|
537
|
+
from edof._apps.file_assoc import (
|
|
538
|
+
associate_edof_files, unassociate_edof_files,
|
|
539
|
+
current_association_status,
|
|
530
540
|
)
|
|
531
|
-
|
|
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,
|
|
532
557
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
533
558
|
if reply != QMessageBox.StandardButton.Yes:
|
|
534
559
|
return
|
|
535
560
|
try:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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)
|
|
541
570
|
except Exception as e:
|
|
542
|
-
QMessageBox.warning(self, "Could not
|
|
543
|
-
f"Failed to
|
|
544
|
-
f"
|
|
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.")
|
|
545
574
|
|
|
546
575
|
def _on_donate(self):
|
|
547
576
|
try:
|
|
@@ -550,6 +579,26 @@ class EdofViewer(QMainWindow):
|
|
|
550
579
|
except Exception:
|
|
551
580
|
pass
|
|
552
581
|
|
|
582
|
+
def _open_in_editor(self):
|
|
583
|
+
"""v4.2.1: launch edof-editor, opening the currently viewed file."""
|
|
584
|
+
import shutil, subprocess
|
|
585
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
586
|
+
exe = shutil.which("edof-editor")
|
|
587
|
+
if exe:
|
|
588
|
+
args = [exe]
|
|
589
|
+
else:
|
|
590
|
+
args = [sys.executable, "-m", "edof._apps.editor"]
|
|
591
|
+
if self._filepath:
|
|
592
|
+
args.append(self._filepath)
|
|
593
|
+
try:
|
|
594
|
+
subprocess.Popen(args, close_fds=True)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
QMessageBox.warning(
|
|
597
|
+
self, "Open in Editor",
|
|
598
|
+
"Could not launch the editor.\n"
|
|
599
|
+
"Make sure it is installed: pip install \"edof[all]\"\n\n"
|
|
600
|
+
f"({e})")
|
|
601
|
+
|
|
553
602
|
|
|
554
603
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
555
604
|
# Entry point
|
|
@@ -230,13 +230,28 @@ def _sync_body_from_textboxes(doc) -> None:
|
|
|
230
230
|
para_runs = _split_runs_at_newlines(all_runs)
|
|
231
231
|
old_paragraphs = doc.body.paragraphs or []
|
|
232
232
|
new_paragraphs: List[Paragraph] = []
|
|
233
|
+
|
|
234
|
+
def _runs_align(runs):
|
|
235
|
+
# v4.2.1: the editor stamps the chosen alignment onto the runs of a
|
|
236
|
+
# paragraph (set_alignment). Promote that onto Paragraph.alignment so
|
|
237
|
+
# it is the durable, canonical home: it survives serialization and is
|
|
238
|
+
# read by the renderer, the .docx exporter and style resolution. Without
|
|
239
|
+
# this the alignment lived only on the runs / the page-local map and was
|
|
240
|
+
# lost on save and on export.
|
|
241
|
+
for _r in runs:
|
|
242
|
+
a = getattr(_r, 'alignment', None)
|
|
243
|
+
if a:
|
|
244
|
+
return a
|
|
245
|
+
return None
|
|
246
|
+
|
|
233
247
|
for i, runs in enumerate(para_runs):
|
|
248
|
+
run_al = _runs_align(runs)
|
|
234
249
|
if i < len(old_paragraphs):
|
|
235
250
|
old = old_paragraphs[i]
|
|
236
251
|
new_p = Paragraph(
|
|
237
252
|
runs=runs,
|
|
238
253
|
style_id=old.style_id,
|
|
239
|
-
alignment=old.alignment,
|
|
254
|
+
alignment=(run_al if run_al is not None else old.alignment),
|
|
240
255
|
line_height=old.line_height,
|
|
241
256
|
space_before_mm=old.space_before_mm,
|
|
242
257
|
space_after_mm=old.space_after_mm,
|
|
@@ -251,7 +266,7 @@ def _sync_body_from_textboxes(doc) -> None:
|
|
|
251
266
|
widow_orphan_control=old.widow_orphan_control,
|
|
252
267
|
)
|
|
253
268
|
else:
|
|
254
|
-
new_p = Paragraph(runs=runs)
|
|
269
|
+
new_p = Paragraph(runs=runs, alignment=run_al)
|
|
255
270
|
new_paragraphs.append(new_p)
|
|
256
271
|
# v4.1.23.52: once pagination has run, the page bodies' hard-break flags are
|
|
257
272
|
# the authoritative source of truth for page_break_before. Re-apply it so a
|
|
@@ -330,13 +330,15 @@ class TextRun:
|
|
|
330
330
|
if self.background is not None: d["background"] = _rgba_to_hex(self.background)
|
|
331
331
|
if self.line_height is not None: d["line_height"] = self.line_height
|
|
332
332
|
if self.letter_spacing is not None: d["letter_spacing"] = self.letter_spacing
|
|
333
|
+
if self.alignment is not None: d["alignment"] = self.alignment
|
|
333
334
|
return d
|
|
334
335
|
|
|
335
336
|
@classmethod
|
|
336
337
|
def from_dict(cls, d: dict) -> "TextRun":
|
|
337
338
|
r = cls(text=d.get("text", ""))
|
|
338
339
|
for k in ("font_family", "font_size", "bold", "italic",
|
|
339
|
-
"underline", "strikethrough", "line_height", "letter_spacing"
|
|
340
|
+
"underline", "strikethrough", "line_height", "letter_spacing",
|
|
341
|
+
"alignment"):
|
|
340
342
|
if k in d: setattr(r, k, d[k])
|
|
341
343
|
for k in ("color", "background"):
|
|
342
344
|
if k in d:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edof
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.2
|
|
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
|
|
|
@@ -185,7 +185,7 @@ A `Document` contains pages; each `Page` contains objects. All measurements are
|
|
|
185
185
|
| `Table` | Formatted table with per-cell styling | `cells`, `col_widths`, `row_heights`, `table_border` |
|
|
186
186
|
| `Group` | Container with optional clip | `children` |
|
|
187
187
|
|
|
188
|
-
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.
|
|
188
|
+
Common fields on every object: `transform` (position/size/rotation/flip), `opacity`, `layer`, `visible`, `visible_if`, `blend_mode`, `lock_level`, `lock_text`, `lock_position`, `tags`, `shadow`, `effects` (v4.2.0+).
|
|
189
189
|
|
|
190
190
|
#### Transform fields
|
|
191
191
|
|
|
@@ -207,7 +207,7 @@ All color fields accept tuples in 0–255 range. Both RGB (3-tuple) and RGBA (4-
|
|
|
207
207
|
obj.style.color = (74, 144, 226) # RGB → alpha 255 implied
|
|
208
208
|
obj.fill.color = (245, 245, 248, 255) # RGBA — explicit
|
|
209
209
|
obj.stroke.color = (0, 0, 0, 200) # RGBA — translucent black
|
|
210
|
-
shape.fill.color = "#4a90e2" # v4.
|
|
210
|
+
shape.fill.color = "#4a90e2" # v4.2.0+: hex string accepted
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
When read back, colors come as tuples (`obj.style.color` returns `(74, 144, 226)`, `obj.fill.color` returns `(245, 245, 248, 255)`).
|
|
@@ -469,7 +469,7 @@ heading.lock_text = True # text never editable until lock_text is cleared
|
|
|
469
469
|
### Headings with auto-computed height (no more silent text loss)
|
|
470
470
|
|
|
471
471
|
```python
|
|
472
|
-
# v4.
|
|
472
|
+
# v4.2.0+: add_textbox_auto computes height from content and accepts style kwargs
|
|
473
473
|
heading = page.add_textbox_auto(
|
|
474
474
|
20, 20, 170, "MY HEADING",
|
|
475
475
|
font_size=24, bold=True, alignment="center",
|
|
@@ -481,7 +481,7 @@ next_y = heading.transform.y + heading.transform.height + 5 # 5 mm gap
|
|
|
481
481
|
### Tables with explicit sizing
|
|
482
482
|
|
|
483
483
|
```python
|
|
484
|
-
# v4.
|
|
484
|
+
# v4.2.0+: make_table accepts position + size and computes total height
|
|
485
485
|
tbl = edof.make_table(
|
|
486
486
|
rows=[
|
|
487
487
|
["Product", "Qty", "Price"],
|
|
@@ -509,7 +509,7 @@ tb.style.color = edof.as_color("#4a90e2") # hex via helper
|
|
|
509
509
|
tb.style.color = edof.as_color("#4a90e2cc") # 8-digit hex with alpha
|
|
510
510
|
```
|
|
511
511
|
|
|
512
|
-
### Per-side padding (v4.
|
|
512
|
+
### Per-side padding (v4.2.0+)
|
|
513
513
|
|
|
514
514
|
```python
|
|
515
515
|
tb.style.padding = 2.0 # uniform 2 mm on all sides
|
|
@@ -556,7 +556,7 @@ A PyQt6 desktop editor (`edof-editor`) ships with the library. It is a working e
|
|
|
556
556
|
|
|
557
557
|
## Viewer
|
|
558
558
|
|
|
559
|
-
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.
|
|
559
|
+
A lightweight read-only viewer (`edof-viewer`) ships alongside the editor (v4.2.0+). It is designed for the case where someone receives a `.edof` file and just wants to view, print, or convert it — without launching the full editor.
|
|
560
560
|
|
|
561
561
|
```bash
|
|
562
562
|
pip install 'edof[viewer]'
|
|
@@ -698,7 +698,7 @@ A small `.bat` makes switching painless:
|
|
|
698
698
|
@echo off
|
|
699
699
|
call D:\apps\Edof_V401\edof-python\.venv\Scripts\activate.bat
|
|
700
700
|
cd /d D:\apps\Edof_V401\edof-python
|
|
701
|
-
cmd /k prompt [edof v4.
|
|
701
|
+
cmd /k prompt [edof v4.2.2] $P$G
|
|
702
702
|
```
|
|
703
703
|
|
|
704
704
|
Each version's venv is independent. Removing a version is `rmdir /s /q <folder>`; nothing else needs cleanup.
|
|
@@ -19,6 +19,20 @@ edof/_apps/edof_text_editor.py
|
|
|
19
19
|
edof/_apps/file_assoc.py
|
|
20
20
|
edof/_apps/shortcuts.py
|
|
21
21
|
edof/_apps/viewer.py
|
|
22
|
+
edof/_apps/assets/__init__.py
|
|
23
|
+
edof/_apps/assets/icons/__init__.py
|
|
24
|
+
edof/_apps/assets/icons/edof-document-512.png
|
|
25
|
+
edof/_apps/assets/icons/edof-document.icns
|
|
26
|
+
edof/_apps/assets/icons/edof-document.ico
|
|
27
|
+
edof/_apps/assets/icons/edof-document.png
|
|
28
|
+
edof/_apps/assets/icons/edof-editor-512.png
|
|
29
|
+
edof/_apps/assets/icons/edof-editor.icns
|
|
30
|
+
edof/_apps/assets/icons/edof-editor.ico
|
|
31
|
+
edof/_apps/assets/icons/edof-editor.png
|
|
32
|
+
edof/_apps/assets/icons/edof-viewer-512.png
|
|
33
|
+
edof/_apps/assets/icons/edof-viewer.icns
|
|
34
|
+
edof/_apps/assets/icons/edof-viewer.ico
|
|
35
|
+
edof/_apps/assets/icons/edof-viewer.png
|
|
22
36
|
edof/api/__init__.py
|
|
23
37
|
edof/api/commands.py
|
|
24
38
|
edof/crypto/__init__.py
|
|
@@ -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.2"
|
|
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" }
|
|
@@ -66,6 +66,7 @@ include = ["edof*"]
|
|
|
66
66
|
|
|
67
67
|
[tool.setuptools.package-data]
|
|
68
68
|
edof = ["py.typed", "editor_lang/*.json"]
|
|
69
|
+
"edof._apps.assets.icons" = ["*.ico", "*.png", "*.icns"]
|
|
69
70
|
|
|
70
71
|
[tool.black]
|
|
71
72
|
line-length = 100
|
|
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
|