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