edof 4.2.1__tar.gz → 4.2.3__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.3}/PKG-INFO +8 -8
- {edof-4.2.1 → edof-4.2.3}/README.md +7 -7
- {edof-4.2.1 → edof-4.2.3}/edof/__init__.py +2 -1
- edof-4.2.3/edof/_apps/_assoc_dialog.py +92 -0
- edof-4.2.3/edof/_apps/assets/__init__.py +34 -0
- edof-4.2.3/edof/_apps/assets/icons/__init__.py +1 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-document-512.png +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-document.icns +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-document.ico +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-document.png +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-editor-512.png +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-editor.icns +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-editor.ico +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-editor.png +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-viewer-512.png +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-viewer.icns +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-viewer.ico +0 -0
- edof-4.2.3/edof/_apps/assets/icons/edof-viewer.png +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/cli.py +4 -3
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/editor.py +22 -34
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/edof_text_editor.py +22 -0
- edof-4.2.3/edof/_apps/file_assoc.py +389 -0
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/viewer.py +40 -21
- {edof-4.2.1 → edof-4.2.3}/edof/engine/document_paginate.py +17 -2
- {edof-4.2.1 → edof-4.2.3}/edof/format/styles.py +3 -1
- {edof-4.2.1 → edof-4.2.3}/edof/version.py +1 -1
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/PKG-INFO +8 -8
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/SOURCES.txt +15 -0
- {edof-4.2.1 → edof-4.2.3}/pyproject.toml +2 -1
- edof-4.2.1/edof/_apps/file_assoc.py +0 -251
- {edof-4.2.1 → edof-4.2.3}/LICENSE +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/_apps/shortcuts.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/api/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/api/commands.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/crypto/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/crypto/document_protection.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/crypto/encryption.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/crypto/permissions.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/editor_lang/en.json +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/color.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/debug_log.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/document_flow.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/renderer.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/text_engine.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/text_layout.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/textbox_flow.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/engine/transform.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/exceptions.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/bitmap.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/legacy_v3.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/pdf.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/pdf_writer.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/printer.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/export/svg.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/document.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/document_body.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/document_boxes.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/objects.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/serializer.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/svg_io.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/format/variables.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/gui/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/gui/pyqt6_widget.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/gui/tkinter_canvas.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/interop/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/interop/docx_io.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/py.typed +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/units.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/__init__.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/compat.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/legacy_v2.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/pdf_import.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/qr.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/rtf.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof/utils/safe_eval.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/dependency_links.txt +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/entry_points.txt +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/requires.txt +0 -0
- {edof-4.2.1 → edof-4.2.3}/edof.egg-info/top_level.txt +0 -0
- {edof-4.2.1 → edof-4.2.3}/setup.cfg +0 -0
- {edof-4.2.1 → edof-4.2.3}/tests/test_v310.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/tests/test_v400.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/tests/test_v401.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/tests/test_v402.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/tests/test_v403.py +0 -0
- {edof-4.2.1 → edof-4.2.3}/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.3
|
|
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,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()
|
|
@@ -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
|
|
@@ -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}")
|
|
@@ -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,42 +10305,14 @@ class EdofEditor(QMainWindow):
|
|
|
10289
10305
|
dlg.exec()
|
|
10290
10306
|
|
|
10291
10307
|
def _open_file_assoc_dialog(self):
|
|
10292
|
-
"""
|
|
10308
|
+
"""Manage the .edof file association (choose app in-app, register/remove)."""
|
|
10293
10309
|
try:
|
|
10294
|
-
from edof._apps.
|
|
10295
|
-
associate_edof_files, unassociate_edof_files,
|
|
10296
|
-
current_association_status,
|
|
10297
|
-
)
|
|
10310
|
+
from edof._apps._assoc_dialog import manage_association
|
|
10298
10311
|
except Exception as e:
|
|
10299
10312
|
QMessageBox.warning(self, "File association",
|
|
10300
10313
|
f"Could not load file association module: {e}")
|
|
10301
10314
|
return
|
|
10302
|
-
|
|
10303
|
-
is_assoc = "associated" in status.lower() and "not " not in status.lower()
|
|
10304
|
-
msg = (f"<h3>File association</h3>"
|
|
10305
|
-
f"<p>Current status: <b>{status}</b></p>"
|
|
10306
|
-
f"<p>When .edof files are associated, double-clicking a "
|
|
10307
|
-
f"<code>.edof</code> file in your file manager opens it in "
|
|
10308
|
-
f"<b>edof-viewer</b> (read-only).</p>"
|
|
10309
|
-
f"<p>{'<b>Currently associated.</b> Click Unassociate to remove.' if is_assoc else 'Click Associate to register .edof files.'}</p>")
|
|
10310
|
-
bb_text = "Unassociate" if is_assoc else "Associate"
|
|
10311
|
-
ret = QMessageBox.question(self, "File association", msg,
|
|
10312
|
-
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
|
|
10313
|
-
if ret != QMessageBox.StandardButton.Ok: return
|
|
10314
|
-
try:
|
|
10315
|
-
if is_assoc:
|
|
10316
|
-
ok, info = unassociate_edof_files()
|
|
10317
|
-
else:
|
|
10318
|
-
ok, info = associate_edof_files()
|
|
10319
|
-
if ok:
|
|
10320
|
-
QMessageBox.information(self, "File association",
|
|
10321
|
-
f"<p><b>Success.</b></p><p>{info}</p>")
|
|
10322
|
-
else:
|
|
10323
|
-
QMessageBox.warning(self, "File association",
|
|
10324
|
-
f"<p><b>Failed.</b></p><p>{info}</p>")
|
|
10325
|
-
except Exception as e:
|
|
10326
|
-
QMessageBox.critical(self, "File association",
|
|
10327
|
-
f"Error: {e}")
|
|
10315
|
+
manage_association(self)
|
|
10328
10316
|
|
|
10329
10317
|
# ─────────────────────────────────────────────────────────────────────
|
|
10330
10318
|
# v4.1.2: Multi-window support for embedded sub-documents
|
|
@@ -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.
|