diff --git a/README.md b/README.md
index 3377cc0..4436d78 100644
--- a/README.md
+++ b/README.md
@@ -5,29 +5,64 @@
diff --git a/app.py b/app.py
index b95b0d3..6df4f49 100644
--- a/app.py
+++ b/app.py
@@ -4,11 +4,11 @@
# when the imposter is sus
# from configparser import ConfigParser
from gui.config_manager import ConfigManager
-from PySide6.QtCore import QTranslator
+from PySide6.QtCore import QTranslator, QProcess
from PySide6.QtGui import QFileOpenEvent
from gui.welcome import WelcomeWindow
from gui.newfile import NewFileDialog
-import os
+import os, shutil
class OpenPosterApplication(QtWidgets.QApplication):
def __init__(self, *args, **kwargs):
@@ -79,6 +79,10 @@ def event(self, event):
if action[0] == "open":
action_type = "open"
break
+ if action[0] == "reset":
+ process = QProcess()
+ process.startDetached(sys.executable, sys.argv)
+ sys.exit(0)
if action[0] == "new":
newdlg = NewFileDialog()
newdlg.exec()
diff --git a/assets/OpenPoster-macOS-Default-1024x1024@2x.png b/assets/OpenPoster-macOS-Default-1024x1024@2x.png
new file mode 100644
index 0000000..3776cd1
Binary files /dev/null and b/assets/OpenPoster-macOS-Default-1024x1024@2x.png differ
diff --git a/assets/OpenPoster.icon/Assets/Layer_1-removebg-preview.png b/assets/OpenPoster.icon/Assets/Layer_1-removebg-preview.png
new file mode 100644
index 0000000..f824549
Binary files /dev/null and b/assets/OpenPoster.icon/Assets/Layer_1-removebg-preview.png differ
diff --git a/assets/OpenPoster.icon/Assets/Layer_2-removebg-preview.png b/assets/OpenPoster.icon/Assets/Layer_2-removebg-preview.png
new file mode 100644
index 0000000..5414a7d
Binary files /dev/null and b/assets/OpenPoster.icon/Assets/Layer_2-removebg-preview.png differ
diff --git a/assets/OpenPoster.icon/icon.json b/assets/OpenPoster.icon/icon.json
new file mode 100644
index 0000000..54e2b59
--- /dev/null
+++ b/assets/OpenPoster.icon/icon.json
@@ -0,0 +1,48 @@
+{
+ "fill" : {
+ "solid" : "extended-gray:1.00000,1.00000"
+ },
+ "groups" : [
+ {
+ "layers" : [
+ {
+ "glass" : false,
+ "image-name" : "Layer_2-removebg-preview.png",
+ "name" : "Layer_2-removebg-preview",
+ "position" : {
+ "scale" : 2,
+ "translation-in-points" : [
+ 0,
+ 0
+ ]
+ }
+ },
+ {
+ "image-name" : "Layer_1-removebg-preview.png",
+ "name" : "Layer_1-removebg-preview",
+ "position" : {
+ "scale" : 2,
+ "translation-in-points" : [
+ 0,
+ 0
+ ]
+ }
+ }
+ ],
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : true,
+ "value" : 0.5
+ }
+ }
+ ],
+ "supported-platforms" : {
+ "circles" : [
+ "watchOS"
+ ],
+ "squares" : "shared"
+ }
+}
\ No newline at end of file
diff --git a/gui/_assets.py b/gui/_assets.py
index 404af23..a147574 100644
--- a/gui/_assets.py
+++ b/gui/_assets.py
@@ -7,6 +7,8 @@ def __init__(self):
self.cafilepath = ""
self.cachedImages = {}
self.missing_assets = set()
+ self.config_dir = os.path.join(os.path.expanduser("~"), ".openposter")
+ self.assets_cache_dir = os.path.join(self.config_dir, "assets-cache")
if hasattr(sys, '_MEIPASS'):
self.app_base_path = sys._MEIPASS
@@ -18,12 +20,27 @@ def loadImage(self, src_path):
return None
if src_path in self.cachedImages:
+ print(f"Found image in memory cache: {src_path}")
return self.cachedImages[src_path]
+ filename = os.path.basename(src_path)
+ cache_path = os.path.join(self.assets_cache_dir, filename)
+ if os.path.exists(cache_path):
+ print(f"Found image directly in assets cache: {cache_path}")
+ try:
+ img = QImage(cache_path)
+ if not img.isNull():
+ pixmap = QPixmap.fromImage(img)
+ self.cachedImages[src_path] = pixmap
+ return pixmap
+ except Exception as e:
+ print(f"Error loading cached image {cache_path}: {e}")
+
asset_path = self.findAssetPath(src_path)
-
+
if not asset_path or not os.path.exists(asset_path):
print(f"Could not find asset: {src_path}")
+ print(f"Checked in assets cache: {cache_path}")
if not hasattr(self, 'missing_assets'):
self.missing_assets = set()
@@ -48,7 +65,20 @@ def loadImage(self, src_path):
def findAssetPath(self, src_path):
if not src_path:
return None
-
+
+ if src_path.startswith("assets/"):
+ filename = os.path.basename(src_path)
+ cache_path = os.path.join(self.assets_cache_dir, filename)
+ if os.path.exists(cache_path):
+ print(f"Found asset in cache directory: {cache_path}")
+ return cache_path
+
+ filename = os.path.basename(src_path)
+ cache_path = os.path.join(self.assets_cache_dir, filename)
+ if os.path.exists(cache_path):
+ print(f"Found asset in cache by basename: {cache_path}")
+ return cache_path
+
if src_path.startswith("themes/") or src_path.startswith("icons/") or src_path.startswith("assets/"):
app_level_path = os.path.join(self.app_base_path, src_path)
if os.path.exists(app_level_path):
diff --git a/gui/_parse.py b/gui/_parse.py
index 9f59959..25d3c94 100644
--- a/gui/_parse.py
+++ b/gui/_parse.py
@@ -60,6 +60,11 @@ def parseColor(self, color_str) -> QColor:
if color_str.startswith("#"):
return QColor(color_str)
+ if " " in color_str and all(p.replace(".", "", 1).isdigit() for p in color_str.split()):
+ parts = color_str.split()
+ r = int(float(parts[0])); g = int(float(parts[1])); b = int(float(parts[2]))
+ a = float(parts[3]) if len(parts) == 4 else 1.0
+ return QColor(r, g, b, int(a * 255))
except:
pass
diff --git a/gui/custom_widgets.py b/gui/custom_widgets.py
index 2b08550..3ae4e51 100644
--- a/gui/custom_widgets.py
+++ b/gui/custom_widgets.py
@@ -1,6 +1,6 @@
from PySide6.QtCore import Qt, QPointF, QRectF, Signal, QObject, QTimer, QEvent
from PySide6.QtGui import QColor, QPen, QBrush, QTransform, QCursor, QPainterPath, QPainter
-from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QApplication, QStyleOptionGraphicsItem, QWidget, QGraphicsTextItem
+from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QApplication, QStyleOptionGraphicsItem, QWidget, QGraphicsTextItem, QGraphicsPixmapItem
import math
import platform
@@ -18,14 +18,18 @@ class HandleType:
class ResizeHandle(QGraphicsRectItem):
"""Visual handle for resizing operations"""
def __init__(self, handle_type, parent=None):
- super().__init__(parent)
+ super().__init__(-10, -10, 20, 20, parent)
self.handle_type = handle_type
- self.setRect(-4, -4, 8, 8)
- self.setBrush(QBrush(QColor(100, 150, 255)))
- self.setPen(QPen(QColor(50, 100, 200), 1))
+
+ self.visual_rect = QRectF(-4, -4, 8, 8)
+
+ self.setBrush(Qt.NoBrush)
+ self.setPen(Qt.NoPen)
+
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
self.setFlag(QGraphicsItem.ItemIsMovable, False)
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
+ self.setAcceptHoverEvents(True)
cursor_map = {
HandleType.TopLeft: Qt.SizeFDiagCursor,
@@ -38,7 +42,28 @@ def __init__(self, handle_type, parent=None):
HandleType.Left: Qt.SizeHorCursor,
HandleType.Rotation: Qt.PointingHandCursor
}
- self.setCursor(cursor_map.get(handle_type, Qt.ArrowCursor))
+ self.handle_cursor = QCursor(cursor_map.get(handle_type, Qt.ArrowCursor))
+
+ def paint(self, painter, option, widget):
+ painter.setBrush(QBrush(QColor(100, 150, 255)))
+ painter.setPen(QPen(QColor(50, 100, 200), 1))
+ painter.drawRect(self.visual_rect)
+
+ def hoverMoveEvent(self, event):
+ if self.visual_rect.contains(event.pos()):
+ self.setCursor(self.handle_cursor)
+ else:
+ self.unsetCursor()
+ super().hoverMoveEvent(event)
+
+ def hoverLeaveEvent(self, event):
+ self.unsetCursor()
+ super().hoverLeaveEvent(event)
+
+ def shape(self):
+ path = QPainterPath()
+ path.addRect(self.rect())
+ return path
class EditableGraphicsItem(QObject):
"""A wrapper class that adds editing capabilities to QGraphicsItems"""
@@ -243,15 +268,16 @@ def updateBoundingBox(self):
def startEdit(self, handle_type, handle_pos):
"""Start edit operation (resize or rotate)"""
- self.isEditing = True
self.currentHandle = handle_type
+ self.isEditing = True
self.initialClickPos = handle_pos
self.initialItemPos = self.item.pos()
- self.initialItemTransform = QTransform(self.item.transform())
self.initialBoundingRect = self.item.boundingRect()
+ self.initialItemTransform = QTransform(self.item.transform())
+ self.initialSceneTransform = self.item.sceneTransform()
+ self.initialRotation = self.getItemRotation()
if handle_type == HandleType.Rotation:
- self.initialRotation = self.getItemRotation()
center = self.item.mapToScene(self.initialBoundingRect.center())
dx = handle_pos.x() - center.x()
dy = handle_pos.y() - center.y()
@@ -266,10 +292,9 @@ def startEdit(self, handle_type, handle_pos):
self.original_path_at_drag_start = self.item.path()
def handleEditOperation(self, new_pos):
- """Handle move, resize, or rotate operations"""
- if not self.isEditing or not self.currentHandle:
+ if self.currentHandle is None:
return
-
+
if self.currentHandle == HandleType.Rotation:
self.handleRotation(new_pos)
else:
@@ -279,92 +304,82 @@ def handleEditOperation(self, new_pos):
self.itemChanged.emit(self.item)
def handleRotation(self, mouse_pos):
- """Handle rotation logic"""
- center = self.item.mapToScene(self.initialBoundingRect.center())
- dx = mouse_pos.x() - center.x()
- dy = mouse_pos.y() - center.y()
- current_angle_rad = math.atan2(dy, dx)
- current_angle_deg = math.degrees(current_angle_rad)
+ center_scene = self.item.mapToScene(self.item.boundingRect().center())
- rotation_delta = current_angle_deg - self.initialAngle
+ initial_vec = self.initialClickPos - center_scene
+ current_vec = mouse_pos - center_scene
- new_transform = QTransform(self.initialItemTransform)
- new_transform.translate(self.initialBoundingRect.center().x(), self.initialBoundingRect.center().y())
- new_transform.rotate(rotation_delta)
- new_transform.translate(-self.initialBoundingRect.center().x(), -self.initialBoundingRect.center().y())
+ angle_delta = math.degrees(math.atan2(current_vec.y(), current_vec.x())) - math.degrees(math.atan2(initial_vec.y(), initial_vec.x()))
- self.item.setTransform(new_transform)
-
- def handleResize(self, mouse_pos_scene):
-
+ transform = QTransform(self.initialItemTransform)
- inv_transform, _ = self.item.transform().inverted()
- mouse_pos_item = inv_transform.map(mouse_pos_scene)
-
- initial_click_item = inv_transform.map(self.initialClickPos)
+ rotation_center = self.item.boundingRect().center()
- new_rect = QRectF(self.originalRect)
- delta = mouse_pos_item - initial_click_item
+ transform.translate(rotation_center.x(), rotation_center.y())
+ transform.rotate(angle_delta)
+ transform.translate(-rotation_center.x(), -rotation_center.y())
- handle = self.currentHandle
+ self.item.setTransform(transform)
- if handle == HandleType.TopLeft:
- new_rect.setTopLeft(self.originalRect.topLeft() + delta)
- elif handle == HandleType.Top:
- new_rect.setTop(self.originalRect.top() + delta.y())
- elif handle == HandleType.TopRight:
- new_rect.setTopRight(self.originalRect.topRight() + delta)
- elif handle == HandleType.Right:
- new_rect.setRight(self.originalRect.right() + delta.x())
- elif handle == HandleType.BottomRight:
- new_rect.setBottomRight(self.originalRect.bottomRight() + delta)
- elif handle == HandleType.Bottom:
- new_rect.setBottom(self.originalRect.bottom() + delta.y())
- elif handle == HandleType.BottomLeft:
- new_rect.setBottomLeft(self.originalRect.bottomLeft() + delta)
- elif handle == HandleType.Left:
- new_rect.setLeft(self.originalRect.left() + delta.x())
+ def handleResize(self, mouse_pos_scene):
+ new_transform = QTransform(self.initialItemTransform)
- if new_rect.width() < 1:
- new_rect.setWidth(1)
- if new_rect.height() < 1:
- new_rect.setHeight(1)
-
- x_scale = new_rect.width() / self.originalRect.width()
- y_scale = new_rect.height() / self.originalRect.height()
+ inv_initial_transform, _ = self.initialSceneTransform.inverted()
+
+ initial_local_pos = inv_initial_transform.map(self.initialClickPos)
+ current_local_pos = inv_initial_transform.map(mouse_pos_scene)
+
+ delta = current_local_pos - initial_local_pos
+
+ scale_x = 1.0
+ scale_y = 1.0
+
+ current_w = self.initialBoundingRect.width()
+ current_h = self.initialBoundingRect.height()
+
+ if self.currentHandle in [HandleType.TopLeft, HandleType.Left, HandleType.BottomLeft]:
+ if current_w - delta.x() > 0:
+ scale_x = (current_w - delta.x()) / current_w
+ elif self.currentHandle in [HandleType.TopRight, HandleType.Right, HandleType.BottomRight]:
+ if current_w + delta.x() > 0:
+ scale_x = (current_w + delta.x()) / current_w
+
+ if self.currentHandle in [HandleType.TopLeft, HandleType.Top, HandleType.TopRight]:
+ if current_h - delta.y() > 0:
+ scale_y = (current_h - delta.y()) / current_h
+ elif self.currentHandle in [HandleType.BottomLeft, HandleType.Bottom, HandleType.BottomRight]:
+ if current_h + delta.y() > 0:
+ scale_y = (current_h + delta.y()) / current_h
+
+ anchor_point = QPointF()
+ if self.currentHandle == HandleType.TopLeft:
+ anchor_point = self.initialBoundingRect.bottomRight()
+ elif self.currentHandle == HandleType.Top:
+ anchor_point = QPointF(self.initialBoundingRect.center().x(), self.initialBoundingRect.bottom())
+ elif self.currentHandle == HandleType.TopRight:
+ anchor_point = self.initialBoundingRect.bottomLeft()
+ elif self.currentHandle == HandleType.Right:
+ anchor_point = QPointF(self.initialBoundingRect.left(), self.initialBoundingRect.center().y())
+ elif self.currentHandle == HandleType.BottomRight:
+ anchor_point = self.initialBoundingRect.topLeft()
+ elif self.currentHandle == HandleType.Bottom:
+ anchor_point = QPointF(self.initialBoundingRect.center().x(), self.initialBoundingRect.top())
+ elif self.currentHandle == HandleType.BottomLeft:
+ anchor_point = self.initialBoundingRect.topRight()
+ elif self.currentHandle == HandleType.Left:
+ anchor_point = QPointF(self.initialBoundingRect.right(), self.initialBoundingRect.center().y())
+
+ new_transform.translate(anchor_point.x(), anchor_point.y())
+ new_transform.scale(scale_x, scale_y)
+ new_transform.translate(-anchor_point.x(), -anchor_point.y())
- new_transform = QTransform(self.initialItemTransform)
-
- center = QPointF()
- if handle == HandleType.BottomRight:
- center = self.originalRect.topLeft()
- elif handle == HandleType.TopLeft:
- center = self.originalRect.bottomRight()
- elif handle == HandleType.TopRight:
- center = self.originalRect.bottomLeft()
- elif handle == HandleType.BottomLeft:
- center = self.originalRect.topRight()
- elif handle == HandleType.Top:
- center = self.originalRect.bottomLeft()
- elif handle == HandleType.Bottom:
- center = self.originalRect.topLeft()
- elif handle == HandleType.Left:
- center = self.originalRect.topRight()
- elif handle == HandleType.Right:
- center = self.originalRect.topLeft()
-
- new_transform.translate(center.x(), center.y())
- new_transform.scale(x_scale, y_scale)
- new_transform.translate(-center.x(), -center.y())
-
self.item.setTransform(new_transform)
def endEdit(self):
- if self.isEditing:
- self.isEditing = False
- self.currentHandle = None
- self.itemChanged.emit(self.item)
- self.editFinished.emit(self.item)
+ self.isEditing = False
+ self.currentHandle = None
+ self.itemChanged.emit(self.item)
+ self.editFinished.emit(self.item)
def getItemRotation(self):
transform = self.item.transform()
@@ -442,103 +457,78 @@ def makeItemEditable(self, item):
return self.editableItems[item]
def mousePressEvent(self, event):
- """Handle mouse press events for editing"""
- if not self.editMode:
- super().mousePressEvent(event)
+ handle = self.getHandleAt(event.scenePos())
+ if handle and self.currentEditableItem:
+ self.currentEditableItem.startEdit(handle.handle_type, event.scenePos())
+ self.isDragging = True
+ event.accept()
return
- item_at_pos = self.itemAt(event.scenePos(), QTransform())
-
- if item_at_pos and isinstance(item_at_pos, QGraphicsTextItem) and item_at_pos.data(0) and str(item_at_pos.data(0)).endswith("_name"):
- if item_at_pos.parentItem():
- item_at_pos = item_at_pos.parentItem()
+ super().mousePressEvent(event)
- if item_at_pos and isinstance(item_at_pos, ResizeHandle):
- parent_item = item_at_pos.parentItem()
- if parent_item in self.editableItems:
- editable_item = self.editableItems[parent_item]
- editable_item.startEdit(item_at_pos.handle_type, event.scenePos())
- self.currentEditableItem = editable_item
- self.isDragging = True
- event.accept()
- return
+ items_at_pos = self.items(event.scenePos())
+ top_item = next((item for item in items_at_pos if item.data(1) == "Layer" and item.flags() & QGraphicsItem.ItemIsSelectable), None)
- is_layer = item_at_pos and hasattr(item_at_pos, 'data') and item_at_pos.data(1) == "Layer"
- if is_layer:
- self.selectItem(item_at_pos)
- super().mousePressEvent(event)
- return
+ if top_item:
+ if not self.currentEditableItem or self.currentEditableItem.item != top_item:
+ self.selectItem(top_item)
+ elif not event.isAccepted():
+ self.deselectAllItems()
- self.deselectAllItems()
- super().mousePressEvent(event)
+ def getHandleAt(self, pos):
+ if not self.currentEditableItem:
+ return None
+ items_at_pos = self.items(pos)
+ for item in items_at_pos:
+ if isinstance(item, ResizeHandle):
+ return item
+ return None
def mouseMoveEvent(self, event):
- """Handle mouse move events for editing"""
- if self.editMode and self.isDragging and self.currentEditableItem:
- print(f"Dragging in scene: pos={event.scenePos()}, dragging={self.isDragging}")
+ if self.isDragging and self.currentEditableItem:
self.currentEditableItem.handleEditOperation(event.scenePos())
+ self.onItemChanged(self.currentEditableItem.item)
event.accept()
else:
- super(CheckerboardGraphicsScene, self).mouseMoveEvent(event)
-
- if self.editMode and self.currentEditableItem:
- self.currentEditableItem.updateBoundingBox()
+ super().mouseMoveEvent(event)
+ if self.currentEditableItem and event.buttons() & Qt.LeftButton:
+ self.currentEditableItem.itemChanged.emit(self.currentEditableItem.item)
def mouseReleaseEvent(self, event):
- """Handle mouse release events for editing"""
- if self.editMode and self.isDragging and self.currentEditableItem:
+ if self.isDragging and self.currentEditableItem:
self.currentEditableItem.endEdit()
self.isDragging = False
event.accept()
- print(f"Edit ended, isDragging={self.isDragging}")
else:
- super(CheckerboardGraphicsScene, self).mouseReleaseEvent(event)
-
- if self.editMode and self.currentEditableItem:
- self.currentEditableItem.updateBoundingBox()
-
+ super().mouseReleaseEvent(event)
+ if self.currentEditableItem:
+ self.currentEditableItem.editFinished.emit(self.currentEditableItem.item)
+
def selectItem(self, item):
- """Select an item for editing and show its bounding box."""
- if self.currentEditableItem and self.currentEditableItem.item == item:
+ if self.isItemDeleted(item):
return
- self.deselectAllItems()
-
- if not self.isItemDeleted(item):
- try:
- if item not in self.editableItems:
- self.makeItemEditable(item)
-
- editable_item = self.editableItems[item]
- editable_item.setupBoundingBox()
- self.currentEditableItem = editable_item
+ if self.currentEditableItem and self.currentEditableItem.item != item:
+ self.deselectAllItems()
+
+ if not self.currentEditableItem or self.currentEditableItem.item != item:
+ self.currentEditableItem = self.makeItemEditable(item)
+ if self.currentEditableItem:
+ self.currentEditableItem.item.setSelected(True)
+ self.currentEditableItem.setupBoundingBox()
layer_id = item.data(0)
if layer_id:
- self.itemSelectedOnCanvas.emit(str(layer_id))
- except (RuntimeError, AttributeError):
- if item in self.editableItems:
- del self.editableItems[item]
-
- def deselectAllItems(self):
- """Deselect all items"""
- items_to_cleanup = []
- for item, editable_item in self.editableItems.items():
- try:
- if not self.isItemDeleted(item):
- editable_item.removeBoundingBox()
- else:
- items_to_cleanup.append(item)
- except (RuntimeError, AttributeError):
- items_to_cleanup.append(item)
-
- for item in items_to_cleanup:
- if item in self.editableItems:
- del self.editableItems[item]
+ self.itemSelectedOnCanvas.emit(layer_id)
+ else:
+ self.deselectAllItems()
+ def deselectAllItems(self):
+ if self.currentEditableItem and not self.isItemDeleted(self.currentEditableItem.item):
+ self.currentEditableItem.removeBoundingBox()
+ self.currentEditableItem.item.setSelected(False)
self.currentEditableItem = None
-
+
def isItemDeleted(self, item):
- """Check if a QGraphicsItem has been deleted by Qt"""
try:
_ = item.pos()
return False
diff --git a/gui/discord_rpc.py b/gui/discord_rpc.py
new file mode 100644
index 0000000..4ee2043
--- /dev/null
+++ b/gui/discord_rpc.py
@@ -0,0 +1,89 @@
+from PySide6.QtCore import QThread, Signal, QObject
+from pypresence import Presence
+import time
+import platform
+
+class RPCWorker(QObject):
+ finished = Signal()
+ error = Signal(str)
+
+ def __init__(self, client_id):
+ super().__init__()
+ self.client_id = client_id
+ self.rpc = None
+ self.running = False
+
+ def run(self):
+ self.running = True
+ try:
+ self.rpc = Presence(self.client_id)
+ self.rpc.connect()
+
+ system = platform.system()
+ if system == "Darwin":
+ os_name = "macOS"
+ elif system == "Windows":
+ os_name = "Windows"
+ else:
+ os_name = "Linux"
+
+ self.rpc.update(
+ state="Editing .ca file",
+ details=f"Using OpenPoster on {os_name}",
+ start=time.time(),
+ large_image="openposter",
+ large_text="OpenPoster",
+ )
+
+ while self.running:
+ for _ in range(150):
+ if not self.running:
+ break
+ time.sleep(0.1)
+
+ except Exception as e:
+ self.error.emit(str(e))
+ finally:
+ if self.rpc:
+ self.rpc.close()
+ self.finished.emit()
+
+ def stop(self):
+ self.running = False
+
+class DiscordRPC:
+ def __init__(self, client_id="1392812529259909161"):
+ self.client_id = client_id
+ self.thread = None
+ self.worker = None
+
+ def start(self):
+ if self.is_running():
+ return
+
+ self.thread = QThread()
+ self.worker = RPCWorker(self.client_id)
+ self.worker.moveToThread(self.thread)
+
+ self.worker.error.connect(self.on_error)
+ self.thread.started.connect(self.worker.run)
+ self.worker.finished.connect(self.thread.quit)
+ self.worker.finished.connect(self.worker.deleteLater)
+ self.thread.finished.connect(self.thread.deleteLater)
+
+ self.thread.start()
+
+ def stop(self):
+ if self.worker:
+ self.worker.stop()
+ if self.thread and self.thread.isRunning():
+ self.thread.quit()
+ self.thread.wait()
+ self.thread = None
+ self.worker = None
+
+ def on_error(self, error_message):
+ print(f"Discord RPC Error: {error_message}")
+
+ def is_running(self):
+ return self.thread is not None and self.thread.isRunning()
\ No newline at end of file
diff --git a/gui/mainwindow.py b/gui/mainwindow.py
index b113bb8..0fb6bad 100644
--- a/gui/mainwindow.py
+++ b/gui/mainwindow.py
@@ -6,6 +6,7 @@
from PySide6.QtCore import Qt, QRectF, QPointF, QSize, QEvent, QVariantAnimation, QKeyCombination, QKeyCombination, QTimer, QSettings, QStandardPaths, QDir, QObject, QProcess, QByteArray, QBuffer, QIODevice, QXmlStreamReader, QPoint, QMimeData, QRegularExpression, QTranslator
from PySide6.QtGui import QPixmap, QImage, QBrush, QPen, QColor, QTransform, QPainter, QLinearGradient, QIcon, QPalette, QFont, QShortcut, QKeySequence, QAction, QCursor, QDesktopServices
from PySide6.QtWidgets import QFileDialog, QTreeWidgetItem, QMainWindow, QTableWidgetItem, QGraphicsRectItem, QGraphicsPixmapItem, QGraphicsTextItem, QApplication, QHeaderView, QPushButton, QHBoxLayout, QVBoxLayout, QLabel, QTreeWidget, QWidget, QGraphicsItemAnimation, QMessageBox, QDialog, QColorDialog, QProgressDialog, QSizePolicy, QSplitter, QFrame, QToolButton, QGraphicsView, QGraphicsScene, QStyleFactory, QSpacerItem, QMenu, QLineEdit, QTableWidget, QTableWidgetItem, QSystemTrayIcon, QGraphicsProxyWidget, QGraphicsDropShadowEffect, QMenu, QTreeWidgetItemIterator, QInputDialog, QSlider, QTextEdit
+from PySide6.QtWidgets import QSpinBox, QAbstractSpinBox
from ui.ui_mainwindow import Ui_OpenPoster
from .custom_widgets import CustomGraphicsView, CheckerboardGraphicsScene
import PySide6.QtCore as QtCore
@@ -14,6 +15,7 @@
import re
import subprocess
import tempfile, shutil
+import time
import resources_rc
from gui._meta import __version__
@@ -29,6 +31,7 @@
from .exportoptions_window import ExportOptionsDialog
from .theme_manager import ThemeManager
from .preview_renderer import PreviewRenderer
+from .discord_rpc import DiscordRPC
class MainWindow(QMainWindow):
def __init__(self, config_manager, translator):
@@ -63,6 +66,9 @@ def __init__(self, config_manager, translator):
self.preview = PreviewRenderer(self)
+ # Discord RPC
+ self.discord_rpc = DiscordRPC()
+
# Restore window geometry
self.loadWindowGeometry()
@@ -72,6 +78,25 @@ def __init__(self, config_manager, translator):
# print(f"OpenPoster v{__version__} started") # Commented out startup message
+ def retranslate_ui(self):
+ self.ui.retranslateUi(self)
+ self.ui.treeWidget.clear()
+ self.ui.statesTreeWidget.clear()
+ self.populateLayersTreeWidget()
+ self.populateStatesTreeWidget()
+
+ def load_language(self, lang_code):
+ if hasattr(QApplication.instance(), 'translator'):
+ QApplication.instance().removeTranslator(QApplication.instance().translator)
+
+ translator = QTranslator()
+ if translator.load(f"languages/app_{lang_code}.qm"):
+ QApplication.instance().installTranslator(translator)
+ QApplication.instance().translator = translator
+ self.translator = translator
+ else:
+ print(f"Failed to load translation for {lang_code}")
+
# app resources
def initAssetFinder(self):
if hasattr(sys, '_MEIPASS'):
@@ -100,6 +125,8 @@ def bindOperationFunctions(self):
self.applySpringAnimationToItem = self._applyAnimation.applySpringAnimationToItem
self._assets = Assets()
+ self._assets.config_dir = self.config_manager.config_dir
+ self._assets.assets_cache_dir = os.path.join(self._assets.config_dir, 'assets-cache')
self.findAssetPath = self._assets.findAssetPath
self.loadImage = self._assets.loadImage
@@ -215,35 +242,55 @@ def addlayer(self, **kwargs):
root_bounds = [float(b) for b in root_layer.bounds]
root_width = root_bounds[2]
root_height = root_bounds[3]
-
- new_height = root_height * 0.3
- if root_height > 0:
- aspect_ratio = root_width / root_height
- new_width = new_height * aspect_ratio
- else:
- new_width = root_width * 0.3
-
center_x = root_width / 2
center_y = root_height / 2
+
+ if layer_type != "text":
+ new_height = root_height * 0.3
+
+ if root_height > 0:
+ aspect_ratio = root_width / root_height
+ new_width = new_height * aspect_ratio
+ else:
+ new_width = root_width * 0.3
+
+ layer.bounds = ['0', '0', str(new_width), str(new_height)]
+ layer.scale_factor = 0.3
- layer.bounds = ['0', '0', str(new_width), str(new_height)]
layer.position = [str(center_x), str(center_y)]
- layer.scale_factor = 0.3
+
except (ValueError, IndexError) as e:
print(f"Warning: Could not get root layer bounds to resize new layer: {e}")
pass
+ parent_layer = None
+ selected_item = self.ui.treeWidget.currentItem()
+
+ if selected_item:
+ item_type = selected_item.text(1)
+ if item_type == "Layer" or item_type == "Root":
+ layer_id = selected_item.text(2)
+ if layer_id:
+ parent_layer = self.cafile.rootlayer.findlayer(layer_id)
+
+ if not parent_layer:
+ parent_layer = self.cafile.rootlayer
+
+ if not parent_layer:
+ self.create_themed_message_box(
+ QMessageBox.Critical,
+ "Error",
+ "Cannot add a layer. No valid parent layer could be determined."
+ ).exec()
+ return
+
if layer_type == "text":
text, ok = QInputDialog.getText(self, "New Text Layer", "Enter text:", QLineEdit.Normal, getattr(layer, "string", ""))
if not ok or not text:
return
layer.string = text
- if not hasattr(layer, "fontSize") or not layer.fontSize:
- root_height = float(root_layer.bounds[3])
- default_font_size = int(root_height * 0.05 * 2)
- layer.fontSize = str(default_font_size)
if not hasattr(layer, "fontFamily") or not layer.fontFamily:
layer.fontFamily = "Helvetica"
if not hasattr(layer, "alignmentMode") or not layer.alignmentMode:
@@ -251,6 +298,14 @@ def addlayer(self, **kwargs):
if not hasattr(layer, "color") or not layer.color:
layer.color = "255 255 255"
+ default_font_size = 24
+ if not hasattr(layer, "fontSize") or not layer.fontSize:
+ layer.fontSize = str(default_font_size)
+
+ text_width = len(text) * default_font_size * 0.6
+ text_height = default_font_size * 1.2
+ layer.bounds = ['0', '0', str(text_width), str(text_height)]
+ layer.layer_class = "CATextLayer"
elif layer_type == "image":
image_path, _ = QFileDialog.getOpenFileName(self, "Select Image File", "", "Image Files (*.png *.jpg *.jpeg *.bmp *.gif)")
if not image_path:
@@ -269,17 +324,38 @@ def addlayer(self, **kwargs):
new_width = current_height * aspect_ratio
layer.bounds[2] = str(new_width)
- except Exception as e:
- print(f"Could not resize image based on aspect ratio: {e}")
- layer.content.src = image_path
+ assets_cache_dir = os.path.join(self.config_manager.config_dir, 'assets-cache')
+ if not os.path.exists(assets_cache_dir):
+ os.makedirs(assets_cache_dir)
- if hasattr(self, "currentInspectObject"):
- element = self.currentInspectObject
- else:
- element = self.cafile.rootlayer
+ image_filename = os.path.basename(image_path)
+ dest_path = os.path.join(assets_cache_dir, image_filename)
+
+ shutil.copy2(image_path, dest_path)
+
+ layer.content.src = f"assets/{image_filename}"
+
+ if hasattr(self.cafile, 'assets'):
+ with open(image_path, 'rb') as f:
+ self.cafile.assets[image_filename] = f.read()
+
+ print(f"Copied image to cache: {dest_path} and set path to assets/{image_filename}")
- element.addlayer(layer)
+ self.cachedImages = {}
+ if hasattr(self._assets, 'cachedImages'):
+ self._assets.cachedImages = {}
+ except Exception as e:
+ print(f"Could not process image: {e}")
+
+ try:
+ image_filename = os.path.basename(image_path)
+ layer.content.src = f"assets/{image_filename}"
+ print(f"Error occurred but still using relative path: assets/{image_filename}")
+ except:
+ layer.content.src = image_path
+
+ parent_layer.addlayer(layer)
self.ui.treeWidget.clear()
self.populateLayersTreeWidget()
self.renderPreview(self.cafile.rootlayer)
@@ -332,14 +408,32 @@ def initUI(self):
self.ui.addButton.setMenu(self.add_menu_ui)
self.ui.addButton.setEnabled(True)
- self.ui.openFile.clicked.connect(self.openFile)
self.ui.treeWidget.currentItemChanged.connect(self.openInInspector)
+ self.ui.treeWidget.setColumnHidden(2, True)
self.ui.statesTreeWidget.currentItemChanged.connect(self.openStateInInspector)
self.ui.tableWidget.itemChanged.connect(self.onInspectorChanged)
- self.ui.tableWidget.verticalHeader().setDefaultSectionSize(self.ui.tableWidget.fontMetrics().height() * 2.5)
+ self.ui.tableWidget.verticalHeader().setDefaultSectionSize(self.ui.tableWidget.fontMetrics().height() * 2.3)
+ table_header = self.ui.tableWidget.horizontalHeader()
+ table_header.setFixedHeight(int(self.ui.tableWidget.fontMetrics().height() * 2.5))
+ for i in range(self.ui.tableWidget.columnCount()):
+ item = self.ui.tableWidget.horizontalHeaderItem(i)
+ if item:
+ item.setTextAlignment(Qt.AlignCenter)
+ tree_header = self.ui.treeWidget.header()
+ tree_header.setFixedHeight(int(self.ui.treeWidget.fontMetrics().height() * 2.3))
+ tree_header_item = self.ui.treeWidget.headerItem()
+ for i in range(self.ui.treeWidget.columnCount()):
+ tree_header_item.setTextAlignment(i, Qt.AlignCenter)
+ states_header = self.ui.statesTreeWidget.header()
+ states_header.setFixedHeight(int(self.ui.statesTreeWidget.fontMetrics().height() * 2.3))
+ states_header_item = self.ui.statesTreeWidget.headerItem()
+ for i in range(self.ui.statesTreeWidget.columnCount()):
+ states_header_item.setTextAlignment(i, Qt.AlignCenter)
self.ui.filename.mousePressEvent = self.toggleFilenameDisplay
self.showFullPath = True
+ self.show_inspector_placeholder()
+
self.scene = CheckerboardGraphicsScene()
# Initialize animation helper now that scene exists
from ._applyanimation import ApplyAnimation
@@ -448,6 +542,7 @@ def open_ca_file(self, path):
self.isDirty = False
self.ui.addButton.setEnabled(True)
self.updateFilenameDisplay()
+ self.discord_rpc.start()
except Exception as e:
self.create_themed_message_box(
QMessageBox.Critical,
@@ -512,16 +607,13 @@ def openInInspector(self, current: QTreeWidgetItem, _) -> None:
if hasattr(self.scene, 'currentEditableItem') and self.scene.currentEditableItem:
self.scene.currentEditableItem.removeBoundingBox()
self.scene.currentEditableItem = None
- self.ui.tableWidget.blockSignals(True)
- self.ui.tableWidget.setRowCount(0)
- self.ui.tableWidget.blockSignals(False)
- self.currentInspectObject = None
+ self.show_inspector_placeholder()
return
- self.ui.tableWidget.blockSignals(True)
- self.currentInspectObject = None
- self.currentSelectedItem = current
+ self.ui.tableWidget.horizontalHeader().setVisible(True)
+ self.ui.tableWidget.blockSignals(True)
self.ui.tableWidget.setRowCount(0)
+
row_index = 0
element_type = current.text(1)
@@ -897,6 +989,30 @@ def openInInspector(self, current: QTreeWidgetItem, _) -> None:
if hasattr(element, "tracking") and element.tracking:
self.add_inspector_row("TRACKING", self.formatFloat(element.tracking), row_index)
row_index += 1
+
+ if hasattr(element, "leading") and element.leading:
+ self.add_inspector_row("LEADING", self.formatFloat(element.leading), row_index)
+ row_index += 1
+
+ if hasattr(element, "verticalAlignmentMode") and element.verticalAlignmentMode:
+ self.add_inspector_row("VERTICAL ALIGNMENT", element.verticalAlignmentMode, row_index)
+ row_index += 1
+
+ if hasattr(element, "resizingMode") and element.resizingMode:
+ self.add_inspector_row("RESIZING MODE", element.resizingMode, row_index)
+ row_index += 1
+
+ if hasattr(element, "allowsEdgeAntialiasing"):
+ self.add_inspector_row("EDGE ANTIALIASING", "Yes" if element.allowsEdgeAntialiasing else "No", row_index)
+ row_index += 1
+
+ if hasattr(element, "allowsGroupOpacity"):
+ self.add_inspector_row("GROUP OPACITY", "Yes" if element.allowsGroupOpacity else "No", row_index)
+ row_index += 1
+
+ if hasattr(element, "classIfAvailable") and element.classIfAvailable:
+ self.add_inspector_row("CLASS IF AVAILABLE", element.classIfAvailable, row_index)
+ row_index += 1
relationships_properties = False
if (hasattr(element, "states") and element.states) or \
@@ -954,6 +1070,8 @@ def add_category_header(self, category_name, row_index):
self.ui.tableWidget.setItem(row_index, 0, header_item)
self.ui.tableWidget.setSpan(row_index, 0, 1, 2)
+ header_item.setTextAlignment(Qt.AlignCenter)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 2.3))
return row_index + 1
@@ -1004,11 +1122,22 @@ def apply_scale(value):
is_text_layer = hasattr(layer, "layer_class") and layer.layer_class == "CATextLayer"
if is_text_layer:
- base_font_size = root_height * 0.05
- new_font_size = base_font_size * scale_factor
-
if hasattr(layer, "fontSize"):
- layer.fontSize = str(int(new_font_size))
+ if hasattr(layer, "original_font_size"):
+ original_font_size = float(layer.original_font_size)
+ else:
+ original_font_size = float(layer.fontSize)
+ layer.original_font_size = original_font_size
+
+ new_font_size = original_font_size * scale_factor
+ layer.fontSize = str(int(max(1, new_font_size)))
+
+ if hasattr(layer, "string") and layer.string:
+ text = layer.string
+ text_width = len(text) * new_font_size * 0.6
+ text_height = new_font_size * 1.2
+ layer.bounds[2] = str(text_width)
+ layer.bounds[3] = str(text_height)
else:
target_height = root_height * scale_factor
@@ -1038,6 +1167,42 @@ def apply_scale(value):
slider_layout.addWidget(scale_label)
self.ui.tableWidget.setCellWidget(row_index, 1, slider_widget)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 3))
+ elif key == "OPACITY":
+ slider_widget = QWidget()
+ slider_layout = QHBoxLayout(slider_widget)
+ slider_layout.setContentsMargins(2, 2, 2, 2)
+ opacity_slider = QSlider(Qt.Horizontal)
+ opacity_slider.setRange(0, 100)
+ try:
+ current_percent = float(value_str)
+ except:
+ current_percent = 100.0
+ opacity_slider.setValue(int(current_percent))
+ opacity_slider.setTickPosition(QSlider.TicksBelow)
+ opacity_slider.setTickInterval(10)
+ opacity_label = QLabel(f"{int(current_percent)}%")
+ def update_opacity_label(v):
+ opacity_label.setText(f"{v}%")
+ update_timer_op = QTimer()
+ update_timer_op.setSingleShot(True)
+ update_timer_op.setInterval(50)
+ def apply_opacity(v):
+ op = v / 100.0
+ if hasattr(self, 'currentInspectObject') and self.currentInspectObject:
+ layer = self.currentInspectObject
+ layer.opacity = str(op)
+ if not update_timer_op.isActive():
+ update_timer_op.timeout.connect(lambda: self.renderPreview(self.cafile.rootlayer))
+ update_timer_op.start()
+ self.markDirty()
+ opacity_slider.valueChanged.connect(update_opacity_label)
+ opacity_slider.valueChanged.connect(lambda v: apply_opacity(v))
+ opacity_slider.sliderReleased.connect(lambda: self.renderPreview(self.cafile.rootlayer))
+ slider_layout.addWidget(opacity_slider)
+ slider_layout.addWidget(opacity_label)
+ self.ui.tableWidget.setCellWidget(row_index, 1, slider_widget)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 3))
elif isinstance(value, bool) or (isinstance(value_str, str) and value_str.lower() in ["yes", "no", "true", "false"]):
if isinstance(value, bool):
display_value = "Yes" if value else "No"
@@ -1050,6 +1215,56 @@ def apply_scale(value):
value_item = QTableWidgetItem(self.formatFloat(value) if isinstance(value, (float)) else str(value))
self.ui.tableWidget.setItem(row_index, 1, value_item)
+ elif key == "BACKGROUND COLOR" or key == "COLOR":
+ widget = QWidget()
+ layout = QHBoxLayout(widget)
+ layout.setContentsMargins(2, 2, 2, 2)
+ line_edit = QLineEdit(value_str)
+ button = QPushButton()
+ button.setFixedSize(20, 20)
+
+ def update_button(col_str):
+ col = self.parseColor(col_str)
+ if col and isinstance(col, QColor):
+ if col.alpha() != 255:
+ css_col = f"rgba({col.red()},{col.green()},{col.blue()},{col.alpha()/255:.2f})"
+ else:
+ css_col = col.name()
+ else:
+ css_col = ''
+ button.setStyleSheet(f"background-color: {css_col}; border: 1px solid black;")
+
+ update_button(value_str)
+
+ def apply_color_change(col_str):
+ if hasattr(self, 'currentInspectObject'):
+ layer = self.currentInspectObject
+ if key == "BACKGROUND COLOR":
+ layer.backgroundColor = col_str
+ elif key == "COLOR":
+ layer.color = col_str
+ update_button(col_str)
+ self.renderPreview(self.cafile.rootlayer)
+ self.markDirty()
+
+ def on_text_changed():
+ apply_color_change(line_edit.text())
+
+ def pick_color():
+ initial = self.parseColor(line_edit.text())
+ color = QColorDialog.getColor(initial, self, "Select Color")
+ if color.isValid():
+ col_str = color.name() if color.alpha() == 255 else f"rgba({color.red()},{color.green()},{color.blue()},{color.alpha()/255:.2f})"
+ line_edit.setText(col_str)
+ apply_color_change(col_str)
+
+ button.clicked.connect(pick_color)
+ line_edit.editingFinished.connect(on_text_changed)
+
+ layout.addWidget(line_edit)
+ layout.addWidget(button)
+ self.ui.tableWidget.setCellWidget(row_index, 1, widget)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 3))
elif value_str.startswith("#") and (len(value_str) == 7 or len(value_str) == 9):
try:
color = QColor(value_str)
@@ -1069,10 +1284,71 @@ def apply_scale(value):
try:
parts = value_str.split()
if len(parts) == 2:
- value_item = QTableWidgetItem(f"X: {self.formatFloat(float(parts[0]))}, Y: {self.formatFloat(float(parts[1]))}")
+ if key == "POSITION":
+ pos_widget = QWidget()
+ pos_layout = QHBoxLayout(pos_widget)
+ pos_layout.setContentsMargins(2, 2, 2, 2)
+ x_spin = QSpinBox()
+ x_spin.setRange(-10000, 10000)
+ x_spin.setValue(int(float(parts[0])))
+ x_spin.setButtonSymbols(QAbstractSpinBox.NoButtons)
+ y_spin = QSpinBox()
+ y_spin.setRange(-10000, 10000)
+ y_spin.setValue(int(float(parts[1])))
+ y_spin.setButtonSymbols(QAbstractSpinBox.NoButtons)
+ def on_x_changed(val):
+ layer = self.currentInspectObject
+ if layer and hasattr(layer, "position"):
+ layer.position[0] = str(val)
+ self.renderPreview(self.cafile.rootlayer)
+ self.markDirty()
+ x_spin.valueChanged.connect(on_x_changed)
+ def on_y_changed(val):
+ layer = self.currentInspectObject
+ if layer and hasattr(layer, "position"):
+ layer.position[1] = str(val)
+ self.renderPreview(self.cafile.rootlayer)
+ self.markDirty()
+ y_spin.valueChanged.connect(on_y_changed)
+ pos_layout.addWidget(x_spin)
+ pos_layout.addWidget(y_spin)
+ self.ui.tableWidget.setCellWidget(row_index, 1, pos_widget)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 2.5))
+ else:
+ value_item = QTableWidgetItem(f"X: {self.formatFloat(float(parts[0]))}, Y: {self.formatFloat(float(parts[1]))}")
elif len(parts) == 4:
if key == "BOUNDS":
- value_item = QTableWidgetItem(f"W: {self.formatFloat(float(parts[2]))}, H: {self.formatFloat(float(parts[3]))}")
+ spin_widget = QWidget()
+ spin_layout = QHBoxLayout(spin_widget)
+ spin_layout.setContentsMargins(2, 2, 2, 2)
+ width_spin = QSpinBox()
+ width_spin.setRange(1, 10000)
+ width_spin.setValue(int(float(parts[2])))
+ width_spin.setButtonSymbols(QAbstractSpinBox.NoButtons)
+ height_spin = QSpinBox()
+ height_spin.setRange(1, 10000)
+ height_spin.setValue(int(float(parts[3])))
+ height_spin.setButtonSymbols(QAbstractSpinBox.NoButtons)
+ def on_width_changed(val):
+ layer = self.currentInspectObject
+ if layer and hasattr(layer, "bounds"):
+ layer.bounds[2] = str(val)
+ self._update_scale_from_bounds(layer)
+ self.renderPreview(self.cafile.rootlayer)
+ self.markDirty()
+ width_spin.valueChanged.connect(on_width_changed)
+ def on_height_changed(val):
+ layer = self.currentInspectObject
+ if layer and hasattr(layer, "bounds"):
+ layer.bounds[3] = str(val)
+ self._update_scale_from_bounds(layer)
+ self.renderPreview(self.cafile.rootlayer)
+ self.markDirty()
+ height_spin.valueChanged.connect(on_height_changed)
+ spin_layout.addWidget(width_spin)
+ spin_layout.addWidget(height_spin)
+ self.ui.tableWidget.setCellWidget(row_index, 1, spin_widget)
+ self.ui.tableWidget.setRowHeight(row_index, int(self.ui.tableWidget.fontMetrics().height() * 2.5))
else:
value_item = QTableWidgetItem(f"X: {self.formatFloat(float(parts[0]))}, Y: {self.formatFloat(float(parts[1]))}, " +
f"W: {self.formatFloat(float(parts[2]))}, H: {self.formatFloat(float(parts[3]))}")
@@ -1575,10 +1851,9 @@ def setupShortcuts(self):
settings_shortcut.activated.connect(self.showSettingsDialog)
self.shortcuts_list.append(settings_shortcut)
- # Standard shortcut for opening a file
- open_file_shortcut = QShortcut(QKeySequence(QKeySequence.StandardKey.Open), self)
- open_file_shortcut.activated.connect(self.openFile)
- self.shortcuts_list.append(open_file_shortcut)
+ save_file_shortcut = QShortcut(QKeySequence(QKeySequence.StandardKey.Save), self)
+ save_file_shortcut.activated.connect(self.saveFile)
+ self.shortcuts_list.append(save_file_shortcut)
export_shortcut_str = self.config_manager.get_export_shortcut()
if export_shortcut_str:
@@ -1687,6 +1962,7 @@ def closeEvent(self, event):
return
self.saveSplitterSizes()
self.saveWindowGeometry()
+ self.discord_rpc.stop()
super().closeEvent(event)
def saveFile(self):
@@ -1835,31 +2111,42 @@ def exportFile(self):
def _create_tendies_structure(self, base_dir, ca_source_path):
"""Creates the necessary directory structure for a .tendies bundle inside base_dir."""
try:
- # Copy descriptors template
- root = sys._MEIPASS if hasattr(sys, '_MEIPASS') else (os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.getcwd())
- descriptors_src = os.path.join(root, "descriptors")
- if not os.path.exists(descriptors_src):
- raise FileNotFoundError("descriptors template directory not found")
-
- descriptors_dest = os.path.join(base_dir, "descriptors")
- shutil.copytree(descriptors_src, descriptors_dest, dirs_exist_ok=True)
-
- # Find the EID directory (assuming only one)
- eid = next((d for d in os.listdir(descriptors_dest) if os.path.isdir(os.path.join(descriptors_dest, d)) and not d.startswith('.')), None)
- if not eid:
- raise FileNotFoundError("Could not find EID directory within descriptors template")
-
- # Define the path for the .wallpaper bundle
- contents_dir = os.path.join(descriptors_dest, eid, "versions", "0", "contents")
- wallpaper_dir = os.path.join(contents_dir, "OpenPoster.wallpaper")
- os.makedirs(wallpaper_dir, exist_ok=True)
-
- # Copy the .ca bundle content into the .wallpaper directory
- if not os.path.exists(ca_source_path):
- raise FileNotFoundError(f".ca source path does not exist: {ca_source_path}")
- ca_basename = os.path.basename(ca_source_path)
- ca_dest_dir = os.path.join(wallpaper_dir, ca_basename)
- shutil.copytree(ca_source_path, ca_dest_dir)
+ temp_save_dir = tempfile.mkdtemp()
+ try:
+ ca_basename = os.path.basename(ca_source_path)
+ if not ca_basename:
+ ca_basename = "export_temp.ca"
+
+ self.cafile.write_file(ca_basename, temp_save_dir)
+
+ ca_source_path = os.path.join(temp_save_dir, ca_basename)
+
+ root = sys._MEIPASS if hasattr(sys, '_MEIPASS') else (os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.getcwd())
+ descriptors_src = os.path.join(root, "descriptors")
+ if not os.path.exists(descriptors_src):
+ raise FileNotFoundError("descriptors template directory not found")
+
+ descriptors_dest = os.path.join(base_dir, "descriptors")
+ shutil.copytree(descriptors_src, descriptors_dest, dirs_exist_ok=True)
+
+ eid = next((d for d in os.listdir(descriptors_dest) if os.path.isdir(os.path.join(descriptors_dest, d)) and not d.startswith('.')), None)
+ if not eid:
+ raise FileNotFoundError("Could not find EID directory within descriptors template")
+
+ contents_dir = os.path.join(descriptors_dest, eid, "versions", "0", "contents")
+ wallpaper_dir = os.path.join(contents_dir, "OpenPoster.wallpaper")
+ os.makedirs(wallpaper_dir, exist_ok=True)
+
+ if not os.path.exists(ca_source_path):
+ raise FileNotFoundError(f".ca source path does not exist: {ca_source_path}")
+
+ ca_dest_dir = os.path.join(wallpaper_dir, ca_basename)
+ shutil.copytree(ca_source_path, ca_dest_dir)
+ print(f"Successfully copied updated .ca content to tendies structure: {ca_dest_dir}")
+
+ finally:
+ if os.path.exists(temp_save_dir):
+ shutil.rmtree(temp_save_dir)
except Exception as e:
print(f"Error creating tendies structure in {base_dir}: {e}")
@@ -1869,21 +2156,36 @@ def markDirty(self):
self.isDirty = True
def onItemMoved(self, item):
+ if item is None:
+ return
+
layer_id = item.data(0)
- if not layer_id: return
+ if not layer_id:
+ return
+
layer = self.cafile.rootlayer.findlayer(layer_id)
if layer:
- pos = item.pos()
-
scene_rect = item.sceneBoundingRect()
w = scene_rect.width()
h = scene_rect.height()
x0, y0 = (float(b) for b in layer.bounds[:2])
-
layer.bounds = [str(x0), str(y0), str(w), str(h)]
- layer.position = [str(pos.x()), str(pos.y())]
+ try:
+ root_h = float(self.cafile.rootlayer.bounds[3])
+ except Exception:
+ root_h = 1000
+
+ center_x = scene_rect.x() + w/2
+ center_y = scene_rect.y() + h/2
+
+ is_flipped = getattr(self.cafile.rootlayer, 'geometryFlipped', "0") == "1"
+
+ if not is_flipped:
+ center_y = root_h - center_y
+
+ layer.position = [str(center_x), str(center_y)]
m11 = item.transform().m11()
m12 = item.transform().m12()
@@ -1900,16 +2202,32 @@ def onItemMoved(self, item):
if not label:
continue
key = label.text()
- if key == 'POSITION':
- self.ui.tableWidget.item(r, 1).setText(self.formatPoint(' '.join(layer.position)))
- elif key == 'BOUNDS':
- self.ui.tableWidget.item(r, 1).setText(self.formatPoint(' '.join(layer.bounds)))
+ widget = self.ui.tableWidget.cellWidget(r, 1)
+ if key == 'POSITION' and widget:
+ x_spin = widget.layout().itemAt(0).widget()
+ y_spin = widget.layout().itemAt(1).widget()
+ x_spin.blockSignals(True)
+ x_spin.setValue(int(float(layer.position[0])))
+ x_spin.blockSignals(False)
+ y_spin.blockSignals(True)
+ y_spin.setValue(int(float(layer.position[1])))
+ y_spin.blockSignals(False)
+ elif key == 'BOUNDS' and widget:
+ w_spin = widget.layout().itemAt(0).widget()
+ h_spin = widget.layout().itemAt(1).widget()
+ w_spin.blockSignals(True)
+ w_spin.setValue(int(float(layer.bounds[2])))
+ w_spin.blockSignals(False)
+ h_spin.blockSignals(True)
+ h_spin.setValue(int(float(layer.bounds[3])))
+ h_spin.blockSignals(False)
elif key == 'TRANSFORM' and layer.transform:
self.ui.tableWidget.item(r, 1).setText(self.formatPoint(layer.transform))
self.ui.tableWidget.blockSignals(False)
self.markDirty()
def onTransformChanged(self, item):
+ self.onItemMoved(item)
self.markDirty()
def onInspectorChanged(self, item):
@@ -1937,6 +2255,7 @@ def onInspectorChanged(self, item):
self.renderPreview(self.cafile.rootlayer)
elif key == 'BOUNDS':
self.currentInspectObject.bounds = value.split(" ")
+ self._update_scale_from_bounds(self.currentInspectObject)
self.renderPreview(self.cafile.rootlayer)
elif key == 'ANCHOR POINT':
self.currentInspectObject.anchorPoint = value
@@ -2044,6 +2363,15 @@ def _clear_nugget_exports_cache(self):
shutil.rmtree(export_dir)
os.makedirs(export_dir, exist_ok=True)
+ self._clear_assets_cache()
+
+ def _clear_assets_cache(self):
+ assets_cache_dir = os.path.join(self.config_manager.config_dir, 'assets-cache')
+ if os.path.exists(assets_cache_dir):
+ shutil.rmtree(assets_cache_dir)
+ os.makedirs(assets_cache_dir, exist_ok=True)
+ return assets_cache_dir
+
def open_project(self, path):
if not path or not os.path.exists(path):
QMessageBox.warning(self, "Open Error", f"The file or folder does not exist: {path}")
@@ -2134,4 +2462,45 @@ def delete_selected_layer(self):
self.currentInspectObject = None
self.renderPreview(self.cafile.rootlayer)
- self.markDirty()
\ No newline at end of file
+ self.markDirty()
+
+ def _update_scale_from_bounds(self, layer):
+ """Helper function to update scale_factor based on layer bounds"""
+ if hasattr(layer, 'scale_factor') and layer.id != self.cafile.rootlayer.id:
+ try:
+ root_height = float(self.cafile.rootlayer.bounds[3])
+ layer_height = float(layer.bounds[3])
+ new_scale = layer_height / root_height
+ layer.scale_factor = new_scale
+
+ for row in range(self.ui.tableWidget.rowCount()):
+ key_item = self.ui.tableWidget.item(row, 0)
+ if key_item and key_item.text() == "SCALE":
+ scale_widget = self.ui.tableWidget.cellWidget(row, 1)
+ if scale_widget:
+ slider = scale_widget.findChild(QSlider)
+ label = scale_widget.findChild(QLabel)
+ if slider and label:
+ slider.blockSignals(True)
+ slider.setValue(int(new_scale * 100))
+ slider.blockSignals(False)
+ label.setText(f"{int(new_scale * 100)}%")
+ return True
+ except (ValueError, IndexError, ZeroDivisionError, AttributeError) as e:
+ print(f"Error updating scale from bounds: {e}")
+ return False
+
+ def show_inspector_placeholder(self):
+ self.ui.tableWidget.blockSignals(True)
+ self.ui.tableWidget.setRowCount(1)
+
+ placeholder_item = QTableWidgetItem("Select a layer to display properties")
+ placeholder_item.setTextAlignment(Qt.AlignCenter)
+ placeholder_item.setFlags(Qt.ItemIsEnabled)
+
+ self.ui.tableWidget.setItem(0, 0, placeholder_item)
+ self.ui.tableWidget.setSpan(0, 0, 1, self.ui.tableWidget.columnCount())
+ self.ui.tableWidget.horizontalHeader().setVisible(False)
+
+ self.currentInspectObject = None
+ self.ui.tableWidget.blockSignals(False)
diff --git a/gui/preview_renderer.py b/gui/preview_renderer.py
index 0823825..0def5e9 100644
--- a/gui/preview_renderer.py
+++ b/gui/preview_renderer.py
@@ -2,6 +2,7 @@
from PySide6.QtGui import QTransform
from PySide6.QtWidgets import QGraphicsRectItem, QGraphicsPixmapItem, QGraphicsTextItem
from PySide6.QtGui import QPen, QBrush, QColor
+import os
class PreviewRenderer:
def __init__(self, window):
@@ -12,6 +13,7 @@ def __init__(self, window):
self.load_image = window.loadImage
self.parse_transform = window.parseTransform
self.parse_color = window.parseColor
+ self.layer_colors = {}
self.animations = []
def render_preview(self, root_layer, target_state=None):
@@ -30,7 +32,7 @@ def render_preview(self, root_layer, target_state=None):
border = QGraphicsRectItem(bounds)
border.setPen(QPen(QColor(0, 0, 0), 2))
- border.setBrush(QBrush(Qt.transparent))
+ border.setBrush(QBrush(QColor(255, 255, 255)))
self.scene.addItem(border)
base_state = None
@@ -42,6 +44,10 @@ def render_preview(self, root_layer, target_state=None):
rect = self.scene.itemsBoundingRect()
self.scene.setSceneRect(rect)
+ for item in self.scene.items():
+ if item.data(1) == 'Layer':
+ self.scene.makeItemEditable(item)
+
if hasattr(self.animation_helper, 'animations'):
self.animations = list(self.animation_helper.animations)
return self.animations
@@ -56,10 +62,20 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
self.render_layer(sub, QPointF(0, 0), parent_transform, base_state, target_state)
return
+ try:
+ root_h = float(self.window.cafile.rootlayer.bounds[3])
+ except Exception:
+ root_h = 1000
+
+ is_flipped = getattr(self.window.cafile.rootlayer, 'geometryFlipped', "0") == "1"
+
pos = QPointF(0, 0)
if hasattr(layer, 'position') and layer.position:
try:
- pos = QPointF(float(layer.position[0]), float(layer.position[1]))
+ y = float(layer.position[1])
+ if not is_flipped:
+ y = root_h - y
+ pos = QPointF(float(layer.position[0]), y)
except Exception:
pass
@@ -75,13 +91,17 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
if hasattr(layer, 'transform') and layer.transform:
transform = transform * self.parse_transform(layer.transform)
- anchor = QPointF(0.5, 0.5)
- if hasattr(layer, 'anchorPoint') and layer.anchorPoint:
- try:
- ax, ay = map(float, layer.anchorPoint.split()[:2])
- anchor = QPointF(ax, ay)
- except Exception:
- pass
+ anchor_str = getattr(layer, 'anchorPoint', "0.5 0.5") or "0.5 0.5"
+ try:
+ anchor_parts = anchor_str.split()
+ anchor = QPointF(float(anchor_parts[0]), float(anchor_parts[1]))
+ except (ValueError, IndexError):
+ anchor = QPointF(0.5, 0.5)
+
+ if is_flipped:
+ qt_anchor = QPointF(anchor.x(), anchor.y())
+ else:
+ qt_anchor = QPointF(anchor.x(), 1.0 - anchor.y())
zpos = float(getattr(layer, 'zPosition', 0) or 0)
try:
@@ -103,7 +123,7 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
key, val = el.keyPath, el.value
try:
if key == 'position.x': pos.setX(float(val))
- elif key == 'position.y': pos.setY(float(val))
+ elif key == 'position.y': pos.setY(root_h - float(val))
elif key == 'transform': transform = self.parse_transform(val) * parent_transform
elif key == 'opacity': opacity = float(val)
elif key == 'zPosition': zpos = float(val)
@@ -133,8 +153,19 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
if hasattr(layer, 'color') and layer.color:
c = self.parse_color(layer.color)
if c: item.setDefaultTextColor(c)
- item.setTransformOriginPoint(bounds.width()*anchor.x(), bounds.height()*anchor.y())
- item.setPos(QPointF(pos.x() - bounds.width()*anchor.x(), pos.y() - bounds.height()*anchor.y()))
+
+ item_pos = QPointF(
+ pos.x() - bounds.width() * qt_anchor.x(),
+ pos.y() - bounds.height() * qt_anchor.y()
+ )
+
+ transform_origin = QPointF(
+ bounds.width() * qt_anchor.x(),
+ bounds.height() * qt_anchor.y()
+ )
+
+ item.setTransformOriginPoint(transform_origin)
+ item.setPos(item_pos)
item.setTransform(transform)
item.setZValue(zpos)
item.setOpacity(opacity)
@@ -146,13 +177,23 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
elif hasattr(layer, '_content') and layer._content is not None:
src = getattr(layer.content, 'src', None)
if src:
+ if hasattr(self.window, 'config_manager'):
+ self.assets.config_dir = self.window.config_manager.config_dir
+ self.assets.assets_cache_dir = os.path.join(self.assets.config_dir, 'assets-cache')
+
self.assets.cafilepath = self.window.cafilepath
self.assets.cachedImages = self.window.cachedImages
+
+ print(f"Trying to load image: {src}")
pix = self.load_image(src)
self.window.cachedImages = self.assets.cachedImages
- if not pix and src in self.window.missing_assets:
- missing_asset = True
- if pix:
+
+ if not pix:
+ print(f"Failed to load image: {src}")
+ if src in self.window.missing_assets:
+ missing_asset = True
+ else:
+ print(f"Successfully loaded image: {src}")
pitem = QGraphicsPixmapItem()
pitem.setPixmap(pix)
sx = bounds.width()/pix.width() if pix.width()>0 else 1
@@ -160,8 +201,19 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
tf = QTransform().scale(sx, sy)
pitem.setTransform(tf*transform)
ww, hh = pix.width()*sx, pix.height()*sy
- pitem.setTransformOriginPoint(ww*anchor.x(), hh*anchor.y())
- pitem.setPos(QPointF(pos.x() - ww*anchor.x(), pos.y() - hh*anchor.y()))
+
+ item_pos = QPointF(
+ pos.x() - ww * qt_anchor.x(),
+ pos.y() - hh * qt_anchor.y()
+ )
+
+ transform_origin = QPointF(
+ ww * qt_anchor.x(),
+ hh * qt_anchor.y()
+ )
+
+ pitem.setTransformOriginPoint(transform_origin)
+ pitem.setPos(item_pos)
pitem.setTransformationMode(Qt.SmoothTransformation)
pitem.setZValue(zpos)
pitem.setOpacity(opacity)
@@ -173,13 +225,28 @@ def render_layer(self, layer, parent_pos, parent_transform, base_state=None, tar
if not has_content:
rect = QGraphicsRectItem(bounds)
- rect.setTransformOriginPoint(bounds.width()*anchor.x(), bounds.height()*anchor.y())
- rect.setPos(QPointF(pos.x() - bounds.width()*anchor.x(), pos.y() - bounds.height()*anchor.y()))
+
+ item_pos = QPointF(
+ pos.x() - bounds.width() * qt_anchor.x(),
+ pos.y() - bounds.height() * qt_anchor.y()
+ )
+
+ transform_origin = QPointF(
+ bounds.width() * qt_anchor.x(),
+ bounds.height() * qt_anchor.y()
+ )
+
+ rect.setTransformOriginPoint(transform_origin)
+ rect.setPos(item_pos)
rect.setTransform(transform)
rect.setZValue(zpos)
rect.setOpacity(opacity)
- pen = QPen(QColor(200,200,200,180),1)
- brush = QBrush(QColor(180,180,180,30))
+ if layer.id not in self.layer_colors:
+ hue = abs(hash(layer.id)) % 360
+ self.layer_colors[layer.id] = QColor.fromHsv(hue, 200, 200)
+ base_color = self.layer_colors[layer.id]
+ pen = QPen(base_color.darker(), 1)
+ brush = QBrush(base_color)
if layer.id == self.window.cafile.rootlayer.id:
pen = QPen(QColor(0,0,0,200),1.5)
brush = QBrush(Qt.transparent)
@@ -221,7 +288,7 @@ def highlight_layer(self, layer):
item.setSelected(True)
self.window.ui.graphicsView.centerOn(item)
for item in self.scene.items():
- if hasattr(item,'data') and item.data(0)==layer.id+'_name':
+ if hasattr(item,'data') and layer.id is not None and item.data(0)==layer.id+'_name':
item.setDefaultTextColor(QColor(0,120,215))
def highlight_animation(self, layer, animation):
diff --git a/gui/settings_window.py b/gui/settings_window.py
index d4f9e92..e9135d8 100644
--- a/gui/settings_window.py
+++ b/gui/settings_window.py
@@ -88,13 +88,14 @@ def __init__(self, parent=None, config_manager=None):
self.ui.discordButton.clicked.connect(lambda: webbrowser.open("https://discord.gg/t3abQJjHm6"))
self.on_theme_changed(getattr(self.parent_window, 'isDarkMode', False))
- # Appearance Tab
- self.appearanceTab = QWidget()
- self.ui.tabWidget.insertTab(1, self.appearanceTab, "Appearance")
- appearanceLayout = QVBoxLayout(self.appearanceTab)
-
+ theme_header = QLabel("Theme")
+ font = theme_header.font()
+ font.setBold(True)
+ theme_header.setFont(font)
+ self.ui.uiTab.layout().addWidget(theme_header)
+
self.themeTableWidget = QTableWidget()
- appearanceLayout.addWidget(self.themeTableWidget)
+ self.ui.uiTab.layout().addWidget(self.themeTableWidget)
self.populate_theme_list()
self.themeTableWidget.itemClicked.connect(self.on_theme_selected)
@@ -345,6 +346,7 @@ def on_language_selected_from_list(self, item: QTableWidgetItem):
self.config_manager.set_language(lang_code)
if hasattr(self.parent_window, 'load_language'):
self.parent_window.load_language(lang_code)
+ self.parent_window.retranslate_ui()
def on_filename_display_changed(self, value):
self.config_manager.set_filename_display_mode(value)
diff --git a/gui/theme_manager.py b/gui/theme_manager.py
index 8e331c0..d563173 100644
--- a/gui/theme_manager.py
+++ b/gui/theme_manager.py
@@ -1,3 +1,4 @@
+import os
from PySide6.QtCore import QEvent
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication
@@ -11,6 +12,18 @@ def load_theme(self):
theme = self.config.get_config("ui_theme", "dark")
is_dark = (theme == "dark")
self.window.isDarkMode = is_dark
+
+ qss_file = "themes/dark_style.qss" if is_dark else "themes/light_style.qss"
+ qss_path = os.path.join(self.window.app_base_path, qss_file)
+
+ if os.path.exists(qss_path):
+ try:
+ with open(qss_path, "r") as f:
+ qss = f.read()
+ QApplication.instance().setStyleSheet(qss)
+ except Exception as e:
+ print(f"Could not load global QSS '{qss_path}': {e}")
+
if is_dark:
self.apply_dark_mode_styles()
else:
@@ -34,6 +47,10 @@ def apply_dark_mode_styles(self):
widget = getattr(window.ui, name, None)
if widget:
widget.setObjectName(name)
+ for name in ('openFile', 'filename'):
+ w = getattr(window.ui, name, None)
+ if w:
+ w.setStyleSheet("")
except Exception as e:
print(f"[ThemeManager] Error applying dark QSS: {e}")
scene = getattr(window, 'scene', None)
@@ -71,6 +88,10 @@ def apply_light_mode_styles(self):
widget = getattr(window.ui, name, None)
if widget:
widget.setObjectName(name)
+ for name in ('openFile', 'filename'):
+ w = getattr(window.ui, name, None)
+ if w:
+ w.setStyleSheet("")
except Exception as e:
print(f"[ThemeManager] Error applying light QSS: {e}")
scene = getattr(window, 'scene', None)
@@ -85,6 +106,9 @@ def apply_light_mode_styles(self):
f"QPushButton:hover {{ background-color: rgba(0,0,0,0.1); }}"
f"QPushButton:pressed {{ background-color: rgba(0,0,0,0.2); }}"
)
+ tw = getattr(window.ui, 'tableWidget', None)
+ if tw:
+ tw.setStyleSheet("")
def update_category_headers(self):
from PySide6.QtGui import QColor
diff --git a/gui/welcome.py b/gui/welcome.py
index d59f632..d15bbe8 100644
--- a/gui/welcome.py
+++ b/gui/welcome.py
@@ -1,6 +1,9 @@
-from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QWidget, QGraphicsOpacityEffect
+from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QWidget, QGraphicsOpacityEffect, QMessageBox
from PySide6.QtGui import QIcon, QPixmap, QFont
from PySide6.QtCore import Qt, QSize, QPropertyAnimation, QSequentialAnimationGroup, QEasingCurve, QParallelAnimationGroup
+from PySide6.QtGui import QShortcut, QKeySequence
+from .config_manager import ConfigManager
+import os, sys, shutil
class WelcomeWindow(QDialog):
def __init__(self, parent=None):
@@ -22,6 +25,9 @@ def __init__(self, parent=None):
pixmap = icon.pixmap(QSize(128, 128))
self.icon_label.setPixmap(pixmap)
+ self.logo_clicks = 0
+ self.icon_label.mousePressEvent = self.logo_clicked
+
self.title_label = QLabel("Welcome to OpenPoster")
title_font = self.title_label.font()
title_font.setPointSize(30)
@@ -65,8 +71,45 @@ def __init__(self, parent=None):
self.btn_open.clicked.connect(self.on_open)
self.result = None
+ # keyboard shortcuts only on welcome screen
+ new_shortcut = QShortcut(QKeySequence(QKeySequence.StandardKey.New), self)
+ new_shortcut.activated.connect(self.on_new)
+
+ open_shortcut = QShortcut(QKeySequence(QKeySequence.StandardKey.Open), self)
+ open_shortcut.activated.connect(self.on_open)
+
self.setup_animations()
+ def logo_clicked(self, event):
+ self.logo_clicks += 1
+ if self.logo_clicks == 3:
+ self.logo_clicks = 0
+ self.reset_application()
+
+ def reset_application(self):
+ reply = QMessageBox.question(self, 'Reset Application',
+ "Are you sure you want to reset all settings and clear caches? The application will restart.",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
+
+ if reply == QMessageBox.StandardButton.Yes:
+ try:
+ config = ConfigManager()
+
+ nugget_exports_dir = os.path.join(config.config_dir, 'nugget-exports')
+ if os.path.exists(nugget_exports_dir):
+ shutil.rmtree(nugget_exports_dir)
+
+ assets_cache_dir = os.path.join(config.config_dir, 'assets-cache')
+ if os.path.exists(assets_cache_dir):
+ shutil.rmtree(assets_cache_dir)
+
+ config.reset_to_defaults()
+
+ self.result = ("reset", None)
+ self.accept()
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Could not reset settings: {e}")
+
def setup_animations(self):
welcome_widgets = [self.icon_label, self.title_label, self.version_label]
button_widgets = [self.btn_new, self.btn_open]
diff --git a/lib/ca_elements/core/cafile.py b/lib/ca_elements/core/cafile.py
index cba0699..6a560b7 100644
--- a/lib/ca_elements/core/cafile.py
+++ b/lib/ca_elements/core/cafile.py
@@ -1,6 +1,7 @@
import xml.etree.ElementTree as ET
import os.path
import plistlib
+import xml.dom.minidom as minidom
from .calayer import CALayer
@@ -57,4 +58,11 @@ def write_file(self, filename, path="./"):
for key, val in list(elem.attrib.items()):
if val is None:
elem.attrib.pop(key)
- tree.write(os.path.join(capath, self.index['rootDocument']))
+
+ xml_str = ET.tostring(tree.getroot(), encoding='utf-8')
+
+ dom = minidom.parseString(xml_str)
+ formatted_xml_bytes = dom.toprettyxml(indent=" ", encoding="UTF-8")
+
+ with open(os.path.join(capath, self.index['rootDocument']), 'wb') as f:
+ f.write(formatted_xml_bytes)
diff --git a/requirements.txt b/requirements.txt
index dab366e..0fe8389 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
PyInstaller
PySide6-Essentials
+pypresence
diff --git a/themes/dark_style.qss b/themes/dark_style.qss
index a61d00f..450af39 100644
--- a/themes/dark_style.qss
+++ b/themes/dark_style.qss
@@ -5,6 +5,26 @@ QWidget {
color: #D0D0D0;
}
+QPushButton QMenu {
+ background-color: #3A3A3A;
+ border: 1px solid #606060;
+ border-radius: 8px;
+ color: #D0D0D0;
+}
+
+QPushButton QMenu::item {
+ background-color: transparent;
+ color: #D0D0D0;
+ padding: 8px 20px;
+ margin: 0px;
+ border-radius: 0px;
+}
+
+QPushButton QMenu::item:selected {
+ background-color: #5A5A5A;
+ color: #FFFFFF;
+}
+
/* All Headers Styling */
QHeaderView::section {
background-color: #424242;
@@ -302,8 +322,17 @@ QInputDialog QLabel {
#openFileButton {
color: #FFFFFF;
+ border: 1px solid #A0A0A0;
}
#fileNameLabel {
color: #FFFFFF;
-}
\ No newline at end of file
+ border: 1px solid #A0A0A0;
+}
+#openFile {
+ border: 1px solid #A0A0A0;
+}
+#filename {
+ border: 1px solid #A0A0A0;
+ border-radius: 8px;
+}
\ No newline at end of file
diff --git a/themes/light_style.qss b/themes/light_style.qss
index 600b2a9..089319f 100644
--- a/themes/light_style.qss
+++ b/themes/light_style.qss
@@ -5,6 +5,26 @@ QWidget {
color: #303030;
}
+QPushButton QMenu {
+ background-color: #FFFFFF;
+ border: 1px solid #A0A0A0;
+ border-radius: 8px;
+ color: #303030;
+}
+
+QPushButton QMenu::item {
+ background-color: transparent;
+ color: #303030;
+ padding: 8px 20px;
+ margin: 0px;
+ border-radius: 0px;
+}
+
+QPushButton QMenu::item:selected {
+ background-color: #E0E0E0;
+ color: #000000;
+}
+
/* All Headers Styling */
QHeaderView::section {
background-color: #F0F0F0;
@@ -303,8 +323,17 @@ QInputDialog QLabel {
#openFileButton {
color: #303030;
+ border: 1px solid #000000;
}
#fileNameLabel {
color: #303030;
+ border: 1px solid #000000;
+}
+#openFile {
+ border: 1px solid #000000;
+}
+#filename {
+ border: 1px solid #000000;
+ border-radius: 8px;
}
\ No newline at end of file
diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui
index 003d94d..9d647e6 100644
--- a/ui/mainwindow.ui
+++ b/ui/mainwindow.ui
@@ -461,30 +461,6 @@ QTableWidget::item:first-column {
5
-
-
-
-
- false
-
-
- QPushButton {
- border: 1.5px solid palette(highlight);
- border-radius: 8px;
- padding: 3px 8px;
- background-color: rgba(80, 120, 200, 30);
-}
-QPushButton:pressed {
- background-color: rgba(60, 100, 180, 120);
-}
-
-
- Open File
-
-
- 30
-
-
-
-
diff --git a/ui/ui_mainwindow.py b/ui/ui_mainwindow.py
index e2d3067..64c2865 100644
--- a/ui/ui_mainwindow.py
+++ b/ui/ui_mainwindow.py
@@ -292,22 +292,6 @@ def setupUi(self, OpenPoster):
self.horizontalLayout_header = QHBoxLayout(self.headerWidget)
self.horizontalLayout_header.setObjectName(u"horizontalLayout_header")
self.horizontalLayout_header.setContentsMargins(5, 5, 5, 5)
- self.openFile = QPushButton(self.headerWidget)
- self.openFile.setObjectName(u"openFile")
- self.openFile.setAutoFillBackground(False)
- self.openFile.setStyleSheet(u"QPushButton {\n"
-" border: 1.5px solid palette(highlight);\n"
-" border-radius: 8px;\n"
-" padding: 3px 8px;\n"
-" background-color: rgba(80, 120, 200, 30);\n"
-"}\n"
-"QPushButton:pressed {\n"
-" background-color: rgba(60, 100, 180, 120);\n"
-"}")
- self.openFile.setProperty(u"fixedHeight", 30)
-
- self.horizontalLayout_header.addWidget(self.openFile)
-
self.filename = QLabel(self.headerWidget)
self.filename.setObjectName(u"filename")
self.filename.setMinimumSize(QSize(200, 28))
@@ -513,7 +497,6 @@ def setupUi(self, OpenPoster):
# setupUi
def retranslateUi(self, OpenPoster):
- self.openFile.setText(QCoreApplication.translate("OpenPoster", u"Open File", None))
self.filename.setText(QCoreApplication.translate("OpenPoster", u"No File Open", None))
#if QT_CONFIG(tooltip)
self.exportButton.setToolTip(QCoreApplication.translate("OpenPoster", u"Export", None))