forked from Wobbl/Bread_Editor
Compare commits
17 commits
Author | SHA1 | Date | |
---|---|---|---|
436cb3018c | |||
2cb7b80cb8 | |||
e863815b41 | |||
9a3789a99b | |||
d3b3dbc754 | |||
5cf5885d4b | |||
fb7a160d00 | |||
47ede1e689 | |||
ea8b7456cc | |||
b7a6ba567a | |||
b1b442b23a | |||
b2afa06ee1 | |||
93408e9a29 | |||
409213585a | |||
46f49804a8 | |||
7235736309 | |||
de64c08532 |
21 changed files with 231 additions and 149 deletions
32
README.md
32
README.md
|
@ -6,24 +6,32 @@ So I just made one.
|
|||
|
||||
### Features
|
||||
|
||||
| Feature | Description | State |
|
||||
|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| Editing of basic image formats /<br>"Syntax Highlighting" | If u have a very simple black/white image format<br/>where single bits control the color,<br/>the editor can highlight the enabled bits. | <input type="checkbox" disabled checked /> Implemented |
|
||||
| Feature | Description | State |
|
||||
|-----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| Editing of basic image formats /<br>"Syntax Highlighting" | If you have a very simple black/white image format<br/>where single bits control the color,<br/>the editor can highlight the enabled bits. | <input type="checkbox" disabled checked /> Implemented |
|
||||
|
||||
## Setup
|
||||
|
||||
This program was made for Linux.\
|
||||
It may work on Windows or Mac too, but it was not tested on these systems.\
|
||||
To be able to use this program, you have to clone the repository,\
|
||||
install the requirements and make the file called "main.py" executable.\
|
||||
This program was made for Linux. It may work on Windows or Mac too, but it was not tested on these systems.\
|
||||
Before you can install the editor, you first have to install the requirements. That can be done using:
|
||||
|
||||
```bash
|
||||
sudo apt install pyqt6-dev-tools xcb libxcb-cursor0
|
||||
```
|
||||
|
||||
And to install the editor, you just have to clone the repository and install it using pip.\
|
||||
You can do that using these commands:
|
||||
|
||||
```bash
|
||||
git clone git@teapot.informationsanarchistik.de:Wobbl/Bread_Editor.git
|
||||
git clone https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor.git
|
||||
cd Bread_Editor
|
||||
pip install -r requirements.txt
|
||||
chmod +x main.py
|
||||
pip install .
|
||||
```
|
||||
|
||||
Now you can execute the program using `./main.py`.
|
||||
You can also create a desktop shortcut to the file.
|
||||
If you have already set up git for ssh, you can also clone the repository like this:
|
||||
|
||||
```bash
|
||||
git clone git git@teapot.informationsanarchistik.de:Wobbl/Bread_Editor.git
|
||||
```
|
||||
|
||||
You can now start the editor by typing in the terminal: `bread_editor`
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QPlainTextEdit
|
||||
|
||||
|
||||
class BinaryTextEdit(QPlainTextEdit): # rewrite QPlainTextEdit.keyPressEvent because it has no .setValidator()
|
||||
def keyPressEvent(self, event):
|
||||
allowed_keys = {"", "0", "1"}
|
||||
|
||||
if event.text() in allowed_keys:
|
||||
cursor = self.textCursor()
|
||||
pos = cursor.position()
|
||||
text = self.toPlainText()
|
||||
text_length = len(text)
|
||||
|
||||
if not (pos + 1) % 9 == 0 or event.text() in {"", None}: # dont overwrite the separator character
|
||||
super().keyPressEvent(event)
|
||||
|
||||
# skip over the separator character when the cursor is before it.
|
||||
if (pos + 2) % 9 == 0 and not event.text() in {"", None}:
|
||||
if pos == text_length: # append to the input if the cursor is at the end
|
||||
self.insertPlainText(" ")
|
||||
cursor = self.textCursor()
|
||||
cursor.setPosition(pos + 2)
|
||||
|
||||
cursor.setPosition(pos + 2)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
else:
|
||||
event.ignore()
|
1
bread_editor/__init__.py
Normal file
1
bread_editor/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/python3
|
50
bread_editor/binary_text_edit.py
Normal file
50
bread_editor/binary_text_edit.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QPlainTextEdit
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
|
||||
class BinaryTextEdit(QPlainTextEdit): # rewrite QPlainTextEdit.keyPressEvent because it has no .setValidator()
|
||||
def keyPressEvent(self, event):
|
||||
allowed_keys = {"", "0", "1"}
|
||||
|
||||
if event.text() in allowed_keys:
|
||||
cursor = self.textCursor()
|
||||
position = cursor.position()
|
||||
text = self.toPlainText()
|
||||
text_length = len(text)
|
||||
|
||||
if not (position + 1) % 9 == 0 or event.text() in {"", None}: # dont overwrite the separator character
|
||||
super().keyPressEvent(event)
|
||||
|
||||
# skip over the separator character when the cursor is right before it.
|
||||
if (position + 2) % 9 == 0 and not event.text() in {"", None}:
|
||||
if position == text_length - 1: # append to the input if the cursor is at the end
|
||||
self.insertPlainText(" 00000000")
|
||||
|
||||
cursor.setPosition(position + 2)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
elif event.key() == Qt.Key.Key_Backspace or event.key() == Qt.Key.Key_Delete:
|
||||
# delete last byte when backspace or delete is pressed
|
||||
text = self.toPlainText()
|
||||
|
||||
if len(text) >= 9:
|
||||
cursor = self.textCursor()
|
||||
position = cursor.position()
|
||||
|
||||
text = text[:-9] # delete last byte
|
||||
|
||||
self.setPlainText(text)
|
||||
|
||||
# calculate the new cursor position (by subtracting 9, we set the position to the same bit but one byte
|
||||
# before and by floor dividing this by 9 we get the "byte index" and when we multiply this by 9, we get
|
||||
# the character position of the first bit in that byte.)
|
||||
position = (position - 9) // 9 * 9
|
||||
|
||||
cursor.setPosition(position)
|
||||
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
else:
|
||||
event.ignore()
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
from PyQt6.QtWidgets import QWidget, QVBoxLayout
|
||||
from PyQt6.QtGui import QFont, QTextCharFormat, QColor
|
||||
from binary_text_edit import BinaryTextEdit
|
||||
from highlighting import Higlighter
|
||||
from bread_editor.binary_text_edit import BinaryTextEdit
|
||||
from bread_editor.highlighting import Higlighter
|
||||
|
||||
|
||||
class BitEditor:
|
||||
|
@ -55,6 +55,11 @@ class BitEditor:
|
|||
self.font.setLetterSpacing(QFont.SpacingType.PercentageSpacing, spacing)
|
||||
self.input.setFont(self.font)
|
||||
|
||||
self.cursor_width = self.input.fontMetrics().averageCharWidth()
|
||||
|
||||
# set the cursor with to match the letter spacing
|
||||
self.input.setCursorWidth(self.cursor_width + 1) # + 1 because else it somehow draws a 1px wide vertical line
|
||||
|
||||
highlight_ones = self.app.settings.highlight_ones
|
||||
|
||||
highlighter_document = self.input.document() if highlight_ones else None
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/python3
|
||||
import os.path
|
||||
|
||||
from PyQt6.QtWidgets import QFileDialog, QTabWidget
|
||||
from editor import BitEditor
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
from bread_editor.editor import BitEditor
|
||||
|
||||
MAX_FILE_SIZE = 262144 # 2^18
|
||||
|
||||
|
@ -20,15 +20,12 @@ class FileActions:
|
|||
dialog.setViewMode(QFileDialog.ViewMode.List)
|
||||
|
||||
if dialog.exec():
|
||||
for file_path in dialog.selectedFiles():
|
||||
if not file_path in self.app.open_files: # dont open file twice
|
||||
if os.path.getsize(file_path) > MAX_FILE_SIZE:
|
||||
self.app.utils.ftb_popup.exec()
|
||||
return
|
||||
self.open_multiple_files(dialog.selectedFiles())
|
||||
|
||||
self.app.open_files[file_path] = File(self.app, file_path, file_path.split("/")[-1])
|
||||
def create_file(self, content: bytes=(0).to_bytes(1, "big")):
|
||||
# open a dialog where the user can choose a new filepath and create an empty file at that path
|
||||
# bytes=(0).to_bytes(1) creates a byte with only zeros in it
|
||||
|
||||
def create_file(self, content: bin=b""):
|
||||
file_path, extension = QFileDialog.getSaveFileName(
|
||||
caption="New File",
|
||||
directory=self.app.utils.home_path,
|
||||
|
@ -82,6 +79,16 @@ class FileActions:
|
|||
for file_path in self.app.open_files:
|
||||
self.app.open_files[file_path].save()
|
||||
|
||||
def open_multiple_files(self, file_paths):
|
||||
for file_path in file_paths:
|
||||
if not file_path in self.app.open_files and os.path.isfile(file_path):
|
||||
if os.path.getsize(file_path) > MAX_FILE_SIZE:
|
||||
self.app.utils.ftb_popup.exec()
|
||||
return
|
||||
|
||||
file = File(self.app, file_path, file_path.split("/")[-1])
|
||||
self.app.open_files[file_path] = file
|
||||
|
||||
|
||||
class File:
|
||||
def __init__(self, app, path, name):
|
1
bread_editor/gui/__init__.py
Normal file
1
bread_editor/gui/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/python3
|
1
bread_editor/gui/raw_ui/__init__.py
Normal file
1
bread_editor/gui/raw_ui/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
#!/usr/bin/python3
|
36
bread_editor/gui/raw_ui/ui_to_py.py
Normal file
36
bread_editor/gui/raw_ui/ui_to_py.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def convert_ui():
|
||||
working_dir = os.getcwd()
|
||||
|
||||
if not working_dir.split('/')[
|
||||
-1] == "raw_ui": # cd into the right directory if this gets executed from somewhere else
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
for ui_file, script_out in paths.items():
|
||||
os.system(f"pyuic6 {ui_file} {params} ../{script_out}")
|
||||
|
||||
os.chdir(working_dir) # switch back to the working directory from which the script was executed
|
||||
|
||||
|
||||
if __name__ == "__main__": # dont ask for debug on setup
|
||||
input_debug = input("Do you want to debug the gui scripts? (Make gui scripts executable.) (y/n): ")
|
||||
|
||||
debug = input_debug == "y"
|
||||
|
||||
convert_ui()
|
||||
|
||||
else:
|
||||
debug = False
|
||||
|
||||
params = "-o"
|
||||
|
||||
if debug:
|
||||
params = "-xo"
|
||||
|
||||
paths = {
|
||||
"main_window.ui": "main_window.py"
|
||||
}
|
|
@ -3,11 +3,11 @@
|
|||
import sys
|
||||
from PyQt6.QtCore import QTimer
|
||||
from wobbl_tools.data_file import load_dataclass_json
|
||||
from utils import Utils
|
||||
from file import File, FileActions
|
||||
from ui import GUI
|
||||
from settings import Settings
|
||||
from ipc import IPC
|
||||
from bread_editor.utils import Utils
|
||||
from bread_editor.file import File, FileActions
|
||||
from bread_editor.ui import GUI
|
||||
from bread_editor.settings import Settings
|
||||
from bread_editor.ipc import IPC
|
||||
|
||||
|
||||
class BreadEditor:
|
||||
|
@ -24,7 +24,6 @@ class BreadEditor:
|
|||
|
||||
self.gui = GUI(self)
|
||||
|
||||
self.gui.connect_gui(self)
|
||||
self.open_files: dict[str, File] = {}
|
||||
self.open_files_queue = []
|
||||
|
||||
|
@ -38,14 +37,19 @@ class BreadEditor:
|
|||
|
||||
self.gui.post_setup()
|
||||
|
||||
self.utils.on_start()
|
||||
|
||||
self.utils.popup_init()
|
||||
|
||||
self.utils.on_start()
|
||||
|
||||
self.gui.QTMainWindow.show()
|
||||
sys.exit(self.gui.qt_app.exec())
|
||||
|
||||
|
||||
def start_from_command_line():
|
||||
editor = BreadEditor()
|
||||
editor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
editor = BreadEditor()
|
||||
editor.run()
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from utils import Utils
|
||||
from bread_editor.utils import Utils
|
||||
|
||||
|
||||
@dataclass
|
44
bread_editor/ui.py
Normal file
44
bread_editor/ui.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow
|
||||
from bread_editor.gui.main_window import Ui_MainWindow
|
||||
|
||||
|
||||
class GUI:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.qt_app = QApplication([])
|
||||
self.QTMainWindow = QMainWindow()
|
||||
|
||||
self.main_window = Ui_MainWindow()
|
||||
|
||||
self.setup_gui()
|
||||
self.connect_gui()
|
||||
|
||||
def setup_gui(self):
|
||||
self.main_window.setupUi(self.QTMainWindow)
|
||||
self.main_window.settingsDock.hide()
|
||||
|
||||
def post_setup(self):
|
||||
self.main_window.bitsAreSquaresSetting.blockSignals(True)
|
||||
self.main_window.bitsAreSquaresSetting.setChecked(self.app.settings.square_bits)
|
||||
self.main_window.bitsAreSquaresSetting.blockSignals(False)
|
||||
|
||||
self.main_window.highlightOnesSetting.setChecked(self.app.settings.highlight_ones)
|
||||
|
||||
self.app.utils.update_style_in_all_bit_editors()
|
||||
|
||||
def connect_gui(self):
|
||||
self.main_window.openFile.triggered.connect(self.app.file_actions.open_files)
|
||||
self.main_window.newFile.triggered.connect(lambda: self.app.file_actions.create_file())
|
||||
self.main_window.saveFile.triggered.connect(self.app.file_actions.save_current_file)
|
||||
self.main_window.saveFileAs.triggered.connect(self.app.file_actions.save_current_file_as)
|
||||
|
||||
self.main_window.menuSettings.triggered.connect(self.main_window.settingsDock.show)
|
||||
|
||||
self.main_window.bitsAreSquaresSetting.stateChanged.connect(self.app.utils.update_style_in_all_bit_editors)
|
||||
self.main_window.highlightOnesSetting.stateChanged.connect(self.app.utils.update_style_in_all_bit_editors)
|
||||
|
||||
self.main_window.openFileTabs.tabCloseRequested.connect(self.app.file_actions.close_current_file)
|
||||
self.QTMainWindow.closeEvent = self.app.utils.on_close
|
|
@ -4,7 +4,6 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from PyQt6.QtWidgets import QMessageBox
|
||||
from file import File
|
||||
|
||||
|
||||
class Utils:
|
||||
|
@ -33,7 +32,7 @@ class Utils:
|
|||
"https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor"
|
||||
)
|
||||
|
||||
def unsaved_changes_popup(self):
|
||||
def unsaved_changes_popup(self): # show popup and simplify return values
|
||||
button = self.usc_popup.exec()
|
||||
|
||||
match button:
|
||||
|
@ -69,14 +68,14 @@ class Utils:
|
|||
def on_close(self, event):
|
||||
changes = False
|
||||
|
||||
for file_path in self.app.open_files:
|
||||
for file_path in self.app.open_files: # check for files that have unsaved changes
|
||||
file = self.app.open_files[file_path]
|
||||
|
||||
if file.bit_editor.not_saved:
|
||||
changes = True
|
||||
break
|
||||
|
||||
if changes:
|
||||
if changes: # show a popup that informs the user that there are unsaved changes and ask them what to do
|
||||
save_or_not = self.unsaved_changes_popup()
|
||||
|
||||
match save_or_not:
|
||||
|
@ -94,8 +93,7 @@ class Utils:
|
|||
self.close()
|
||||
|
||||
def close(self):
|
||||
print("Bye!")
|
||||
|
||||
# get paths of open files and save them to the settings to reopen them automatically on next start
|
||||
file_keys = self.app.open_files.keys()
|
||||
open_files = []
|
||||
|
||||
|
@ -106,7 +104,10 @@ class Utils:
|
|||
|
||||
self.app.settings.save(f"{self.editor_path}/settings.json")
|
||||
|
||||
print("Bye!")
|
||||
|
||||
def update_style_in_all_bit_editors(self):
|
||||
# update the highlighting and character spacing when a setting has changed
|
||||
self.app.settings.highlight_ones = self.app.gui.main_window.highlightOnesSetting.isChecked()
|
||||
self.app.settings.square_bits = self.app.gui.main_window.bitsAreSquaresSetting.isChecked()
|
||||
|
||||
|
@ -116,23 +117,18 @@ class Utils:
|
|||
editor.update_style()
|
||||
|
||||
def on_start(self):
|
||||
# either open the lastly opened files or open the files specified by the parameters
|
||||
if len(sys.argv) == 1: # if no parameters were passed to the editor
|
||||
self.load_files(self.app.settings.last_opened_files)
|
||||
file_paths = self.app.settings.last_opened_files
|
||||
|
||||
else:
|
||||
file_paths = sys.argv[1:]
|
||||
|
||||
self.load_files(file_paths)
|
||||
|
||||
def load_files(self, file_paths):
|
||||
for file_path in file_paths:
|
||||
if not file_path in self.app.open_files and os.path.isfile(file_path):
|
||||
file = File(self.app, file_path, file_path.split("/")[-1])
|
||||
self.app.open_files[file_path] = file
|
||||
self.app.file_actions.open_multiple_files(file_paths)
|
||||
|
||||
def check_file_queue(self):
|
||||
if not len(self.app.open_files_queue) == 0:
|
||||
if not self.app.open_files_queue == []: # check for file paths in the queue that the ipc server put there
|
||||
filenames = self.app.open_files_queue
|
||||
self.app.open_files_queue = []
|
||||
|
||||
self.load_files(filenames)
|
||||
self.app.file_actions.open_multiple_files(filenames)
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
def connect_gui(app):
|
||||
app.gui.main_window.openFile.triggered.connect(app.file_actions.open_files)
|
||||
app.gui.main_window.newFile.triggered.connect(app.file_actions.create_file)
|
||||
app.gui.main_window.saveFile.triggered.connect(app.file_actions.save_current_file)
|
||||
app.gui.main_window.saveFileAs.triggered.connect(app.file_actions.save_current_file_as)
|
||||
|
||||
app.gui.main_window.menuSettings.triggered.connect(app.gui.main_window.settingsDock.show)
|
||||
|
||||
app.gui.main_window.bitsAreSquaresSetting.stateChanged.connect(app.utils.update_style_in_all_bit_editors)
|
||||
app.gui.main_window.highlightOnesSetting.stateChanged.connect(app.utils.update_style_in_all_bit_editors)
|
||||
|
||||
app.gui.main_window.openFileTabs.tabCloseRequested.connect(app.file_actions.close_current_file)
|
||||
app.gui.QTMainWindow.closeEvent = app.utils.on_close
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
|
||||
working_dir = os.getcwd()
|
||||
|
||||
if not working_dir.split('/')[-1] == "raw_ui": # cd into the right directory if this gets executed from somewhere else
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
input_debug = input("Do you want to debug the gui scripts? (Make gui scripts executable.) (y/n): ")
|
||||
|
||||
debug = input_debug == "y"
|
||||
|
||||
params = "-o"
|
||||
if debug:
|
||||
params = "-xo"
|
||||
|
||||
paths = {
|
||||
"main_window.ui": "main_window.py"
|
||||
}
|
||||
|
||||
for ui_file, script_out in paths.items():
|
||||
os.system(f"pyuic6 {ui_file} {params} ../{script_out}")
|
30
setup.py
Normal file
30
setup.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import setuptools
|
||||
from bread_editor.gui.raw_ui import ui_to_py
|
||||
from pathlib import Path
|
||||
|
||||
ui_to_py.convert_ui() # convert the .ui files to .py files before setup
|
||||
|
||||
this_directory = Path(__file__).parent # use readme file as long description
|
||||
long_description = (this_directory / "README.md").read_text()
|
||||
|
||||
setuptools.setup(
|
||||
name="Bread Editor",
|
||||
version="0.0",
|
||||
description="A binary editor with that you can edit single bits.",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor",
|
||||
author="The Wobbler",
|
||||
author_email="emil@i21k.de",
|
||||
packages=["bread_editor", "bread_editor.gui"],
|
||||
package_data={"": ["*.ui", "*.txt"]},
|
||||
install_requires=[
|
||||
"PyQt6",
|
||||
"wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@main#egg=wobbl_tools"
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": ["bread_editor=bread_editor.main:start_from_command_line"],
|
||||
}
|
||||
)
|
33
ui.py
33
ui.py
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow
|
||||
from PyQt6.QtGui import QFont
|
||||
from gui.main_window import Ui_MainWindow
|
||||
from connect_gui import connect_gui
|
||||
|
||||
|
||||
class GUI:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.qt_app = QApplication([])
|
||||
self.QTMainWindow = QMainWindow()
|
||||
|
||||
self.main_window = Ui_MainWindow()
|
||||
|
||||
self.setup_gui()
|
||||
|
||||
self.connect_gui = connect_gui
|
||||
|
||||
def setup_gui(self):
|
||||
self.main_window.setupUi(self.QTMainWindow)
|
||||
self.main_window.settingsDock.hide()
|
||||
|
||||
def post_setup(self):
|
||||
self.main_window.bitsAreSquaresSetting.blockSignals(True)
|
||||
self.main_window.bitsAreSquaresSetting.setChecked(self.app.settings.square_bits)
|
||||
self.main_window.bitsAreSquaresSetting.blockSignals(False)
|
||||
|
||||
self.main_window.highlightOnesSetting.setChecked(self.app.settings.highlight_ones)
|
||||
|
||||
self.app.utils.update_style_in_all_bit_editors()
|
Loading…
Reference in a new issue