Files
Beakfx-mbq/mbq_browser.py
T
Beak 1a94bf93df almost everything
just about all the features for the browser only. Next is to make the wedge node and incorporate it.
2026-05-25 21:45:20 -07:00

368 lines
13 KiB
Python

from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QFrame, QGroupBox,
QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton,
QScrollArea, QTextEdit, QMessageBox, QFileDialog,
)
from PySide6.QtGui import QAction, QDesktopServices
from PySide6.QtCore import Qt, QTimer, QEvent, QUrl
from mbq_functions import ImageCanvas, WorkflowCache
from mbq_parser import get_png_metadata
from mbq_logic import MetaViewLogicMixin
import sys
class StatusBulb(QLabel):
_COLORS = {"on": "#44cc44", "warn": "#c8823a", "off": "#2a2a2a"}
def __init__(self, tooltip_name, parent=None):
super().__init__(parent)
self._name = tooltip_name
self.setFixedSize(14, 14)
self._state = "off"
self._apply()
def set_state(self, state: str):
if state != self._state:
self._state = state
self._apply()
def _apply(self):
c = self._COLORS[self._state]
self.setStyleSheet(f"background:{c}; border-radius:7px; border:1px solid #555;")
self.setToolTip(f"{self._name}: {'ON' if self._state != 'off' else 'OFF'}")
class MetaViewApp(MetaViewLogicMixin, QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MBQ — Meta Browser")
self.workflow_cache = WorkflowCache(max_size=50)
self.resize(1400, 900)
self.folder_model = None
self.image_cache = {}
self.thumb_cache = {}
self.cache_size = 5
self.design_canvas_w = 1024
self.design_canvas_h = 768
self.design_thumb_w = 120
self.design_thumb_h = 90
self.scale_factor = 1.0
# ---- Central Widget ----
central = QWidget()
self.setCentralWidget(central)
# ---- Master Grid Layout ----
self.main_layout = QGridLayout(central)
self.main_layout.setContentsMargins(10, 20, 10, 20)
self.main_layout.setSpacing(10)
self.main_layout.setColumnStretch(0, 0)
self.main_layout.setColumnStretch(1, 3)
self.main_layout.setColumnStretch(2, 1)
self.main_layout.setRowStretch(0, 1)
# --- Left Spacer ---
self.left_spacer = QFrame()
self.left_spacer.setFixedWidth(40)
self.left_spacer.setStyleSheet("background: transparent;")
self.main_layout.addWidget(self.left_spacer, 0, 0)
# --- Center Image Area ---
self.center_group = QGroupBox("Image View")
self.center_group.setStyleSheet("""
QGroupBox {
border: 2px solid #333333;
border-radius: 4px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 10px;
padding: 0 5px 0 5px;
background: palette(window);
}
""")
self.main_layout.addWidget(self.center_group, 0, 1)
center_layout = QGridLayout(self.center_group)
center_layout.setRowStretch(0, 5)
center_layout.setRowStretch(1, 1)
center_layout.setRowStretch(2, 0)
center_layout.setColumnStretch(0, 1)
# ---- Image Canvas ----
self.image_view = ImageCanvas()
self.image_view.fileDropped.connect(self.load_image_from_path)
self.image_view.setMinimumSize(800, 600)
center_layout.addWidget(self.image_view, 0, 0)
# ---- Filmstrip Frame ----
self.filmstrip_frame = QFrame()
filmstrip_layout = QVBoxLayout(self.filmstrip_frame)
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("color: #252222;")
filmstrip_layout.addWidget(sep)
self.thumb_scroll_area = QScrollArea()
self.thumb_scroll_area.setWidgetResizable(True)
self.thumb_scroll_area.setMinimumHeight(100)
self.thumb_container = QWidget()
self.thumb_layout = QHBoxLayout(self.thumb_container)
self.thumb_scroll_area.setWidget(self.thumb_container)
filmstrip_layout.addWidget(self.thumb_scroll_area)
center_layout.addWidget(self.filmstrip_frame, 1, 0)
# ---- Button Frame ----
self.button_frame = QFrame()
btn_layout = QHBoxLayout(self.button_frame)
btn_layout.setContentsMargins(0, 2, 0, 2)
next_btn = QPushButton("Next Image")
prev_btn = QPushButton("Previous Image")
open_btn = QPushButton("Open Image")
btn_layout.addWidget(next_btn)
btn_layout.addWidget(prev_btn)
btn_layout.addWidget(open_btn)
# ---- Status Bulbs ----
btn_layout.addStretch()
bulb_style = "color: #888; font-size: 10px;"
self.bulb_zoom = StatusBulb("Zoom Lock")
zoom_lbl = QLabel("Zoom")
zoom_lbl.setStyleSheet(bulb_style)
zoom_group = QHBoxLayout()
zoom_group.setSpacing(4)
zoom_group.addWidget(self.bulb_zoom)
zoom_group.addWidget(zoom_lbl)
btn_layout.addLayout(zoom_group)
self.bulb_wedge = StatusBulb("Wedge node")
wedge_lbl = QLabel("Wedge")
wedge_lbl.setStyleSheet(bulb_style)
wedge_group = QHBoxLayout()
wedge_group.setSpacing(4)
wedge_group.addWidget(self.bulb_wedge)
wedge_group.addWidget(wedge_lbl)
btn_layout.addLayout(wedge_group)
self.bulb_uncomfy = StatusBulb("UnComfy")
uncomfy_lbl = QLabel("UnComfy")
uncomfy_lbl.setStyleSheet(bulb_style)
uncomfy_group = QHBoxLayout()
uncomfy_group.setSpacing(4)
uncomfy_group.addWidget(self.bulb_uncomfy)
uncomfy_group.addWidget(uncomfy_lbl)
btn_layout.addLayout(uncomfy_group)
center_layout.addWidget(self.button_frame, 2, 0)
# --- Right Metadata Panel ---
self.right_metadata = QGroupBox("Metadata")
self.right_metadata.setMinimumWidth(300)
metadata_vbox = QVBoxLayout()
metadata_vbox.setContentsMargins(8, 8, 8, 8)
metadata_vbox.setSpacing(4)
self.right_metadata.setLayout(metadata_vbox)
label_style = "color: #888; font-size: 11px;"
def style_label(lbl):
lbl.setStyleSheet(label_style)
return lbl
self.file_name_label = style_label(QLabel("File:"))
self.file_dim_label = style_label(QLabel("Dimensions:"))
self.file_size_label = style_label(QLabel("Size:"))
self.file_mod_label = style_label(QLabel("Modified:"))
_sel = Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard
self.file_name_value = QLabel("—")
self.file_name_value.setTextInteractionFlags(_sel)
self.file_dim_value = QLabel("—")
self.file_dim_value.setTextInteractionFlags(_sel)
self.file_size_value = QLabel("—")
self.file_size_value.setTextInteractionFlags(_sel)
self.file_mod_value = QLabel("—")
self.file_mod_value.setTextInteractionFlags(_sel)
file_grid = QGridLayout()
file_grid.setHorizontalSpacing(8)
file_grid.setVerticalSpacing(4)
file_grid.setContentsMargins(0, 0, 0, 0)
file_grid.addWidget(self.file_name_label, 0, 0)
file_grid.addWidget(self.file_name_value, 0, 1)
file_grid.addWidget(self.file_dim_label, 1, 0)
file_grid.addWidget(self.file_dim_value, 1, 1)
file_grid.addWidget(self.file_size_label, 2, 0)
file_grid.addWidget(self.file_size_value, 2, 1)
file_grid.addWidget(self.file_mod_label, 3, 0)
file_grid.addWidget(self.file_mod_value, 3, 1)
metadata_vbox.addLayout(file_grid)
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("color: #333;")
metadata_vbox.addWidget(sep)
mono_style = "font-family: 'Courier New', 'Consolas', monospace; font-size: 9pt;"
self.primary_display = QTextEdit()
self.primary_display.setReadOnly(True)
self.primary_display.setLineWrapMode(QTextEdit.WidgetWidth)
self.primary_display.setStyleSheet(mono_style)
metadata_vbox.addWidget(self.primary_display, stretch=1)
self.tier3_btn = QPushButton("▶ plumbing")
self.tier3_btn.setStyleSheet("text-align: left; padding: 2px 6px; font-size: 10px;")
self.tier3_btn.setVisible(False)
self.tier3_btn.clicked.connect(self.toggle_tier3)
metadata_vbox.addWidget(self.tier3_btn)
self.tier3_display = QTextEdit()
self.tier3_display.setReadOnly(True)
self.tier3_display.setLineWrapMode(QTextEdit.WidgetWidth)
self.tier3_display.setStyleSheet(mono_style)
self.tier3_display.setMaximumHeight(160)
self.tier3_display.setVisible(False)
metadata_vbox.addWidget(self.tier3_display)
self.primary_display.installEventFilter(self)
self.tier3_display.installEventFilter(self)
copy_btn = QPushButton("Copy Metadata")
metadata_vbox.addWidget(copy_btn)
self.main_layout.addWidget(self.right_metadata, 0, 2)
# ---- Menu Bar ----
menu_bar = self.menuBar()
# File
file_menu = menu_bar.addMenu("File")
file_menu.addAction("Open Image", self.open_image_file)
file_menu.addAction("Open Path", self.open_folder)
file_menu.addSeparator()
file_menu.addAction("Exit", QApplication.instance().quit)
# Edit
edit_menu = menu_bar.addMenu("Edit")
edit_menu.addAction("Copy Image", self._copy_image)
edit_menu.addAction("Copy Workflow", self._copy_workflow)
# View
view_menu = menu_bar.addMenu("View")
self._lock_zoom_action = QAction("Lock Zoom", self, checkable=True, shortcut="Z")
self._lock_zoom_action.toggled.connect(self._on_zoom_lock_toggled)
view_menu.addAction(self._lock_zoom_action)
view_menu.addAction("Reset Zoom", self.image_view.reset_zoom)
view_menu.addSeparator()
self._freeze_scroll_action = QAction("Freeze Scroll", self, checkable=True, shortcut="S")
view_menu.addAction(self._freeze_scroll_action)
# About
about_menu = menu_bar.addMenu("About")
about_menu.addAction("About MBQ…", self._show_about)
about_menu.addAction("Help", self._open_help)
# ---- Connect Signals ----
open_btn.clicked.connect(self.open_image_file)
next_btn.clicked.connect(self.show_next_image)
prev_btn.clicked.connect(self.show_prev_image)
copy_btn.clicked.connect(self._copy_workflow)
QTimer.singleShot(100, self.initialize_scale)
self.installEventFilter(self)
# ---- Menu handlers ----
def open_folder(self):
folder = QFileDialog.getExistingDirectory(self, "Open Folder")
if folder:
self.load_folder_from_path(folder)
def _on_zoom_lock_toggled(self, checked: bool):
self.image_view.zoom_locked = checked
self.bulb_zoom.set_state("on" if checked else "off")
def _copy_image(self):
if self.image_view.image_item:
QApplication.clipboard().setPixmap(self.image_view.image_item.pixmap())
def _copy_workflow(self):
lines = [
f"File: {self.file_name_value.text()}",
f"Dimensions: {self.file_dim_value.text()}",
f"Size: {self.file_size_value.text()}",
f"Modified: {self.file_mod_value.text()}",
"",
self.primary_display.toPlainText(),
]
t3 = self.tier3_display.toPlainText()
if t3:
lines += ["", "--- plumbing ---", t3]
QApplication.clipboard().setText("\n".join(lines))
def _show_about(self):
QMessageBox.about(
self,
"About MBQ",
"MBQ — Meta Browser Qt\n"
"PySide6 image viewer for ComfyUI PNGs.\n\n"
"Reads ComfyUI prompt metadata from PNG text chunks\n"
"and displays generation parameters in a side panel.",
)
def _open_help(self):
# TODO: replace with actual repo URL
QDesktopServices.openUrl(QUrl("https://github.com/YOUR_REPO"))
# ---- Event filter ----
def eventFilter(self, obj, event):
if event.type() == QEvent.Wheel:
if obj in (self.primary_display, self.tier3_display):
obj.verticalScrollBar().setValue(
obj.verticalScrollBar().value() - event.angleDelta().y() // 4
)
return True
if event.type() == QEvent.KeyPress:
if event.key() in (Qt.Key_Right, Qt.Key_Left):
self.keyPressEvent(event)
return True
return super().eventFilter(obj, event)
# ---- Cache helpers ----
def get_workflow_data(self, file_path):
return self.workflow_cache.get(file_path, get_png_metadata)
def preload_workflows(self, file_paths):
self.workflow_cache.preload_batch(file_paths, get_png_metadata)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MetaViewApp()
window.show()
if len(sys.argv) > 1:
import os
if os.path.isfile(sys.argv[1]):
QTimer.singleShot(0, lambda: window.load_image_from_path(sys.argv[1]))
elif os.path.isdir(sys.argv[1]):
QTimer.singleShot(0, lambda: window.load_folder_from_path(sys.argv[1]))
sys.exit(app.exec())