diff --git a/README.md b/README.md index 58d0203..3b2ced4 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,32 @@ So I just made one. ### Features -| Feature | Description | State | -|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| -| Editing of basic image formats /
"Syntax Highlighting" | If u have a very simple black/white image format
where single bits control the color,
the editor can highlight the enabled bits. | Implemented | +| Feature | Description | State | +|-----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| Editing of basic image formats /
"Syntax Highlighting" | If you have a very simple black/white image format
where single bits control the color,
the editor can highlight the enabled bits. | 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 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. \ No newline at end of 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` \ No newline at end of file diff --git a/binary_text_edit.py b/binary_text_edit.py deleted file mode 100644 index 39d40b3..0000000 --- a/binary_text_edit.py +++ /dev/null @@ -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() diff --git a/bread_editor/__init__.py b/bread_editor/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/bread_editor/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/bread_editor/binary_text_edit.py b/bread_editor/binary_text_edit.py new file mode 100644 index 0000000..90ef344 --- /dev/null +++ b/bread_editor/binary_text_edit.py @@ -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() diff --git a/editor.py b/bread_editor/editor.py similarity index 83% rename from editor.py rename to bread_editor/editor.py index 62e56b7..64d99f9 100644 --- a/editor.py +++ b/bread_editor/editor.py @@ -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 diff --git a/example.txt b/bread_editor/example.txt similarity index 100% rename from example.txt rename to bread_editor/example.txt diff --git a/file.py b/bread_editor/file.py similarity index 80% rename from file.py rename to bread_editor/file.py index e2a881e..0802f5d 100644 --- a/file.py +++ b/bread_editor/file.py @@ -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): diff --git a/bread_editor/gui/__init__.py b/bread_editor/gui/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/bread_editor/gui/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/bread_editor/gui/raw_ui/__init__.py b/bread_editor/gui/raw_ui/__init__.py new file mode 100644 index 0000000..a93a4bf --- /dev/null +++ b/bread_editor/gui/raw_ui/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python3 diff --git a/gui/raw_ui/main_window.ui b/bread_editor/gui/raw_ui/main_window.ui similarity index 100% rename from gui/raw_ui/main_window.ui rename to bread_editor/gui/raw_ui/main_window.ui diff --git a/bread_editor/gui/raw_ui/ui_to_py.py b/bread_editor/gui/raw_ui/ui_to_py.py new file mode 100644 index 0000000..abf5012 --- /dev/null +++ b/bread_editor/gui/raw_ui/ui_to_py.py @@ -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" +} diff --git a/highlighting.py b/bread_editor/highlighting.py similarity index 100% rename from highlighting.py rename to bread_editor/highlighting.py diff --git a/ipc.py b/bread_editor/ipc.py similarity index 100% rename from ipc.py rename to bread_editor/ipc.py diff --git a/main.py b/bread_editor/main.py similarity index 79% rename from main.py rename to bread_editor/main.py index 52e3d64..fc11e69 100755 --- a/main.py +++ b/bread_editor/main.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() diff --git a/settings.py b/bread_editor/settings.py similarity index 87% rename from settings.py rename to bread_editor/settings.py index ba339d0..1fa47e1 100644 --- a/settings.py +++ b/bread_editor/settings.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from typing import List -from utils import Utils +from bread_editor.utils import Utils @dataclass diff --git a/bread_editor/ui.py b/bread_editor/ui.py new file mode 100644 index 0000000..a866707 --- /dev/null +++ b/bread_editor/ui.py @@ -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 diff --git a/utils.py b/bread_editor/utils.py similarity index 81% rename from utils.py rename to bread_editor/utils.py index 0e7c8bd..b655727 100644 --- a/utils.py +++ b/bread_editor/utils.py @@ -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) diff --git a/connect_gui.py b/connect_gui.py deleted file mode 100644 index 086f55d..0000000 --- a/connect_gui.py +++ /dev/null @@ -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 diff --git a/gui/raw_ui/ui_to_py.py b/gui/raw_ui/ui_to_py.py deleted file mode 100644 index ac953c1..0000000 --- a/gui/raw_ui/ui_to_py.py +++ /dev/null @@ -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}") diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bf7f6c7 --- /dev/null +++ b/setup.py @@ -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"], + } +) diff --git a/ui.py b/ui.py deleted file mode 100644 index 8bda743..0000000 --- a/ui.py +++ /dev/null @@ -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()