Compare commits

...

17 commits
main ... main

Author SHA1 Message Date
436cb3018c Made the cursor also be a square when square bits is enabled. 2024-12-15 16:07:47 +01:00
2cb7b80cb8 Moved function connect_gui() from own file to GUI class for better OOP and less complicated file structure. 2024-12-15 15:43:09 +01:00
e863815b41 Typo in README
schon wieder n problem 😅
2024-12-14 23:42:33 +01:00
9a3789a99b Included necessary libraries in the README.md 2024-12-14 22:15:34 +01:00
d3b3dbc754 Fixed some bugs in the ui conversion script. 2024-12-14 22:01:41 +01:00
5cf5885d4b Set default parameter for int.to_bytes() so that the editor is compatible with Python<3.12
The parameter is only standard since Python3.12.
2024-12-14 19:40:31 +01:00
fb7a160d00 Simplified installation instructions because a manual conversion of the ui files is no longer needed. 2024-12-14 19:23:30 +01:00
47ede1e689 setup.py now automatically executes the gui generation script. 2024-12-14 19:19:29 +01:00
ea8b7456cc Modified the README.md accordingly to the latest changes.
(Explained how to install the editor using pip)
2024-12-14 18:50:49 +01:00
b7a6ba567a Changed folder structure to match the python package standards more and created setup.py for an easy installation. 2024-12-14 18:41:42 +01:00
b1b442b23a Set new file default content to a single byte full of zeros so that the cursor doesn't leave replace mode. 2024-12-09 18:11:46 +01:00
b2afa06ee1 Creating new files now actually works.
Somehow PyQt passed something to the new_file()-function, and it crashed every time because the variable was the wrong type.
2024-12-09 17:49:25 +01:00
93408e9a29 Implemented deleting of bytes. 2024-12-09 17:26:28 +01:00
409213585a I think I fixed the formatting issue when writing new data. 2024-12-09 16:53:33 +01:00
46f49804a8 Implemented size check for files opened via command line, simplified some code and added some comments. 2024-12-09 16:24:15 +01:00
7235736309 Added instruction to convert the ui files to python files. 2024-12-09 14:41:14 +01:00
de64c08532 Added git http clone url to the README.md, as suggested by Megamichi. 2024-12-09 14:35:04 +01:00
21 changed files with 231 additions and 149 deletions

View file

@ -7,23 +7,31 @@ 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 |
|-----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| 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`

View file

@ -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
View file

@ -0,0 +1 @@
#!/usr/bin/python3

View 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()

View file

@ -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

View file

@ -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):

View file

@ -0,0 +1 @@
#!/usr/bin/python3

View file

@ -0,0 +1 @@
#!/usr/bin/python3

View 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"
}

View file

@ -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()

View file

@ -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
View 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

View file

@ -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)

View file

@ -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

View file

@ -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
View 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
View file

@ -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()