Skip to content

Commit 86bc5fe

Browse files
authored
Merge pull request #22 from timrid/feature/refactoring
Feature/refactoring
2 parents 6908672 + f28c306 commit 86bc5fe

38 files changed

Lines changed: 4005 additions & 1762 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Changelog
22
## [Unreleased]
3+
Complete refactoring of the code, so that core components of the construct-editor are seperated vom GUI components. That makes it theoretically possible to add multiple GUI frameworks in the future. Besides this the following notable enhancements are implemented:
4+
5+
Enhanced ConstructEditor:
6+
- Any keypress of an printable key will start editing an item. No ENTER or double click is reqired any more.
7+
- Protected entries (starting with _) are not visible in list view if "hide protected" is activated. (#13)
8+
- Implemented checkbox for `cs.Flag`
9+
- Fixed bug with PaddedString (#14)
10+
- Added module "construct_editor.core.custom" for easier addition of custom constructs.
11+
- Show arrays appropriately (use .[number]. instead of .number.) (#18)
12+
- Show full tooltip of the "name" column, which when fields are too long is shown with ellipses. (#18)
13+
- Implemented "Copy" / Ctrl+C (#18)
14+
- Added "Copy path to clipboard" button in the context menu (#18)
15+
- Fixed a bug, when a struct has multiple times the same `Enum` construct. Then the metadata (eg. byte position) of the last parsed enum value is used for all enum values.
16+
- Added exception dialog to show the complete parse/build exception.
17+
18+
Enhanced HexEditor:
19+
- fix crash when selecting or extending selection before the beginning of the hex editor using the shift LEFT and UP arrow keys (#20)
20+
- Add DELETE key to remove a single byte (#20)
21+
- Add INSERT key to add a single byte (#20)
22+
- Add BACK and DELETE key in hex cell editor (#20)
23+
- Add basic arrow keys in hex cell editor doing the same as Escape (#20)
24+
- Optimize paste from clipboard (#17)
325

426
-------------------------------------------------------------------------------
527
## [0.0.19] - 2022-09-07

README.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
# Construct Editor
2-
**!!! Work in progress !!!**
3-
42
This package provides a GUI (based on wxPython) for 'construct', which is a powerful declarative and symmetrical parser and builder for binary data. It can either be used standalone or embedded as a widget in another application.
53

64

@@ -15,21 +13,14 @@ Features:
1513
- FlagsEnum values
1614
- DateTime values
1715
- undo/redo in HexEditor and in ConstructEditor
18-
- extensible for custom adapters
16+
- extensible for custom constructs
1917

20-
## Installation (for use in your own project)
18+
## Installation
2119
The preferred way to installation is via PyPI:
2220
```
2321
pip install construct-editor
2422
```
2523

26-
## Installation as standalone
27-
The preferred way to installation is:
28-
- Open a command line in the project folder (`construct-editor`)
29-
- Create a new virtual environment via `virtualenv .venv`
30-
- Activate it with `.venv\Scripts\activate.ps1` or `.venv\Scripts\activate.bat`
31-
- Install via `pip install -e .` (remember the `.` at the end of the line)
32-
3324
## Getting started (Standalone)
3425
To start the standalone version, just execute the following in the command line:
3526
```
@@ -41,7 +32,7 @@ This is a simple example
4132
```python
4233
import wx
4334
import construct as cs
44-
import construct_editor as cseditor
35+
from construct_editor.widgets.wx import WxConstructHexEditor
4536

4637
constr = cs.Struct(
4738
"a" / cs.Int16sb,
@@ -50,27 +41,30 @@ constr = cs.Struct(
5041
b = bytes([0x12, 0x34, 0x56, 0x78])
5142

5243
app = wx.App(False)
53-
frame = wx.Frame(None, title="Construct Hex Editor", size=(1000, 500))
54-
editor_panel = cseditor.ConstructHexEditor(frame, construct=constr, binary=b)
44+
frame = wx.Frame(None, title="Construct Hex Editor", size=(1000, 200))
45+
editor_panel = WxConstructHexEditor(frame, construct=constr, binary=b)
5546
editor_panel.construct_editor.expand_all()
5647
frame.Show(True)
5748
app.MainLoop()
5849
```
5950

6051
This snipped generates a GUI like this:
6152

62-
[Screenshot of the example]
53+
![Screenshot of the example](https://raw.githubusercontent.com/timrid/construct-editor/main/doc/example.png)
54+
6355

6456
## Widgets
6557
### ConstructHexEditor
6658
This is the main widget ot this library. It offers a look at the raw binary data and also at the parsed structure.
6759
It offers a way to modify the raw binary data, which is then automaticly converted to the structed view. It also supports to modify the structed data and build the binary data from it.
6860

61+
6962
### ConstructEditor
7063
This is just the right side of the `ConstructHexEditor`, but can be used also used as standalone widget. It provides:
7164
- Viewing the structure of a construct (without binary data)
7265
- Parsing binary data according to the construct
7366

67+
7468
### HexEditor
7569
Just the left side of the `ConstructHexEditor`, but can be used also used as standalone widget. It provides:
7670
- Viewing Bytes in a Hexadecimal form

construct_editor/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
from .widgets.construct_editor import ConstructEditor
2-
from .widgets.construct_hex_editor import ConstructHexEditor
3-
from .widgets.hex_editor import HexEditorGrid
4-
5-
__all__ = ["ConstructHexEditor", "ConstructEditor", "HexEditorGrid"]

construct_editor/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-

construct_editor/core/callbacks.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
from typing import Callable, Generic, TypeVar
3+
4+
from typing_extensions import ParamSpec
5+
6+
T = TypeVar("T")
7+
P = ParamSpec("P")
8+
9+
10+
class CallbackList(Generic[P]):
11+
def __init__(self):
12+
self._callbacks = []
13+
14+
def append(self, callback: Callable[P, None]):
15+
"""
16+
Add new callback function to the list (ignroe duplicates)
17+
"""
18+
if callback not in self._callbacks:
19+
self._callbacks.append(callback)
20+
21+
def remove(self, callback: Callable[P, None]):
22+
"""
23+
Remove callback function from the list.
24+
"""
25+
self._callbacks.remove(callback)
26+
27+
def clear(self):
28+
"""
29+
Clear the complete list.
30+
"""
31+
self._callbacks.clear()
32+
33+
def fire(self, *args: P.args, **kwargs: P.kwargs):
34+
"""
35+
Call all callback functions, with the given parameters.
36+
"""
37+
for callback in self._callbacks:
38+
callback(*args, **kwargs)

construct_editor/core/commands.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# -*- coding: utf-8 -*-
2+
import abc
3+
import typing as t
4+
5+
6+
class Command:
7+
def __init__(self, can_undo: bool, name: str) -> None:
8+
self.can_undo: bool = can_undo
9+
self.name: str = name
10+
11+
@abc.abstractmethod
12+
def do(self) -> bool:
13+
...
14+
15+
@abc.abstractmethod
16+
def undo(self) -> bool:
17+
...
18+
19+
20+
class CommandProcessor:
21+
def __init__(self, max_commands: int) -> None:
22+
self._max_commands = max_commands
23+
24+
self._history: t.List[Command] = []
25+
self._current_command_idx: t.Optional[int] = None
26+
27+
def can_undo(self) -> bool:
28+
"""
29+
Returns true if the currently-active command can be undone, false
30+
otherwise.
31+
"""
32+
current_command = self.get_current_command()
33+
if current_command is None:
34+
return False
35+
else:
36+
return current_command.can_undo
37+
38+
def can_redo(self) -> bool:
39+
"""
40+
Returns true if the currently-active command can be redone, false
41+
otherwise.
42+
"""
43+
next_command = self.get_next_command()
44+
if next_command is None:
45+
return False
46+
else:
47+
return True
48+
49+
def redo(self) -> bool:
50+
"""
51+
Executes (redoes) the current command (the command that has just been
52+
undone if any).
53+
"""
54+
next_command = self.get_next_command()
55+
56+
# no command to redo in the history
57+
if next_command is None:
58+
return False
59+
60+
if next_command.do() is False:
61+
return False
62+
63+
self._increment_current_command()
64+
65+
return True
66+
67+
def undo(self) -> bool:
68+
"""
69+
Undoes the last command executed.
70+
"""
71+
current_command = self.get_current_command()
72+
73+
# no command available
74+
if current_command is None:
75+
return False
76+
77+
# command cant be undone
78+
if not current_command.can_undo:
79+
return False
80+
81+
# error on undo
82+
if current_command.undo() is False:
83+
return False
84+
85+
# set current command to previous command
86+
self._decrement_current_command()
87+
88+
return True
89+
90+
def submit(self, command: Command) -> None:
91+
"""
92+
Submits a new command to the command processor.
93+
94+
The command processor calls Command.do to execute the command; if it
95+
succeeds, the command is stored in the history list, and the associated
96+
edit menu (if any) updated appropriately. If it fails, the command is
97+
deleted immediately.
98+
"""
99+
command.do()
100+
self.store(command)
101+
102+
def store(self, command: Command) -> None:
103+
"""
104+
Just store the command without executing it.
105+
106+
Any command that has been undone will be chopped off the history list.
107+
"""
108+
# We must chop off the current 'branch', so that
109+
# we're at the end of the command list.
110+
if self._current_command_idx is None:
111+
self.clear_commands()
112+
else:
113+
self._history = self._history[: self._current_command_idx + 1]
114+
115+
# Limit history length. Remove fist commands from history
116+
# if an overflow occures
117+
if len(self._history) >= self._max_commands:
118+
if self._current_command_idx is None:
119+
raise ValueError("history and current_command_idx are out of sync")
120+
self._history.pop(0)
121+
self._current_command_idx = self._current_command_idx - 1
122+
123+
# append command to history
124+
self._current_command_idx = len(self._history)
125+
self._history.append(command)
126+
127+
def clear_commands(self):
128+
"""
129+
Deletes all commands in the list and sets the current command pointer to None.
130+
"""
131+
self._history.clear()
132+
self._current_command_idx = None
133+
134+
def get_current_command(self) -> t.Optional[Command]:
135+
"""
136+
Returns the current command.
137+
"""
138+
if self._current_command_idx is None:
139+
return None
140+
141+
return self._history[self._current_command_idx]
142+
143+
def get_next_command(self) -> t.Optional[Command]:
144+
"""
145+
Returns the next command.
146+
"""
147+
if self._current_command_idx is None:
148+
next_command_idx = 0
149+
else:
150+
next_command_idx = self._current_command_idx + 1
151+
152+
if next_command_idx >= len(self._history):
153+
return None
154+
155+
return self._history[next_command_idx]
156+
157+
def _decrement_current_command(self) -> None:
158+
if self._current_command_idx is None:
159+
return
160+
if self._current_command_idx > 0:
161+
self._current_command_idx -= 1
162+
else:
163+
self._current_command_idx = None
164+
165+
def _increment_current_command(self) -> None:
166+
if self._current_command_idx is None:
167+
next_command_idx = 0
168+
else:
169+
next_command_idx = self._current_command_idx + 1
170+
171+
if next_command_idx >= len(self._history):
172+
return
173+
174+
self._current_command_idx = next_command_idx

0 commit comments

Comments
 (0)