forked from Wobbl/Bread_Editor
Changed folder structure to match the python package standards more and created setup.py for an easy installation.
This commit is contained in:
parent
b1b442b23a
commit
b7a6ba567a
17 changed files with 48 additions and 12 deletions
3
bread_editor/__init__.py
Normal file
3
bread_editor/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from bread_editor.main import start_from_command_line
|
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()
|
15
bread_editor/connect_gui.py
Normal file
15
bread_editor/connect_gui.py
Normal 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(lambda: 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
|
62
bread_editor/editor.py
Normal file
62
bread_editor/editor.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
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
|
||||
|
||||
|
||||
class BitEditor:
|
||||
font = QFont("Ubuntu Mono", 12)
|
||||
one_format = QTextCharFormat()
|
||||
one_format.setBackground(QColor(200, 150, 100))
|
||||
|
||||
def __init__(self, app, file):
|
||||
self.app = app
|
||||
self.file = file
|
||||
|
||||
self.not_saved = False
|
||||
|
||||
self.setup_gui()
|
||||
|
||||
def setup_gui(self):
|
||||
# widget that contains the text input
|
||||
self.widget = QWidget(self.app.gui.main_window.openFileTabs, objectName=self.file.path)
|
||||
self.widget_layout = QVBoxLayout()
|
||||
|
||||
self.input = BinaryTextEdit()
|
||||
self.input.setOverwriteMode(True)
|
||||
self.widget_layout.addWidget(self.input)
|
||||
|
||||
self.input.setPlainText(self.app.utils.bstring_to_oz(self.file.content))
|
||||
|
||||
self.input.textChanged.connect(self.on_edit)
|
||||
|
||||
self.bit_highlighter = Higlighter()
|
||||
self.bit_highlighter.add_mapping("1", self.one_format)
|
||||
|
||||
self.update_style()
|
||||
|
||||
self.widget.setLayout(self.widget_layout)
|
||||
|
||||
self.tab_index = self.app.gui.main_window.openFileTabs.addTab( # add a tab for the file in the top files list
|
||||
self.widget,
|
||||
self.file.name
|
||||
)
|
||||
|
||||
def on_edit(self):
|
||||
self.not_saved = True
|
||||
|
||||
def update_style(self):
|
||||
square = self.app.settings.square_bits
|
||||
|
||||
spacing = 200 if square else 100 # add spacing when setting is checked
|
||||
|
||||
self.font.setLetterSpacing(QFont.SpacingType.PercentageSpacing, spacing)
|
||||
self.input.setFont(self.font)
|
||||
|
||||
highlight_ones = self.app.settings.highlight_ones
|
||||
|
||||
highlighter_document = self.input.document() if highlight_ones else None
|
||||
|
||||
self.bit_highlighter.setDocument(highlighter_document)
|
1
bread_editor/example.txt
Normal file
1
bread_editor/example.txt
Normal file
|
@ -0,0 +1 @@
|
|||
This is an example of how a text file looks like in binary!
|
122
bread_editor/file.py
Normal file
122
bread_editor/file.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/python3
|
||||
import os.path
|
||||
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
from bread_editor.editor import BitEditor
|
||||
|
||||
MAX_FILE_SIZE = 262144 # 2^18
|
||||
|
||||
|
||||
class FileActions:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def open_files(self):
|
||||
dialog = QFileDialog(self.app.gui.QTMainWindow)
|
||||
dialog.setWindowTitle("Open File")
|
||||
dialog.setDirectory(self.app.utils.home_path)
|
||||
dialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
|
||||
dialog.setNameFilters(["Binary (*.bin)", "Any (*)"])
|
||||
dialog.setViewMode(QFileDialog.ViewMode.List)
|
||||
|
||||
if dialog.exec():
|
||||
self.open_multiple_files(dialog.selectedFiles())
|
||||
|
||||
def create_file(self, content: bytes=(0).to_bytes(1)):
|
||||
# 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
|
||||
|
||||
file_path, extension = QFileDialog.getSaveFileName(
|
||||
caption="New File",
|
||||
directory=self.app.utils.home_path,
|
||||
filter="Binary (*.bin);;Any (*)",
|
||||
)
|
||||
|
||||
if file_path == "":
|
||||
return
|
||||
|
||||
if "Binary" in extension:
|
||||
file_path = file_path.split(".")[0] + ".bin" # make sure it has the right extension
|
||||
|
||||
file = open(file_path, "bw") # create new empty file
|
||||
file.write(content)
|
||||
file.close()
|
||||
|
||||
self.app.open_files[file_path] = File(self.app, file_path, file_path.split("/")[-1]) # open file
|
||||
|
||||
def save_current_file(self):
|
||||
current_tab = self.app.gui.main_window.openFileTabs.currentWidget()
|
||||
current_file_path = current_tab.objectName()
|
||||
|
||||
self.app.open_files[current_file_path].save()
|
||||
|
||||
def save_current_file_as(self):
|
||||
current_tab = self.app.gui.main_window.openFileTabs.currentWidget() # get currently open file
|
||||
current_file_path = current_tab.objectName()
|
||||
file = self.app.open_files[current_file_path]
|
||||
|
||||
oz_content = file.bit_editor.input.toPlainText() # convert user input to binary data
|
||||
file_content = self.app.utils.oz_string_to_bstring(oz_content)
|
||||
|
||||
self.create_file(file_content)
|
||||
|
||||
def close_current_file(self):
|
||||
current_file_path = self.app.gui.main_window.openFileTabs.currentWidget().objectName()
|
||||
|
||||
if self.app.open_files[current_file_path].bit_editor.not_saved:
|
||||
save_or_not = self.app.utils.unsaved_changes_popup()
|
||||
|
||||
match save_or_not:
|
||||
case "save":
|
||||
self.app.open_files[current_file_path].save()
|
||||
|
||||
case "cancel":
|
||||
return
|
||||
|
||||
self.app.open_files[current_file_path].close()
|
||||
|
||||
def save_all_files(self):
|
||||
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):
|
||||
self.app = app
|
||||
|
||||
self.path = path
|
||||
self.name = name
|
||||
|
||||
file = open(path, "rb")
|
||||
file_content = file.read()
|
||||
file.close()
|
||||
|
||||
self.content = file_content
|
||||
|
||||
self.bit_editor = BitEditor(self.app, self)
|
||||
|
||||
self.app.gui.main_window.openFileTabs.setCurrentIndex(self.app.gui.main_window.openFileTabs.count() - 1)
|
||||
|
||||
def close(self):
|
||||
self.app.gui.main_window.openFileTabs.removeTab(self.bit_editor.tab_index)
|
||||
del self.app.open_files[self.path]
|
||||
|
||||
def save(self):
|
||||
oz_string = self.bit_editor.input.toPlainText()
|
||||
data = self.app.utils.oz_string_to_bstring(oz_string)
|
||||
|
||||
file = open(self.path, "wb")
|
||||
file.write(data)
|
||||
file.close()
|
||||
|
||||
self.app.open_files[self.path].bit_editor.not_saved = False
|
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
|
234
bread_editor/gui/raw_ui/main_window.ui
Normal file
234
bread_editor/gui/raw_ui/main_window.ui
Normal file
|
@ -0,0 +1,234 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>861</width>
|
||||
<height>548</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Bread Editor</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="Europe"/>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTabWidget" name="openFileTabs">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>-1</number>
|
||||
</property>
|
||||
<property name="elideMode">
|
||||
<enum>Qt::ElideNone</enum>
|
||||
</property>
|
||||
<property name="tabsClosable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="movable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>861</width>
|
||||
<height>20</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="fileMenu">
|
||||
<property name="locale">
|
||||
<locale language="English" country="Europe"/>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="newFile"/>
|
||||
<addaction name="openFile"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="saveFile"/>
|
||||
<addaction name="saveFileAs"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuEdit">
|
||||
<property name="locale">
|
||||
<locale language="English" country="Europe"/>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Edit</string>
|
||||
</property>
|
||||
<addaction name="menuSettings"/>
|
||||
</widget>
|
||||
<addaction name="fileMenu"/>
|
||||
<addaction name="menuEdit"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="settingsDock">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>124</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="accessibleDescription">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="floating">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="features">
|
||||
<set>QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string> Settings</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>1</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="settingsDockContents">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="settingsTabs">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="appearanceSettingsTab">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="accessibleName">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="title">
|
||||
<string>Appearance</string>
|
||||
</attribute>
|
||||
<widget class="QCheckBox" name="highlightOnesSetting">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>111</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Highlight ones</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QCheckBox" name="bitsAreSquaresSetting">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>30</y>
|
||||
<width>119</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Bits are squares</string>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<action name="openFile">
|
||||
<property name="checkable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+O</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</action>
|
||||
<action name="saveFile">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="menuSettings">
|
||||
<property name="text">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionNew">
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpen">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="newFile">
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+N</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="saveFileAs">
|
||||
<property name="text">
|
||||
<string>Save As</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
23
bread_editor/gui/raw_ui/ui_to_py.py
Normal file
23
bread_editor/gui/raw_ui/ui_to_py.py
Normal 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}")
|
20
bread_editor/highlighting.py
Normal file
20
bread_editor/highlighting.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import re
|
||||
from PyQt6.QtGui import QSyntaxHighlighter
|
||||
|
||||
|
||||
class Higlighter(QSyntaxHighlighter):
|
||||
def __init__(self, parent=None):
|
||||
QSyntaxHighlighter.__init__(self, parent)
|
||||
|
||||
self.mappings = {}
|
||||
|
||||
def add_mapping(self, pattern, format):
|
||||
self.mappings[pattern] = format
|
||||
|
||||
def highlightBlock(self, text):
|
||||
for pattern, format in self.mappings.items():
|
||||
for match in re.finditer(pattern, text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, format)
|
82
bread_editor/ipc.py
Normal file
82
bread_editor/ipc.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import json
|
||||
import threading
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 58592
|
||||
|
||||
|
||||
class IPC:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.first_instance = not self.already_running()
|
||||
|
||||
self.server_thread = threading.Thread(target=self.server)
|
||||
self.server_thread.daemon = True
|
||||
|
||||
if self.first_instance:
|
||||
self.server_thread.start()
|
||||
|
||||
else:
|
||||
self.send_open_file_message(sys.argv[1:])
|
||||
|
||||
def already_running(self):
|
||||
try:
|
||||
with socket.create_connection((HOST, PORT), timeout=1):
|
||||
return True
|
||||
|
||||
except (ConnectionRefusedError, OSError):
|
||||
return False
|
||||
|
||||
def server(self):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
|
||||
server_socket.bind((HOST, PORT))
|
||||
server_socket.listen(1)
|
||||
|
||||
while True:
|
||||
conn, addr = server_socket.accept()
|
||||
|
||||
with conn:
|
||||
data = conn.recv(1024)
|
||||
if data:
|
||||
message = data.decode('utf-8')
|
||||
|
||||
self.evaluate_message(message)
|
||||
|
||||
def send(self, message):
|
||||
try:
|
||||
with socket.create_connection((HOST, PORT)) as client_socket:
|
||||
client_socket.sendall(message.encode('utf-8'))
|
||||
|
||||
except Exception as error:
|
||||
print("IPC-error:", error)
|
||||
|
||||
def evaluate_message(self, raw_message):
|
||||
data_dict = json.loads(raw_message)
|
||||
message = Message(data_dict["type"], data_dict["content"])
|
||||
|
||||
match message.type:
|
||||
case "open_files":
|
||||
self.app.open_files_queue += message.content
|
||||
|
||||
def send_open_file_message(self, filenames):
|
||||
message = Message("open_files", filenames)
|
||||
|
||||
self.send(message.get_message_data())
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, message_type: str, content):
|
||||
self.type = message_type
|
||||
self.content = content
|
||||
|
||||
def get_message_data(self):
|
||||
data_dict = self.__dict__
|
||||
data_dict = dict(filter(lambda pair: not callable(pair[1]), data_dict.items())) # filter out functions
|
||||
data = json.dumps(data_dict)
|
||||
|
||||
return data
|
56
bread_editor/main.py
Executable file
56
bread_editor/main.py
Executable file
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
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
|
||||
|
||||
|
||||
class BreadEditor:
|
||||
def __init__(self):
|
||||
|
||||
self.utils = Utils(self)
|
||||
self.settings = load_dataclass_json(Settings, f"{self.utils.editor_path}/settings.json")
|
||||
|
||||
self.file_actions = FileActions(self)
|
||||
|
||||
self.ipc = IPC(self)
|
||||
if not self.ipc.first_instance:
|
||||
return
|
||||
|
||||
self.gui = GUI(self)
|
||||
|
||||
self.gui.connect_gui(self)
|
||||
self.open_files: dict[str, File] = {}
|
||||
self.open_files_queue = []
|
||||
|
||||
self.files_queue_timer = QTimer(self.gui.QTMainWindow)
|
||||
self.files_queue_timer.timeout.connect(self.utils.check_file_queue)
|
||||
self.files_queue_timer.start(500)
|
||||
|
||||
def run(self):
|
||||
if not self.ipc.first_instance:
|
||||
return
|
||||
|
||||
self.gui.post_setup()
|
||||
|
||||
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()
|
12
bread_editor/settings.py
Normal file
12
bread_editor/settings.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from bread_editor.utils import Utils
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
last_opened_files: List=field(default_factory=lambda: [f"{Utils.editor_path}/example.txt"])
|
||||
highlight_ones: bool=False
|
||||
square_bits: bool=False
|
32
bread_editor/ui.py
Normal file
32
bread_editor/ui.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow
|
||||
from bread_editor.gui.main_window import Ui_MainWindow
|
||||
from bread_editor.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()
|
134
bread_editor/utils.py
Normal file
134
bread_editor/utils.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PyQt6.QtWidgets import QMessageBox
|
||||
|
||||
|
||||
class Utils:
|
||||
home_path = str(Path.home())
|
||||
editor_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def popup_init(self):
|
||||
self.usc_popup = QMessageBox() # create a popup window that notifies the user that we have unsaved changes
|
||||
self.usc_popup.setWindowTitle("Unsaved Changes!") # usc means unsaved changes
|
||||
self.usc_popup.setText("The file you are trying to close has unsaved changes.")
|
||||
self.usc_popup.setStandardButtons(
|
||||
QMessageBox.StandardButton.Save |
|
||||
QMessageBox.StandardButton.Discard |
|
||||
QMessageBox.StandardButton.Cancel
|
||||
)
|
||||
|
||||
self.ftb_popup = QMessageBox() # dialog that says that the file is too big
|
||||
self.ftb_popup.setWindowTitle("File Too Big!")
|
||||
self.ftb_popup.setText("The file you are trying to open is too big!")
|
||||
self.ftb_popup.setDetailedText(
|
||||
"I am way too lazy to make the editor capable of handling big files,\n"
|
||||
"but you could improve the editor, if this annoys you.\n"
|
||||
"https://teapot.informationsanarchistik.de/Wobbl/Bread_Editor"
|
||||
)
|
||||
|
||||
def unsaved_changes_popup(self): # show popup and simplify return values
|
||||
button = self.usc_popup.exec()
|
||||
|
||||
match button:
|
||||
case QMessageBox.StandardButton.Save:
|
||||
return "save"
|
||||
case QMessageBox.StandardButton.Discard:
|
||||
return "discard"
|
||||
case QMessageBox.StandardButton.Cancel:
|
||||
return "cancel"
|
||||
|
||||
def bstring_to_oz(self, data): # convert binary data to a string of ones and zeros (oz)
|
||||
oz_bytes = []
|
||||
|
||||
for byte in data:
|
||||
oz_bytes.append(format(byte, "08b"))
|
||||
|
||||
oz_string = " ".join(oz_bytes)
|
||||
|
||||
return oz_string
|
||||
|
||||
def oz_string_to_bstring(self, oz_string): # convert a string of zeroes and ones to a binary string
|
||||
oz_bytes = oz_string.split()
|
||||
|
||||
bytes_int = []
|
||||
|
||||
for byte in oz_bytes:
|
||||
bytes_int.append(int(byte, 2))
|
||||
|
||||
binary_string = bytes(bytes_int)
|
||||
|
||||
return binary_string
|
||||
|
||||
def on_close(self, event):
|
||||
changes = False
|
||||
|
||||
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: # 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:
|
||||
case "save":
|
||||
self.app.file_actions.save_all_files()
|
||||
self.close()
|
||||
|
||||
case "cancel":
|
||||
event.ignore()
|
||||
|
||||
case "discard":
|
||||
self.close()
|
||||
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
# 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 = []
|
||||
|
||||
for file_path in file_keys: # convert dict keys to strings
|
||||
open_files.append(str(file_path))
|
||||
|
||||
self.app.settings.last_opened_files = open_files
|
||||
|
||||
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()
|
||||
|
||||
for file_path in self.app.open_files:
|
||||
editor = self.app.open_files[file_path].bit_editor
|
||||
|
||||
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
|
||||
|
||||
else:
|
||||
file_paths = sys.argv[1:]
|
||||
|
||||
self.app.file_actions.open_multiple_files(file_paths)
|
||||
|
||||
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
|
||||
filenames = self.app.open_files_queue
|
||||
self.app.open_files_queue = []
|
||||
|
||||
self.app.file_actions.open_multiple_files(filenames)
|
Loading…
Add table
Add a link
Reference in a new issue