diff --git a/dag_capture.py b/dag_capture.py index e63cc69..029356e 100644 --- a/dag_capture.py +++ b/dag_capture.py @@ -1,352 +1,325 @@ -import logging +""" +DAG Capture - High-resolution screenshot tool for Nuke's node graph. +Supports Nuke 15 (PySide2/OpenGL) and Nuke 16+ (PySide6/QWidget). +""" import nuke -import time - from math import ceil -from typing import Tuple - -if nuke.NUKE_VERSION_MAJOR < 16: - from PySide2 import QtWidgets, QtOpenGL, QtGui, QtCore - DAGType = QtOpenGL.QGLWidget +from typing import List - def is_dag_widget(widget): - return isinstance(widget, QtOpenGL.QGLWidget) -else: +# Qt imports based on Nuke version +IS_NUKE_16 = nuke.NUKE_VERSION_MAJOR >= 16 +if IS_NUKE_16: from PySide6 import QtWidgets, QtGui, QtCore - DAGType = QtWidgets.QWidget - - def is_dag_widget(widget): - return widget.objectName() == 'DAG' - - -def get_dag() -> DAGType: - """Retrieve the QGLWidget of DAG graph""" - stack = QtWidgets.QApplication.topLevelWidgets() - while stack: - widget = stack.pop() - if widget.objectName() == 'DAG.1': - for c in widget.children(): - if is_dag_widget(c): - return c - - stack.extend(c for c in widget.children() if c.isWidgetType()) - - -def grab_dag(dag: DAGType, painter: QtGui.QPainter, xpos: int, ypos: int) -> None: - """Draw dag frame buffer to painter image at given coordinates""" - # updateGL does some funky stuff because grabFrameBuffer grabs the wrong thing without it + ALIGN_RIGHT = QtCore.Qt.AlignmentFlag.AlignRight + STYLED_PANEL = QtWidgets.QFrame.Shape.StyledPanel + BUTTONS = QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + COMP_MODE = QtGui.QPainter.CompositionMode.CompositionMode_SourceOver +else: + from PySide2 import QtWidgets, QtOpenGL, QtGui, QtCore + ALIGN_RIGHT = QtCore.Qt.AlignRight + STYLED_PANEL = QtWidgets.QFrame.StyledPanel + BUTTONS = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + COMP_MODE = QtGui.QPainter.CompositionMode_SourceOver + + +class DagInfo: + """DAG widget and associated group node.""" + def __init__(self, widget, group, name): + self.widget, self.group, self.name = widget, group, name + + +def find_dags() -> List[DagInfo]: + """Find all available DAG widgets.""" + dags = {} + for w in QtWidgets.QApplication.allWidgets(): + name = w.objectName() + if not name or not name.startswith('DAG.') or name in dags: + continue + suffix = name[4:] + child = next((c for c in w.children() if c.objectName() == 'DAG'), None) + if child: + dags[name] = DagInfo(child, None, f"Root (Panel {suffix})") + elif IS_NUKE_16 and not suffix.isdigit(): + group = nuke.toNode(suffix) + dags[name] = DagInfo(w, group, f"{suffix} ({group.Class()})" if group else suffix) + return sorted(dags.values(), key=lambda d: (0 if not d.group else 1, d.name)) + + +def grab_dag(dag, painter, x, y): + """Capture DAG widget to painter.""" try: dag.updateGL() img = dag.grabFrameBuffer() except AttributeError: - # For PySide6 / Nuke 16. Can't seem to access the Open GL Widget anymore. - # Sadly widget.grab() doesn't work either, so we fallback to doing screen captures, even - # though it could have the cursor or other windows in the way. QtWidgets.QApplication.processEvents() screen = QtWidgets.QApplication.primaryScreen() - # Convert the widget position to screen position pos = dag.mapToGlobal(QtCore.QPoint(0, 0)) - pix = screen.grabWindow(0, pos.x(), pos.y(), dag.width(), dag.height()) - - # Need a QImage for the painter - img = pix.toImage() - painter.drawImage(xpos, ypos, img) + img = screen.grabWindow(0, pos.x(), pos.y(), dag.width(), dag.height()).toImage() + painter.drawImage(x, y, img) + + +def zoom_dag(group, zoom, center): + """Zoom DAG using proper group context.""" + if group: + with group: + nuke.zoom(zoom, center) + else: + nuke.zoom(zoom, center) + + +class DagCaptureWorker(QtCore.QObject): + """Timer-based capture for Nuke 16+.""" + finished = QtCore.Signal(bool) + + def __init__(self, dag_info): + super().__init__() + self.dag, self.group = dag_info.widget, dag_info.group + self.tiles, self.idx = [], 0 + self.pixmap, self.painter, self.orig, self.cfg = None, None, None, {} + self.timer = QtCore.QTimer() + self.timer.setSingleShot(True) + self.timer.timeout.connect(self._capture) + + def start(self, path, bbox, zoom, margins, crop, delay): + self.cfg = {'path': path, 'zoom': zoom, 'delay': delay} + self.orig = (nuke.zoom(), tuple(nuke.center())) if not self.group else None + if self.group: + with self.group: + self.orig = (nuke.zoom(), tuple(nuke.center())) + + m = int(margins / zoom) + min_x, min_y, max_x, max_y = bbox[0]-m, bbox[1]-m, bbox[2]+m, bbox[3]+m + cap_w, cap_h = self.dag.width() - crop, self.dag.height() + img_w, img_h = int((max_x-min_x)*zoom), int((max_y-min_y)*zoom) + + self.tiles = [(cap_w*tx, cap_h*ty, + min_x + cap_w/zoom*tx + (cap_w+crop)/zoom/2, + min_y + cap_h/zoom*ty + cap_h/zoom/2) + for tx in range(ceil(img_w/cap_w)) for ty in range(ceil(img_h/cap_h))] + + self.pixmap = QtGui.QPixmap(img_w, img_h) + self.painter = QtGui.QPainter(self.pixmap) + self.painter.setCompositionMode(COMP_MODE) + self.idx = 0 + QtCore.QTimer.singleShot(150, self._next) # Initial delay for window animation + + def _next(self): + if self.idx >= len(self.tiles): + self._finish() + return + _, _, cx, cy = self.tiles[self.idx] + zoom_dag(self.group, self.cfg['zoom'], (cx, cy)) + self.dag.update() + QtWidgets.QApplication.processEvents() + self.timer.start(self.cfg['delay']) + + def _capture(self): + try: nuke.updateUI() + except: pass + for _ in range(10): QtWidgets.QApplication.processEvents() + grab_dag(self.dag, self.painter, self.tiles[self.idx][0], self.tiles[self.idx][1]) + self.idx += 1 + QtCore.QTimer.singleShot(50, self._next) + + def _finish(self): + self.painter.end() + zoom_dag(self.group, self.orig[0], self.orig[1]) + ok = self.pixmap.save(self.cfg['path']) + self.pixmap, self.painter = None, None + self.finished.emit(ok) + + +class DagCaptureThread(QtCore.QThread): + """Thread-based capture for Nuke 15.""" + def __init__(self, dag_info): + super().__init__() + self.dag, self.group = dag_info.widget, dag_info.group + self.cfg, self.successful = {}, False + + def configure(self, path, bbox, zoom, margins, crop, delay): + self.cfg = {'path': path, 'bbox': bbox, 'zoom': zoom, + 'margins': margins, 'crop': crop, 'delay': delay} + + def run(self): + import time + c = self.cfg + if self.group: + with self.group: orig = (nuke.zoom(), tuple(nuke.center())) + else: orig = (nuke.zoom(), tuple(nuke.center())) + + m = int(c['margins']/c['zoom']) + min_x, min_y, max_x, max_y = c['bbox'][0]-m, c['bbox'][1]-m, c['bbox'][2]+m, c['bbox'][3]+m + cap_w, cap_h = self.dag.width()-c['crop'], self.dag.height() + img_w, img_h = int((max_x-min_x)*c['zoom']), int((max_y-min_y)*c['zoom']) + + pix = QtGui.QPixmap(img_w, img_h) + p = QtGui.QPainter(pix) + p.setCompositionMode(COMP_MODE) + + for tx in range(ceil(img_w/cap_w)): + cx = min_x + cap_w/c['zoom']*tx + (cap_w+c['crop'])/c['zoom']/2 + for ty in range(ceil(img_h/cap_h)): + cy = min_y + cap_h/c['zoom']*ty + cap_h/c['zoom']/2 + nuke.executeInMainThreadWithResult(zoom_dag, (self.group, c['zoom'], (cx, cy))) + time.sleep(c['delay']) + nuke.executeInMainThreadWithResult(grab_dag, (self.dag, p, cap_w*tx, cap_h*ty)) + + p.end() + nuke.executeInMainThreadWithResult(zoom_dag, (self.group, orig[0], orig[1])) + self.successful = pix.save(c['path']) class DagCapturePanel(QtWidgets.QDialog): - """UI Panel for DAG capture options""" - - def __init__(self) -> None: - parent = QtWidgets.QApplication.instance().activeWindow() - super(DagCapturePanel, self).__init__(parent) - - # Variables - self.dag = get_dag() - if not self.dag: - raise RuntimeError("Couldn't get DAG widget") - - self.dag_bbox = None - self.capture_thread = DagCapture(self.dag) - self.capture_thread.finished.connect(self.on_thread_finished) - self.selection = [] - - # UI - self.setWindowTitle("DAG Capture options") - - main_layout = QtWidgets.QVBoxLayout() - form_layout = QtWidgets.QFormLayout() - form_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) - form_layout.setLabelAlignment(QtCore.Qt.AlignRight) - main_layout.addLayout(form_layout) - - # region Options + """DAG capture settings dialog.""" + + def __init__(self): + super().__init__(QtWidgets.QApplication.activeWindow()) + self.dags = find_dags() + if not self.dags: + raise RuntimeError("No DAG panels found. Open a DAG view first.") + self.dag, self.worker, self.sel = None, None, [] + self._build_ui() + self.dag_cb.setCurrentIndex(next((i for i,d in enumerate(self.dags) if d.group), 0)) + + def _build_ui(self): + self.setWindowTitle("DAG Capture") + self.setMinimumWidth(320) + L = QtWidgets.QFormLayout(self) + L.setLabelAlignment(ALIGN_RIGHT) + L.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow if IS_NUKE_16 else QtWidgets.QFormLayout.AllNonFixedFieldsGrow) + + # DAG selector + row = QtWidgets.QHBoxLayout() + self.dag_cb = QtWidgets.QComboBox() + self.dag_cb.addItems([d.name for d in self.dags]) + self.dag_cb.currentIndexChanged.connect(self._dag_changed) + row.addWidget(self.dag_cb, 1) + btn = QtWidgets.QPushButton("↻") + btn.setFixedWidth(30) + btn.clicked.connect(self._refresh) + row.addWidget(btn) + L.addRow("DAG:", row) + # Path - container = QtWidgets.QWidget() - path_layout = QtWidgets.QHBoxLayout() - path_layout.setContentsMargins(0, 0, 0, 0) - container.setLayout(path_layout) + row = QtWidgets.QHBoxLayout() self.path = QtWidgets.QLineEdit() - browse_button = QtWidgets.QPushButton("Browse") - browse_button.clicked.connect(self.show_file_browser) - path_layout.addWidget(self.path) - path_layout.addWidget(browse_button) - form_layout.addRow("File Path", container) - - # Zoom - self.zoom_level = QtWidgets.QDoubleSpinBox() - self.zoom_level.setValue(1.0) - self.zoom_level.setRange(0.01, 5) - self.zoom_level.setSingleStep(.5) - self.zoom_level.valueChanged.connect(self.display_info) - form_layout.addRow("Zoom Level", self.zoom_level) - - # Margins + row.addWidget(self.path) + btn = QtWidgets.QPushButton("...") + btn.setFixedWidth(30) + btn.clicked.connect(lambda: self.path.setText( + QtWidgets.QFileDialog.getSaveFileName(self, "Save", "", "PNG (*.png)")[0] or self.path.text())) + row.addWidget(btn) + L.addRow("File:", row) + + # Settings + self.zoom = QtWidgets.QDoubleSpinBox() + self.zoom.setRange(0.01, 5); self.zoom.setValue(1); self.zoom.setSingleStep(0.25) + self.zoom.valueChanged.connect(self._update) + L.addRow("Zoom:", self.zoom) + self.margins = QtWidgets.QSpinBox() - self.margins.setRange(0, 1000) - self.margins.setValue(20) - self.margins.setSuffix("px") - self.margins.setSingleStep(10) - self.margins.valueChanged.connect(self.display_info) - form_layout.addRow("Margins", self.margins) - - # Right Crop - self.ignore_right = QtWidgets.QSpinBox() - self.ignore_right.setRange(0, 1000) - self.ignore_right.setValue(200) - self.ignore_right.setSuffix("px") - self.ignore_right.setToolTip( - "The right side of the DAG usually contains a mini version of itself.\n" - "This gets included in the screen capture, so it is required to crop it out. \n" - "If you scaled it down, you can reduce this number to speed up capture slightly." - ) - self.ignore_right.valueChanged.connect(self.display_info) - form_layout.addRow("Crop Right Side", self.ignore_right) - - # Delay + self.margins.setRange(0, 500); self.margins.setValue(20); self.margins.setSuffix(" px") + self.margins.valueChanged.connect(self._update) + L.addRow("Margins:", self.margins) + + self.crop = QtWidgets.QSpinBox() + self.crop.setRange(0, 500); self.crop.setValue(200); self.crop.setSuffix(" px") + self.crop.setToolTip("Crop minimap from right") + self.crop.valueChanged.connect(self._update) + L.addRow("Crop Right:", self.crop) + self.delay = QtWidgets.QDoubleSpinBox() - self.delay.setValue(0) - self.delay.setRange(0, 1) - self.delay.setSuffix("s") - self.delay.setSingleStep(.1) - self.delay.valueChanged.connect(self.display_info) - self.delay.setToolTip( - "A longer delay ensures the Nuke DAG has fully refreshed between capturing tiles.\n" - "It makes the capture slower, but ensures a correct result.\n" - "Feel free to adjust based on results you have seen on your machine.\n" - "Increase if the capture looks incorrect." - ) - form_layout.addRow("Delay Between Captures", self.delay) - - # Capture all nodes or selection - self.capture = QtWidgets.QComboBox() - self.capture.addItems(["All Nodes", "Selected Nodes"]) - self.capture.currentIndexChanged.connect(self.inspect_dag) - form_layout.addRow("Nodes to Capture", self.capture) - - # Deselect Nodes before Capture? - self.deselect = QtWidgets.QCheckBox("Deselect Nodes before capture") - self.deselect.setChecked(True) - form_layout.addWidget(self.deselect) - # endregion Options - - # Add Information box - self.info = QtWidgets.QLabel("Hi") - info_box = QtWidgets.QFrame() - info_box.setFrameStyle(QtWidgets.QFrame.StyledPanel) - info_box.setLayout(QtWidgets.QVBoxLayout()) - info_box.layout().addWidget(self.info) - main_layout.addWidget(info_box) - - # Buttons - button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel - ) - button_box.accepted.connect(self.do_capture) - button_box.rejected.connect(self.reject) - main_layout.addWidget(button_box) - - self.setLayout(main_layout) - - self.inspect_dag() - - def display_info(self) -> None: - """Displays the calculated information""" - zoom = self.zoom_level.value() - - # Check the size of the current widget, excluding the right side (because of minimap) - capture_width = self.dag.width() - crop = self.ignore_right.value() - if crop >= capture_width: - self.info.setText( - "Error: Crop is larger than capture area.\n" - "Increase DAG size or reduce crop." - ) - return - - capture_width -= crop - capture_height = self.dag.height() - - # Calculate the number of tiles required to cover all - min_x, min_y, max_x, max_y = self.dag_bbox - image_width = (max_x - min_x) * zoom + self.margins.value() * 2 - image_height = (max_y - min_y) * zoom + self.margins.value() * 2 - - horizontal_tiles = int(ceil(image_width / float(capture_width))) - vertical_tiles = int(ceil(image_height / float(capture_height))) - total_tiles = horizontal_tiles * vertical_tiles - total_time = total_tiles * self.delay.value() - - info = "Image Size: {width}x{height}\n" \ - "Number of tiles required: {tiles} (Increase DAG size to reduce) \n" \ - "Estimated Capture Duration: {time}s" - info = info.format(width=int(image_width), height=int(image_height), tiles=total_tiles, - time=total_time) - self.info.setText(info) - - def inspect_dag(self) -> None: - """Calculate the bounding box for DAG""" - nodes = nuke.allNodes() if self.capture.currentIndex() == 0 else nuke.selectedNodes() - - # Calculate the total size of the DAG - min_x, min_y, max_x, max_y = [], [], [], [] - for node in nodes: - min_x.append(node.xpos()) - min_y.append(node.ypos()) - max_x.append(node.xpos() + node.screenWidth()) - max_y.append(node.ypos() + node.screenHeight()) - - self.dag_bbox = (min(min_x), min(min_y), max(max_x), max(max_y)) - self.display_info() - - def show_file_browser(self) -> None: - """Display the file browser""" - filename, _filter = QtWidgets.QFileDialog.getSaveFileName( - parent=self, caption='Select output file', - filter="PNG Image (*.png)") - self.path.setText(filename) - - def do_capture(self) -> None: - """Run the capture thread""" + self.delay.setRange(0, 2); self.delay.setValue(0.10 if IS_NUKE_16 else 0) + self.delay.setSuffix(" s"); self.delay.setSingleStep(0.05) + self.delay.valueChanged.connect(self._update) + L.addRow("Tile Delay:", self.delay) + + self.nodes = QtWidgets.QComboBox() + self.nodes.addItems(["All Nodes", "Selected"]) + self.nodes.currentIndexChanged.connect(self._update) + L.addRow("Capture:", self.nodes) + + self.desel = QtWidgets.QCheckBox("Deselect nodes before capture") + self.desel.setChecked(True) + L.addRow("", self.desel) + + self.info = QtWidgets.QLabel() + f = QtWidgets.QFrame(); f.setFrameStyle(STYLED_PANEL) + QtWidgets.QVBoxLayout(f).addWidget(self.info) + L.addRow(f) + + btns = QtWidgets.QDialogButtonBox(BUTTONS) + btns.accepted.connect(self._capture) + btns.rejected.connect(self.reject) + L.addRow(btns) + + self._dag_changed(self.dag_cb.currentIndex()) + + def _dag_changed(self, i): + if 0 <= i < len(self.dags): + self.dag = self.dags[i] + self.setWindowTitle(f"DAG Capture - {self.dag.name}") + self._update() + + def _refresh(self): + cur = self.dag_cb.currentText() + self.dags = find_dags() + self.dag_cb.blockSignals(True) + self.dag_cb.clear() + self.dag_cb.addItems([d.name for d in self.dags]) + self.dag_cb.setCurrentIndex(next((i for i,d in enumerate(self.dags) if d.name==cur), 0)) + self.dag_cb.blockSignals(False) + self._dag_changed(self.dag_cb.currentIndex()) + + def _bbox(self): + g = self.dag.group + if g: + with g: nodes = nuke.allNodes() if self.nodes.currentIndex()==0 else nuke.selectedNodes() + else: nodes = nuke.allNodes() if self.nodes.currentIndex()==0 else nuke.selectedNodes() + if not nodes: return (0,0,100,100) + return (min(n.xpos() for n in nodes), min(n.ypos() for n in nodes), + max(n.xpos()+n.screenWidth() for n in nodes), max(n.ypos()+n.screenHeight() for n in nodes)) + + def _update(self): + if not self.dag: return + b = self._bbox() + z, m, cr = self.zoom.value(), self.margins.value(), self.crop.value() + cw, ch = self.dag.widget.width()-cr, self.dag.widget.height() + if cw <= 0: self.info.setText("Error: Crop > DAG width"); return + iw, ih = int((b[2]-b[0])*z + m*2), int((b[3]-b[1])*z + m*2) + tiles = ceil(iw/cw) * ceil(ih/ch) + self.info.setText(f"Output: {iw} × {ih} px\nTiles: {tiles}\nEstimated Capture Duration: ~{tiles*self.delay.value():.1f}s") + + def _capture(self): + if not self.path.text(): nuke.message("Specify output path."); return self.hide() - - # Deselect nodes if required: - if self.deselect.isChecked(): - for selected_node in nuke.selectedNodes(): - self.selection.append(selected_node) - selected_node.setSelected(False) - - # Push settings to Thread - self.capture_thread.path = self.path.text() - self.capture_thread.margins = self.margins.value() - self.capture_thread.ignore_right = self.ignore_right.value() - self.capture_thread.delay = self.delay.value() - self.capture_thread.bbox = self.dag_bbox - self.capture_thread.zoom = self.zoom_level.value() - - # Run thread - self.capture_thread.start() - - def on_thread_finished(self) -> None: - """Re-Select previously selected items and display a result popup""" - # Re-Select previously selected items - for node in self.selection: - node.setSelected(True) - - # Display a result popup - if self.capture_thread.successful: - nuke.message( - "Capture complete:\n" - "{}".format(self.path.text()) - ) + b = self._bbox() + if self.desel.isChecked(): + if self.dag.group: + with self.dag.group: self.sel = nuke.selectedNodes() + else: self.sel = nuke.selectedNodes() + for n in self.sel: n.setSelected(False) + + if IS_NUKE_16: + self.worker = DagCaptureWorker(self.dag) + self.worker.finished.connect(self._done) + self.worker.start(self.path.text(), b, self.zoom.value(), + self.margins.value(), self.crop.value(), int(self.delay.value()*1000)) else: - nuke.message( - "Something went wrong with the DAG capture, please check script editor for details" - ) - - -class DagCapture(QtCore.QThread): - """Thread class for capturing screenshot of Nuke DAG""" - def __init__( - self, - dag: DAGType, - path: str = '', - margins: int = 20, - ignore_right: int = 200, - delay=0, - bbox: Tuple[int, int, int, int] = (-50, 50, -50, 50), - zoom: int = 1.0 - ) -> None: - super(DagCapture, self).__init__() - self.dag = dag - self.path = path - self.margins = margins - self.ignore_right = ignore_right - self.delay = delay - self.bbox = bbox - self.zoom = zoom - self.successful = False - - def run(self) -> None: - """On thread start""" - # Store the current dag size and zoom - original_zoom = nuke.zoom() - original_center = nuke.center() - - # Calculate the total size of the DAG - min_x, min_y, max_x, max_y = self.bbox - zoom = self.zoom - min_x -= int(self.margins / zoom) - min_y -= int(self.margins / zoom) - max_x += int(self.margins / zoom) - max_y += int(self.margins / zoom) - - # Get the Dag Widget - dag = self.dag - - # Check the size of the current widget, excluding the right side (because of minimap) - capture_width = dag.width() - self.ignore_right - capture_height = dag.height() - - # Calculate the number of tiles required to cover all - image_width = int((max_x - min_x) * zoom) - image_height = int((max_y - min_y) * zoom) - horizontal_tiles = int(ceil(image_width / float(capture_width))) - vertical_tiles = int(ceil(image_height / float(capture_height))) - - # Create a pixmap to store the results - pixmap = QtGui.QPixmap(image_width, image_height) - painter = QtGui.QPainter(pixmap) - painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver) - - # Move the dag so that the top left corner is in the top left corner, - # screenshot, paste in the pixmap, repeat - for tile_x in range(horizontal_tiles): - x_offset_tile = (min_x + capture_width / zoom * tile_x) - x_offset_zoom = (capture_width + self.ignore_right) / zoom / 2 - center_x = x_offset_tile + x_offset_zoom - for tile_y in range(vertical_tiles): - center_y = (min_y + capture_height / zoom * tile_y) + capture_height / zoom / 2 - nuke.executeInMainThreadWithResult(nuke.zoom, (zoom, (center_x, center_y))) - time.sleep(self.delay) - nuke.executeInMainThreadWithResult(grab_dag, - (dag, painter, capture_width * tile_x, - capture_height * tile_y)) - time.sleep(self.delay) - painter.end() - nuke.executeInMainThreadWithResult(nuke.zoom, (original_zoom, original_center)) - save_successful = pixmap.save(self.path) - if not save_successful: - raise IOError("Failed to save PNG: %s" % self.path) - - self.successful = True + self.worker = DagCaptureThread(self.dag) + self.worker.finished.connect(lambda: self._done(self.worker.successful)) + self.worker.configure(self.path.text(), b, self.zoom.value(), + self.margins.value(), self.crop.value(), self.delay.value()) + self.worker.start() + def _done(self, ok): + for n in self.sel: n.setSelected(True) + self.sel = [] + nuke.message(f"Saved: {self.path.text()}" if ok else "Capture failed.") -def open_dag_capture() -> None: - """Opens a blocking dag capture""" - logging.info("Opening dag capture window") - dag_capture_panel = DagCapturePanel() - dag_capture_panel.show() +def open_dag_capture(): + DagCapturePanel().show() if __name__ == '__main__': - open_dag_capture() + open_dag_capture() \ No newline at end of file