edof 4.2.0__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.0 → edof-4.2.2}/PKG-INFO +12 -10
- {edof-4.2.0 → edof-4.2.2}/README.md +9 -8
- {edof-4.2.0 → 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.0 → edof-4.2.2}/edof/_apps/editor.py +32 -12
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/edof_text_editor.py +22 -0
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/file_assoc.py +142 -39
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/viewer.py +70 -18
- {edof-4.2.0 → edof-4.2.2}/edof/engine/document_paginate.py +17 -2
- {edof-4.2.0 → edof-4.2.2}/edof/format/styles.py +3 -1
- {edof-4.2.0 → edof-4.2.2}/edof/version.py +1 -1
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/PKG-INFO +12 -10
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/SOURCES.txt +14 -0
- {edof-4.2.0 → edof-4.2.2}/pyproject.toml +4 -2
- {edof-4.2.0 → edof-4.2.2}/LICENSE +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/cli.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/_apps/shortcuts.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/api/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/api/commands.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/crypto/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/crypto/document_protection.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/crypto/encryption.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/crypto/permissions.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/editor_lang/en.json +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/color.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/debug_log.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/document_flow.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/renderer.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/text_engine.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/text_layout.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/textbox_flow.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/engine/transform.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/exceptions.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/bitmap.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/legacy_v3.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/pdf.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/pdf_writer.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/printer.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/export/svg.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/document.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/document_body.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/document_boxes.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/objects.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/serializer.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/svg_io.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/format/variables.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/gui/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/gui/pyqt6_widget.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/gui/tkinter_canvas.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/interop/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/interop/docx_io.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/py.typed +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/units.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/__init__.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/compat.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/legacy_v2.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/pdf_import.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/qr.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/rtf.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof/utils/safe_eval.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/dependency_links.txt +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/entry_points.txt +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/requires.txt +0 -0
- {edof-4.2.0 → edof-4.2.2}/edof.egg-info/top_level.txt +0 -0
- {edof-4.2.0 → edof-4.2.2}/setup.cfg +0 -0
- {edof-4.2.0 → edof-4.2.2}/tests/test_v310.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/tests/test_v400.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/tests/test_v401.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/tests/test_v402.py +0 -0
- {edof-4.2.0 → edof-4.2.2}/tests/test_v403.py +0 -0
- {edof-4.2.0 → 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
|
|
|
@@ -29,7 +29,8 @@ Project-URL: Documentation, https://davidschobl.github.io/edof/
|
|
|
29
29
|
Project-URL: Repository, https://github.com/DavidSchobl/edof
|
|
30
30
|
Project-URL: Bug Tracker, https://github.com/DavidSchobl/edof/issues
|
|
31
31
|
Project-URL: Changelog, https://github.com/DavidSchobl/edof/blob/main/CHANGELOG.md
|
|
32
|
-
Project-URL: Funding, https://
|
|
32
|
+
Project-URL: Funding, https://ko-fi.com/davidschobl
|
|
33
|
+
Project-URL: Sponsor, https://github.com/sponsors/DavidSchobl
|
|
33
34
|
Keywords: document,format,pdf,svg,template,edof,print,vector
|
|
34
35
|
Classifier: Development Status :: 4 - Beta
|
|
35
36
|
Classifier: Intended Audience :: Developers
|
|
@@ -87,9 +88,10 @@ Dynamic: license-file
|
|
|
87
88
|
[](https://github.com/DavidSchobl/edof/actions/workflows/publish.yml)
|
|
88
89
|
[](https://pepy.tech/project/edof)
|
|
89
90
|
[](https://github.com/sponsors/DavidSchobl)
|
|
91
|
+
[](https://ko-fi.com/davidschobl)
|
|
90
92
|
|
|
91
93
|
📚 **Documentation**: <https://davidschobl.github.io/edof/> |
|
|
92
|
-
💖 **Support development**: <https://github.com/sponsors/DavidSchobl>
|
|
94
|
+
💖 **Support development**: <https://ko-fi.com/davidschobl> | <https://github.com/sponsors/DavidSchobl>
|
|
93
95
|
|
|
94
96
|
A Python library and visual editor for programmatic document creation, template filling, and high-quality export. Documents are described in code or in a small ZIP-based file format, then rendered to PNG, JPEG, TIFF, BMP, PDF, RTF, or SVG. A PyQt6 desktop editor is included for visual editing with Photoshop-style layer effects, table cell editor, multi-blend compositing, and a path tool.
|
|
95
97
|
|
|
@@ -183,7 +185,7 @@ A `Document` contains pages; each `Page` contains objects. All measurements are
|
|
|
183
185
|
| `Table` | Formatted table with per-cell styling | `cells`, `col_widths`, `row_heights`, `table_border` |
|
|
184
186
|
| `Group` | Container with optional clip | `children` |
|
|
185
187
|
|
|
186
|
-
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+).
|
|
187
189
|
|
|
188
190
|
#### Transform fields
|
|
189
191
|
|
|
@@ -205,7 +207,7 @@ All color fields accept tuples in 0–255 range. Both RGB (3-tuple) and RGBA (4-
|
|
|
205
207
|
obj.style.color = (74, 144, 226) # RGB → alpha 255 implied
|
|
206
208
|
obj.fill.color = (245, 245, 248, 255) # RGBA — explicit
|
|
207
209
|
obj.stroke.color = (0, 0, 0, 200) # RGBA — translucent black
|
|
208
|
-
shape.fill.color = "#4a90e2" # v4.
|
|
210
|
+
shape.fill.color = "#4a90e2" # v4.2.0+: hex string accepted
|
|
209
211
|
```
|
|
210
212
|
|
|
211
213
|
When read back, colors come as tuples (`obj.style.color` returns `(74, 144, 226)`, `obj.fill.color` returns `(245, 245, 248, 255)`).
|
|
@@ -467,7 +469,7 @@ heading.lock_text = True # text never editable until lock_text is cleared
|
|
|
467
469
|
### Headings with auto-computed height (no more silent text loss)
|
|
468
470
|
|
|
469
471
|
```python
|
|
470
|
-
# v4.
|
|
472
|
+
# v4.2.0+: add_textbox_auto computes height from content and accepts style kwargs
|
|
471
473
|
heading = page.add_textbox_auto(
|
|
472
474
|
20, 20, 170, "MY HEADING",
|
|
473
475
|
font_size=24, bold=True, alignment="center",
|
|
@@ -479,7 +481,7 @@ next_y = heading.transform.y + heading.transform.height + 5 # 5 mm gap
|
|
|
479
481
|
### Tables with explicit sizing
|
|
480
482
|
|
|
481
483
|
```python
|
|
482
|
-
# v4.
|
|
484
|
+
# v4.2.0+: make_table accepts position + size and computes total height
|
|
483
485
|
tbl = edof.make_table(
|
|
484
486
|
rows=[
|
|
485
487
|
["Product", "Qty", "Price"],
|
|
@@ -507,7 +509,7 @@ tb.style.color = edof.as_color("#4a90e2") # hex via helper
|
|
|
507
509
|
tb.style.color = edof.as_color("#4a90e2cc") # 8-digit hex with alpha
|
|
508
510
|
```
|
|
509
511
|
|
|
510
|
-
### Per-side padding (v4.
|
|
512
|
+
### Per-side padding (v4.2.0+)
|
|
511
513
|
|
|
512
514
|
```python
|
|
513
515
|
tb.style.padding = 2.0 # uniform 2 mm on all sides
|
|
@@ -554,7 +556,7 @@ A PyQt6 desktop editor (`edof-editor`) ships with the library. It is a working e
|
|
|
554
556
|
|
|
555
557
|
## Viewer
|
|
556
558
|
|
|
557
|
-
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.
|
|
558
560
|
|
|
559
561
|
```bash
|
|
560
562
|
pip install 'edof[viewer]'
|
|
@@ -696,7 +698,7 @@ A small `.bat` makes switching painless:
|
|
|
696
698
|
@echo off
|
|
697
699
|
call D:\apps\Edof_V401\edof-python\.venv\Scripts\activate.bat
|
|
698
700
|
cd /d D:\apps\Edof_V401\edof-python
|
|
699
|
-
cmd /k prompt [edof v4.
|
|
701
|
+
cmd /k prompt [edof v4.2.2] $P$G
|
|
700
702
|
```
|
|
701
703
|
|
|
702
704
|
Each version's venv is independent. Removing a version is `rmdir /s /q <folder>`; nothing else needs cleanup.
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
[](https://github.com/DavidSchobl/edof/actions/workflows/publish.yml)
|
|
7
7
|
[](https://pepy.tech/project/edof)
|
|
8
8
|
[](https://github.com/sponsors/DavidSchobl)
|
|
9
|
+
[](https://ko-fi.com/davidschobl)
|
|
9
10
|
|
|
10
11
|
📚 **Documentation**: <https://davidschobl.github.io/edof/> |
|
|
11
|
-
💖 **Support development**: <https://github.com/sponsors/DavidSchobl>
|
|
12
|
+
💖 **Support development**: <https://ko-fi.com/davidschobl> | <https://github.com/sponsors/DavidSchobl>
|
|
12
13
|
|
|
13
14
|
A Python library and visual editor for programmatic document creation, template filling, and high-quality export. Documents are described in code or in a small ZIP-based file format, then rendered to PNG, JPEG, TIFF, BMP, PDF, RTF, or SVG. A PyQt6 desktop editor is included for visual editing with Photoshop-style layer effects, table cell editor, multi-blend compositing, and a path tool.
|
|
14
15
|
|
|
@@ -102,7 +103,7 @@ A `Document` contains pages; each `Page` contains objects. All measurements are
|
|
|
102
103
|
| `Table` | Formatted table with per-cell styling | `cells`, `col_widths`, `row_heights`, `table_border` |
|
|
103
104
|
| `Group` | Container with optional clip | `children` |
|
|
104
105
|
|
|
105
|
-
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+).
|
|
106
107
|
|
|
107
108
|
#### Transform fields
|
|
108
109
|
|
|
@@ -124,7 +125,7 @@ All color fields accept tuples in 0–255 range. Both RGB (3-tuple) and RGBA (4-
|
|
|
124
125
|
obj.style.color = (74, 144, 226) # RGB → alpha 255 implied
|
|
125
126
|
obj.fill.color = (245, 245, 248, 255) # RGBA — explicit
|
|
126
127
|
obj.stroke.color = (0, 0, 0, 200) # RGBA — translucent black
|
|
127
|
-
shape.fill.color = "#4a90e2" # v4.
|
|
128
|
+
shape.fill.color = "#4a90e2" # v4.2.0+: hex string accepted
|
|
128
129
|
```
|
|
129
130
|
|
|
130
131
|
When read back, colors come as tuples (`obj.style.color` returns `(74, 144, 226)`, `obj.fill.color` returns `(245, 245, 248, 255)`).
|
|
@@ -386,7 +387,7 @@ heading.lock_text = True # text never editable until lock_text is cleared
|
|
|
386
387
|
### Headings with auto-computed height (no more silent text loss)
|
|
387
388
|
|
|
388
389
|
```python
|
|
389
|
-
# v4.
|
|
390
|
+
# v4.2.0+: add_textbox_auto computes height from content and accepts style kwargs
|
|
390
391
|
heading = page.add_textbox_auto(
|
|
391
392
|
20, 20, 170, "MY HEADING",
|
|
392
393
|
font_size=24, bold=True, alignment="center",
|
|
@@ -398,7 +399,7 @@ next_y = heading.transform.y + heading.transform.height + 5 # 5 mm gap
|
|
|
398
399
|
### Tables with explicit sizing
|
|
399
400
|
|
|
400
401
|
```python
|
|
401
|
-
# v4.
|
|
402
|
+
# v4.2.0+: make_table accepts position + size and computes total height
|
|
402
403
|
tbl = edof.make_table(
|
|
403
404
|
rows=[
|
|
404
405
|
["Product", "Qty", "Price"],
|
|
@@ -426,7 +427,7 @@ tb.style.color = edof.as_color("#4a90e2") # hex via helper
|
|
|
426
427
|
tb.style.color = edof.as_color("#4a90e2cc") # 8-digit hex with alpha
|
|
427
428
|
```
|
|
428
429
|
|
|
429
|
-
### Per-side padding (v4.
|
|
430
|
+
### Per-side padding (v4.2.0+)
|
|
430
431
|
|
|
431
432
|
```python
|
|
432
433
|
tb.style.padding = 2.0 # uniform 2 mm on all sides
|
|
@@ -473,7 +474,7 @@ A PyQt6 desktop editor (`edof-editor`) ships with the library. It is a working e
|
|
|
473
474
|
|
|
474
475
|
## Viewer
|
|
475
476
|
|
|
476
|
-
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.
|
|
477
478
|
|
|
478
479
|
```bash
|
|
479
480
|
pip install 'edof[viewer]'
|
|
@@ -615,7 +616,7 @@ A small `.bat` makes switching painless:
|
|
|
615
616
|
@echo off
|
|
616
617
|
call D:\apps\Edof_V401\edof-python\.venv\Scripts\activate.bat
|
|
617
618
|
cd /d D:\apps\Edof_V401\edof-python
|
|
618
|
-
cmd /k prompt [edof v4.
|
|
619
|
+
cmd /k prompt [edof v4.2.2] $P$G
|
|
619
620
|
```
|
|
620
621
|
|
|
621
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)
|
|
@@ -10534,13 +10552,14 @@ class EdofEditor(QMainWindow):
|
|
|
10534
10552
|
self._canvas.schedule_render(100)
|
|
10535
10553
|
|
|
10536
10554
|
def _open_donate(self):
|
|
10537
|
-
"""v4.1.0: Open the
|
|
10555
|
+
"""v4.1.0: Open the donate page (Ko-fi) in the user's browser."""
|
|
10538
10556
|
try:
|
|
10539
10557
|
import webbrowser
|
|
10540
|
-
webbrowser.open("https://
|
|
10558
|
+
webbrowser.open("https://ko-fi.com/davidschobl")
|
|
10541
10559
|
except Exception as e:
|
|
10542
10560
|
QMessageBox.information(self, "Support",
|
|
10543
10561
|
f"To support the developer, visit:\n"
|
|
10562
|
+
f"https://ko-fi.com/davidschobl\n"
|
|
10544
10563
|
f"https://github.com/sponsors/DavidSchobl\n\n({e})")
|
|
10545
10564
|
|
|
10546
10565
|
def _show_about(self):
|
|
@@ -10580,8 +10599,9 @@ class EdofEditor(QMainWindow):
|
|
|
10580
10599
|
f"<a href='https://pypi.org/project/edof/'>pypi.org/project/edof</a></p>"
|
|
10581
10600
|
f"<h3>Support the developer</h3>"
|
|
10582
10601
|
f"<p>If edof saves you time and you'd like to support its development, "
|
|
10583
|
-
f"
|
|
10584
|
-
f"
|
|
10602
|
+
f"you can donate any amount via "
|
|
10603
|
+
f"<a href='https://ko-fi.com/davidschobl'>Ko-fi</a> or "
|
|
10604
|
+
f"<a href='https://github.com/sponsors/DavidSchobl'>GitHub Sponsors</a>. "
|
|
10585
10605
|
f"Every contribution helps keep edof maintained and free.</p>"
|
|
10586
10606
|
f"<p>License: MIT | © 2025 DavidSchobl</p>")
|
|
10587
10607
|
|
|
@@ -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__":
|