From 750e8840f073d5a0be420c001691fb7326b09b4e Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sun, 16 Nov 2025 14:43:29 -0600 Subject: [PATCH] Prepping editor more... --- assets/map/map.json | 2 +- tools/dusk/event.py | 18 ++++ tools/editortool/map/.editor | 3 + tools/editortool/map/chunkpanel.py | 66 +++++++----- tools/editortool/map/map.py | 36 +++---- tools/editortool/map/mapdata.py | 78 ++++++++++++++ tools/editortool/map/mapinfopanel.py | 39 ++++--- tools/editortool/map/toolbar.py | 39 +++++++ tools/editortool/maptool.py | 153 +++++++++------------------ tools/editortool/statusbar.py | 18 ++++ 10 files changed, 285 insertions(+), 167 deletions(-) create mode 100644 tools/dusk/event.py create mode 100644 tools/editortool/map/.editor create mode 100644 tools/editortool/map/mapdata.py create mode 100644 tools/editortool/map/toolbar.py create mode 100644 tools/editortool/statusbar.py diff --git a/assets/map/map.json b/assets/map/map.json index 3e9a96b..fa38343 100644 --- a/assets/map/map.json +++ b/assets/map/map.json @@ -1,3 +1,3 @@ { - "mapName": "Some Map" + "mapName": "Test" } \ No newline at end of file diff --git a/tools/dusk/event.py b/tools/dusk/event.py new file mode 100644 index 0000000..d98eed8 --- /dev/null +++ b/tools/dusk/event.py @@ -0,0 +1,18 @@ +class Event: + def __init__(self): + self._subscribers = [] + + def sub(self, callback): + """Subscribe a callback to the event.""" + if callback not in self._subscribers: + self._subscribers.append(callback) + + def unsub(self, callback): + """Unsubscribe a callback from the event.""" + if callback in self._subscribers: + self._subscribers.remove(callback) + + def invoke(self, *args, **kwargs): + """Invoke all subscribers with the given arguments.""" + for callback in self._subscribers: + callback(*args, **kwargs) diff --git a/tools/editortool/map/.editor b/tools/editortool/map/.editor new file mode 100644 index 0000000..1e1ac88 --- /dev/null +++ b/tools/editortool/map/.editor @@ -0,0 +1,3 @@ +{ + "lastFile": "/home/yourwishes/htdocs/dusk/assets/map/map.json" +} \ No newline at end of file diff --git a/tools/editortool/map/chunkpanel.py b/tools/editortool/map/chunkpanel.py index 3f57162..52b6163 100644 --- a/tools/editortool/map/chunkpanel.py +++ b/tools/editortool/map/chunkpanel.py @@ -1,36 +1,54 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QGridLayout +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QGridLayout, QTreeWidget, QTreeWidgetItem from editortool.map.map import map class ChunkPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) - self.chunk_info_label = QLabel("Tile Information") - layout.addWidget(self.chunk_info_label) - self.move_label = QLabel("Move Selection") - layout.addWidget(self.move_label) + self.chunkInfoLabel = QLabel("Tile Information") + layout.addWidget(self.chunkInfoLabel) grid = QGridLayout() - self.btn_up = QPushButton("U") - self.btn_n = QPushButton("N") - self.btn_down = QPushButton("D") - self.btn_w = QPushButton("W") - self.btn_s = QPushButton("S") - self.btn_e = QPushButton("E") + self.btnUp = QPushButton("U") + self.btnN = QPushButton("N") + self.btnDown = QPushButton("D") + self.btnW = QPushButton("W") + self.btnS = QPushButton("S") + self.btnE = QPushButton("E") # Arrange buttons: U N D on top row, W S E on bottom row - grid.addWidget(self.btn_up, 0, 0) - grid.addWidget(self.btn_n, 0, 1) - grid.addWidget(self.btn_down, 0, 2) - grid.addWidget(self.btn_w, 1, 0) - grid.addWidget(self.btn_s, 1, 1) - grid.addWidget(self.btn_e, 1, 2) + grid.addWidget(self.btnUp, 0, 0) + grid.addWidget(self.btnN, 0, 1) + grid.addWidget(self.btnDown, 0, 2) + grid.addWidget(self.btnW, 1, 0) + grid.addWidget(self.btnS, 1, 1) + grid.addWidget(self.btnE, 1, 2) layout.addLayout(grid) - layout.addStretch() - self.btn_n.clicked.connect(lambda: map.moveRelative(0, 1, 0)) - self.btn_s.clicked.connect(lambda: map.moveRelative(0, -1, 0)) - self.btn_e.clicked.connect(lambda: map.moveRelative(1, 0, 0)) - self.btn_w.clicked.connect(lambda: map.moveRelative(-1, 0, 0)) - self.btn_up.clicked.connect(lambda: map.moveRelative(0, 0, 1)) - self.btn_down.clicked.connect(lambda: map.moveRelative(0, 0, -1)) \ No newline at end of file + # Add expandable tree list + self.tree = QTreeWidget() + self.tree.setHeaderLabel("Chunks") + # Example tree items + parentItem = QTreeWidgetItem(self.tree, ["Chunk 1"]) + childItem1 = QTreeWidgetItem(parentItem, ["Tile A"]) + childItem2 = QTreeWidgetItem(parentItem, ["Tile B"]) + parentItem2 = QTreeWidgetItem(self.tree, ["Chunk 2"]) + QTreeWidgetItem(parentItem2, ["Tile C"]) + QTreeWidgetItem(parentItem2, ["Tile D"]) + self.tree.expandAll() # Expand by default, remove if you want collapsed + layout.addWidget(self.tree, 1) # Give tree stretch factor so it expands + + self.btnN.clicked.connect(lambda: map.moveRelative(0, 1, 0)) + self.btnS.clicked.connect(lambda: map.moveRelative(0, -1, 0)) + self.btnE.clicked.connect(lambda: map.moveRelative(1, 0, 0)) + self.btnW.clicked.connect(lambda: map.moveRelative(-1, 0, 0)) + self.btnUp.clicked.connect(lambda: map.moveRelative(0, 0, 1)) + self.btnDown.clicked.connect(lambda: map.moveRelative(0, 0, -1)) + + # Subscribe to parent's fileSaving event + if parent is not None and hasattr(parent, 'fileSaving'): + parent.fileSaving.subscribe(self.onFileSaving) + + def onFileSaving(self, objOut): + # Inject 'name' into the object + objOut['name'] = 'ChunkPanel' \ No newline at end of file diff --git a/tools/editortool/map/map.py b/tools/editortool/map/map.py index ed72e29..6d86334 100644 --- a/tools/editortool/map/map.py +++ b/tools/editortool/map/map.py @@ -1,36 +1,24 @@ +from dusk.event import Event + +MAP_WIDTH = 5 +MAP_HEIGHT = 5 +MAP_DEPTH = 3 + class Map: def __init__(self): - self._position = [0, 0, 0] # x, y, z - self.listeners = [] + self.position = [0, 0, 0] # x, y, z + self.positionEvent = Event() self.chunks = [] - self.width = 5 - self.height = 5 - self.depth = 3 - - @property - def position(self): - return self._position - - @position.setter - def position(self, value): - self._position = value - self.notifyPositionListeners() - - def addPositionListener(self, listener): - self.listeners.append(listener) - - def notifyPositionListeners(self): - for listener in self.listeners: - listener(self._position) def moveTo(self, x, y, z): self.position = [x, y, z] + self.positionEvent.invoke(self.position) def moveRelative(self, x, y, z): self.moveTo( - self._position[0] + x, - self._position[1] + y, - self._position[2] + z + self.position[0] + x, + self.position[1] + y, + self.position[2] + z ) # Singleton instance diff --git a/tools/editortool/map/mapdata.py b/tools/editortool/map/mapdata.py new file mode 100644 index 0000000..40be67f --- /dev/null +++ b/tools/editortool/map/mapdata.py @@ -0,0 +1,78 @@ +import json +from dusk.event import Event +from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtCore import QTimer +import os + +MAP_DEFAULT_PATH = os.path.join(os.path.dirname(__file__), '../../../assets/map/') +EDITOR_CONFIG_PATH = os.path.join(os.path.dirname(__file__), '.editor') + +class MapData: + def __init__(self): + self.data = {} + self.dataOriginal = {} + self.onMapData = Event() + self.mapFileName = None + + QTimer.singleShot(16, self.loadLastFile) + + def loadLastFile(self): + if os.path.exists(EDITOR_CONFIG_PATH): + try: + with open(EDITOR_CONFIG_PATH, 'r') as f: + config = json.load(f) + lastFile = config.get('lastFile') + if lastFile and os.path.exists(lastFile): + self.load(lastFile) + except Exception: + pass + + def updateEditorConfig(self): + try: + config = {'lastFile': self.mapFileName if self.mapFileName else ""} + with open(EDITOR_CONFIG_PATH, 'w') as f: + json.dump(config, f, indent=2) + except Exception: + pass + + def newFile(self): + self.data = {} + self.dataOriginal = {} + self.mapFileName = None + self.onMapData.invoke(self.data) + self.updateEditorConfig() + + def save(self, fname=None): + if not self.mapFileName and fname is None: + filePath, _ = QFileDialog.getSaveFileName(None, "Save Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") + if not filePath: + return + self.mapFileName = filePath + if fname: + self.mapFileName = fname + if not self.isMapFileDirty(): + return + try: + with open(self.mapFileName, 'w') as f: + json.dump(self.data, f, indent=2) + self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy + self.updateEditorConfig() + except Exception as e: + QMessageBox.critical(None, "Save Error", f"Failed to save map file:\n{e}") + + def load(self, fileName): + try: + with open(fileName, 'r') as f: + self.data = json.load(f) + self.mapFileName = fileName + self.dataOriginal = json.loads(json.dumps(self.data)) # Deep copy + self.onMapData.invoke(self.data) + self.updateEditorConfig() + except Exception as e: + QMessageBox.critical(None, "Load Error", f"Failed to load map file:\n{e}") + + def isMapFileDirty(self): + return json.dumps(self.data, sort_keys=True) != json.dumps(self.dataOriginal, sort_keys=True) + + def isDirty(self): + return self.isMapFileDirty() \ No newline at end of file diff --git a/tools/editortool/map/mapinfopanel.py b/tools/editortool/map/mapinfopanel.py index 5ad8fa4..67d1fc8 100644 --- a/tools/editortool/map/mapinfopanel.py +++ b/tools/editortool/map/mapinfopanel.py @@ -2,22 +2,23 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton from editortool.map.map import map from dusk.defs import defs -CHUNK_WIDTH = int(defs.get('CHUNK_WIDTH')) -CHUNK_HEIGHT = int(defs.get('CHUNK_HEIGHT')) -CHUNK_DEPTH = int(defs.get('CHUNK_DEPTH')) +chunkWidth = int(defs.get('CHUNK_WIDTH')) +chunkHeight = int(defs.get('CHUNK_HEIGHT')) +chunkDepth = int(defs.get('CHUNK_DEPTH')) class MapInfoPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) - layout = QVBoxLayout() - map_info_label = QLabel("Map Information") - layout.addWidget(map_info_label) + self.parent = parent - map_title_label = QLabel("Map Title") - self.map_title_input = QLineEdit() - layout.addWidget(map_title_label) - layout.addWidget(self.map_title_input) + # Components + layout = QVBoxLayout() + + mapTitleLabel = QLabel("Map Title") + self.mapTitleInput = QLineEdit() + layout.addWidget(mapTitleLabel) + layout.addWidget(self.mapTitleInput) tilePositionLabel = QLabel("Tile Position") layout.addWidget(tilePositionLabel) @@ -43,13 +44,18 @@ class MapInfoPanel(QWidget): layout.addStretch() self.setLayout(layout) + # Events self.tileGo.clicked.connect(self.goToPosition) self.tileXInput.returnPressed.connect(self.goToPosition) self.tileYInput.returnPressed.connect(self.goToPosition) self.tileZInput.returnPressed.connect(self.goToPosition) + map.positionEvent.sub(self.updatePositionLabels) + parent.mapData.onMapData.sub(self.onMapData) + self.mapTitleInput.textChanged.connect(self.onMapNameChange) + + # Initial label setting self.updatePositionLabels(map.position) - map.addPositionListener(self.updatePositionLabels) def goToPosition(self): try: @@ -64,5 +70,12 @@ class MapInfoPanel(QWidget): self.tileXInput.setText(str(pos[0])) self.tileYInput.setText(str(pos[1])) self.tileZInput.setText(str(pos[2])) - self.chunkPosLabel.setText(f"Chunk Position: {pos[0] % CHUNK_WIDTH}, {pos[1] % CHUNK_HEIGHT}, {pos[2] % CHUNK_DEPTH}") - self.chunkLabel.setText(f"Chunk: {pos[0] // CHUNK_WIDTH}, {pos[1] // CHUNK_HEIGHT}, {pos[2] // CHUNK_DEPTH}") \ No newline at end of file + self.chunkPosLabel.setText(f"Chunk Position: {pos[0] % chunkWidth}, {pos[1] % chunkHeight}, {pos[2] % chunkDepth}") + self.chunkLabel.setText(f"Chunk: {pos[0] // chunkWidth}, {pos[1] // chunkHeight}, {pos[2] // chunkDepth}") + + def onMapData(self, data): + self.updatePositionLabels(map.position) + self.mapTitleInput.setText(data.get("mapName", "")) + + def onMapNameChange(self, text): + self.parent.mapData.data['mapName'] = text \ No newline at end of file diff --git a/tools/editortool/map/toolbar.py b/tools/editortool/map/toolbar.py new file mode 100644 index 0000000..b69fb58 --- /dev/null +++ b/tools/editortool/map/toolbar.py @@ -0,0 +1,39 @@ +import os +from PyQt5.QtWidgets import QAction, QMenuBar, QFileDialog +from editortool.map.mapdata import MAP_DEFAULT_PATH + +class MapToolbar: + def __init__(self, parent): + self.parent = parent + self.menubar = parent.menuBar() + self.fileMenu = self.menubar.addMenu("File") + + self.actionNew = QAction("New", parent) + self.actionOpen = QAction("Open", parent) + self.actionSave = QAction("Save", parent) + self.actionSaveAs = QAction("Save As", parent) + + self.actionNew.triggered.connect(self.newFile) + self.actionOpen.triggered.connect(self.openFile) + self.actionSave.triggered.connect(self.saveFile) + self.actionSaveAs.triggered.connect(self.saveAsFile) + self.fileMenu.addAction(self.actionNew) + self.fileMenu.addAction(self.actionOpen) + self.fileMenu.addAction(self.actionSave) + self.fileMenu.addAction(self.actionSaveAs) + + def newFile(self): + self.parent.mapData.newFile() + + def openFile(self): + filePath, _ = QFileDialog.getOpenFileName(self.menubar, "Open Map File", MAP_DEFAULT_PATH, "Map Files (*.json)") + if filePath: + self.parent.mapData.load(filePath) + + def saveFile(self): + self.parent.mapData.save() + + def saveAsFile(self): + filePath, _ = QFileDialog.getSaveFileName(self.menubar, "Save Map File As", MAP_DEFAULT_PATH, "Map Files (*.json)") + if filePath: + self.parent.mapData.save(filePath) \ No newline at end of file diff --git a/tools/editortool/maptool.py b/tools/editortool/maptool.py index 9207ae6..7ef2c4f 100644 --- a/tools/editortool/maptool.py +++ b/tools/editortool/maptool.py @@ -1,134 +1,77 @@ -import sys import os -import json -from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QWidget, - QHBoxLayout, QVBoxLayout, QPushButton, - QLabel, QSlider, QDialog, QRadioButton, QDialogButtonBox, - QAction, QFileDialog, QLineEdit, QMessageBox -) +from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QMessageBox from editortool.map.glwidget import GLWidget from editortool.map.chunkpanel import ChunkPanel from editortool.map.mapinfopanel import MapInfoPanel +from editortool.map.mapdata import MapData +from editortool.map.toolbar import MapToolbar +from editortool.statusbar import StatusBar + class MapWindow(QMainWindow): def __init__(self): super().__init__() + # Subclasses + self.mapData = MapData() + + # Window setup self.setWindowTitle("Dusk Map Editor") self.resize(1280, 720) - # File menu setup - menubar = self.menuBar() - file_menu = menubar.addMenu("File") - - action_new = QAction("New", self) - action_open = QAction("Open", self) - action_save = QAction("Save", self) - action_save_as = QAction("Save As", self) - - file_menu.addAction(action_new) - file_menu.addAction(action_open) - file_menu.addAction(action_save) - file_menu.addAction(action_save_as) - - action_new.triggered.connect(self.newFile) - action_open.triggered.connect(self.openFile) - action_save.triggered.connect(self.saveFile) - action_save_as.triggered.connect(self.saveFileAs) - - self.current_file = None - self.dirty = False - self.highlighted_pos = [0, 0, 0] # x, y, z + # Components + self.toolbar = MapToolbar(self) central = QWidget() self.setCentralWidget(central) - main_layout = QHBoxLayout(central) + mainLayout = QHBoxLayout(central) # Left panel (ChunkPanel) - self.chunk_panel = ChunkPanel() - left_widget = QWidget() - left_widget.setLayout(self.chunk_panel.layout()) - left_widget.setFixedWidth(200) - main_layout.addWidget(left_widget) + self.chunkPanel = ChunkPanel() + leftWidget = QWidget() + leftWidget.setLayout(self.chunkPanel.layout()) + leftWidget.setFixedWidth(200) + mainLayout.addWidget(leftWidget) # Center panel (GLWidget + controls) - self.gl_widget = GLWidget(self) - main_layout.addWidget(self.gl_widget, stretch=3) + self.glWidget = GLWidget(self) + mainLayout.addWidget(self.glWidget, stretch=3) # Right panel (MapInfoPanel) - self.map_info_panel = MapInfoPanel() - right_widget = self.map_info_panel - right_widget.setFixedWidth(250) - main_layout.addWidget(right_widget) + self.mapInfoPanel = MapInfoPanel(self) + rightWidget = self.mapInfoPanel + rightWidget.setFixedWidth(250) + mainLayout.addWidget(rightWidget) - self.map_title_input = self.map_info_panel.map_title_input - self.map_title_input.textChanged.connect(self._on_map_title_changed) + # Status bar + self.statusBar = StatusBar(self) + self.setStatusBar(self.statusBar) - def _on_map_title_changed(self): - self.dirty = True - - def newFile(self): - self.current_file = None - self.map_title_input.setText("") - self.dirty = False - - def openFile(self): - default_dir = os.path.join(os.path.dirname(__file__), '../../assets/map/') - fname, _ = QFileDialog.getOpenFileName(self, "Open JSON File", default_dir, "JSON Files (*.json)") - if fname: - self.current_file = fname - try: - with open(fname, "r", encoding="utf-8") as f: - data = json.load(f) - self.map_title_input.setText(data.get("mapName", "")) - self.dirty = False - except Exception as e: - self.map_title_input.setText("") - QMessageBox.critical(self, "Error", f"Failed to load file:\n{e}") - - def saveFile(self): - if self.current_file: - self._save_to_file(self.current_file) - else: - self.saveFileAs() - - def saveFileAs(self): - default_dir = os.path.join(os.path.dirname(__file__), '../../assets/map/') - fname, _ = QFileDialog.getSaveFileName(self, "Save JSON File As", default_dir, "JSON Files (*.json)") - if fname: - self.current_file = fname - self._save_to_file(fname) - - def _save_to_file(self, fname): - data = { - "mapName": self.map_title_input.text() - } - try: - with open(fname, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - self.dirty = False - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to save file:\n{e}") + def generateData(self): + objOut = {} + self.fileSaving.invoke(objOut) + self.mapData.data = objOut + return objOut def closeEvent(self, event): - if self.dirty: - reply = QMessageBox.question( - self, - "Unsaved Changes", - "You have unsaved changes. Do you want to save before closing?", - QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, - QMessageBox.Save - ) - if reply == QMessageBox.Save: - self.saveFile() - if self.dirty: - event.ignore() - return - elif reply == QMessageBox.Cancel: + if not self.mapData.isDirty(): + event.accept() + return + + reply = QMessageBox.question( + self, + "Unsaved Changes", + "You have unsaved changes. Do you want to save before closing?", + QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel, + QMessageBox.Save + ) + if reply == QMessageBox.Save: + self.saveFile() + if self.mapData.isDirty(): event.ignore() return - # Discard: continue closing + elif reply == QMessageBox.Cancel: + event.ignore() + return event.accept() \ No newline at end of file diff --git a/tools/editortool/statusbar.py b/tools/editortool/statusbar.py new file mode 100644 index 0000000..7ed455f --- /dev/null +++ b/tools/editortool/statusbar.py @@ -0,0 +1,18 @@ +from PyQt5.QtWidgets import QStatusBar, QLabel + +class StatusBar(QStatusBar): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.leftLabel = QLabel("Ready") + self.rightLabel = QLabel("") + self.addWidget(self.leftLabel, 1) + self.addPermanentWidget(self.rightLabel) + + parent.mapData.onMapData.sub(self.onMapData) + + def setStatus(self, message): + self.leftLabel.setText(message) + + def onMapData(self, data): + self.rightLabel.setText(self.parent.mapData.mapFileName if self.parent.mapData.mapFileName else "Untitled.json") \ No newline at end of file