Compare commits

..

1 commit
main ... main

Author SHA1 Message Date
megamichi
ada2d1c7cf http link in README 2024-12-08 20:22:28 +01:00
21 changed files with 148 additions and 230 deletions

View file

@ -7,31 +7,23 @@ So I just made one.
### Features
| 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 |
|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| 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 |
## Setup
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.\
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.\
You can do that using these commands:
```bash
git clone https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor.git
cd Bread_Editor
pip install .
pip install -r requirements.txt
chmod +x main.py
```
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`
Now you can execute the program using `./main.py`.
You can also create a desktop shortcut to the file.

30
binary_text_edit.py Normal file
View file

@ -0,0 +1,30 @@
#!/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()

View file

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

View file

@ -1,50 +0,0 @@
#!/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

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

View file

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

View file

@ -1,36 +0,0 @@
#!/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

@ -1,44 +0,0 @@
#!/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

15
connect_gui.py Normal file
View file

@ -0,0 +1,15 @@
#!/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

@ -2,8 +2,8 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout
from PyQt6.QtGui import QFont, QTextCharFormat, QColor
from bread_editor.binary_text_edit import BinaryTextEdit
from bread_editor.highlighting import Higlighter
from binary_text_edit import BinaryTextEdit
from highlighting import Higlighter
class BitEditor:
@ -55,11 +55,6 @@ 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
from bread_editor.editor import BitEditor
from PyQt6.QtWidgets import QFileDialog, QTabWidget
from editor import BitEditor
MAX_FILE_SIZE = 262144 # 2^18
@ -20,12 +20,15 @@ class FileActions:
dialog.setViewMode(QFileDialog.ViewMode.List)
if dialog.exec():
self.open_multiple_files(dialog.selectedFiles())
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
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
self.app.open_files[file_path] = File(self.app, file_path, file_path.split("/")[-1])
def create_file(self, content: bin=b""):
file_path, extension = QFileDialog.getSaveFileName(
caption="New File",
directory=self.app.utils.home_path,
@ -79,16 +82,6 @@ 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):

23
gui/raw_ui/ui_to_py.py Normal file
View file

@ -0,0 +1,23 @@
#!/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}")

View file

@ -3,11 +3,11 @@
import sys
from PyQt6.QtCore import QTimer
from wobbl_tools.data_file import load_dataclass_json
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
from utils import Utils
from file import File, FileActions
from ui import GUI
from settings import Settings
from ipc import IPC
class BreadEditor:
@ -24,6 +24,7 @@ class BreadEditor:
self.gui = GUI(self)
self.gui.connect_gui(self)
self.open_files: dict[str, File] = {}
self.open_files_queue = []
@ -37,19 +38,14 @@ class BreadEditor:
self.gui.post_setup()
self.utils.popup_init()
self.utils.on_start()
self.utils.popup_init()
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 bread_editor.utils import Utils
from utils import Utils
@dataclass

View file

@ -1,30 +0,0 @@
#!/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 Normal file
View file

@ -0,0 +1,33 @@
#!/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()

View file

@ -4,6 +4,7 @@ import os
import sys
from pathlib import Path
from PyQt6.QtWidgets import QMessageBox
from file import File
class Utils:
@ -32,7 +33,7 @@ class Utils:
"https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor"
)
def unsaved_changes_popup(self): # show popup and simplify return values
def unsaved_changes_popup(self):
button = self.usc_popup.exec()
match button:
@ -68,14 +69,14 @@ class Utils:
def on_close(self, event):
changes = False
for file_path in self.app.open_files: # check for files that have unsaved changes
for file_path in self.app.open_files:
file = self.app.open_files[file_path]
if file.bit_editor.not_saved:
changes = True
break
if changes: # show a popup that informs the user that there are unsaved changes and ask them what to do
if changes:
save_or_not = self.unsaved_changes_popup()
match save_or_not:
@ -93,7 +94,8 @@ class Utils:
self.close()
def close(self):
# get paths of open files and save them to the settings to reopen them automatically on next start
print("Bye!")
file_keys = self.app.open_files.keys()
open_files = []
@ -104,10 +106,7 @@ 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()
@ -117,18 +116,23 @@ 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
file_paths = self.app.settings.last_opened_files
self.load_files(self.app.settings.last_opened_files)
else:
file_paths = sys.argv[1:]
self.app.file_actions.open_multiple_files(file_paths)
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
def check_file_queue(self):
if not self.app.open_files_queue == []: # check for file paths in the queue that the ipc server put there
if not len(self.app.open_files_queue) == 0:
filenames = self.app.open_files_queue
self.app.open_files_queue = []
self.app.file_actions.open_multiple_files(filenames)
self.load_files(filenames)