Skip to content

screenshot_capture

Screenshot capture widget and handler for text box component.

Based on AYON core screenshot widget with proper DPI handling.

ScreenMarquee

Bases: QObject

Screen marquee selection tool.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
class ScreenMarquee(QObject):
    """Screen marquee selection tool."""

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        screens_by_id = {}
        for screen in QApplication.screens():
            screen_id = uuid.uuid4().hex
            screen_dialog = ScreenMarqueeDialog(screen, screen_id)
            screens_by_id[screen_id] = screen_dialog
            screen_dialog.mouse_moved.connect(self._on_mouse_move)
            screen_dialog.mouse_pressed.connect(self._on_mouse_press)
            screen_dialog.mouse_released.connect(self._on_mouse_release)
            screen_dialog.close_requested.connect(self._on_close_request)

        self._screens_by_id = screens_by_id
        self._finished = False
        self._captured = False
        self._start_pos = None
        self._end_pos = None
        self._start_screen_id = None
        self._pix = None

    def get_captured_pixmap(self):
        if self._pix is None:
            return QPixmap()
        return self._pix

    def _close_dialogs(self):
        for dialog in self._screens_by_id.values():
            dialog.close()

    def _on_close_request(self):
        self._close_dialogs()
        self._finished = True

    def _on_mouse_release(self, pos):
        start_screen_dialog = self._screens_by_id.get(self._start_screen_id)
        if start_screen_dialog is None:
            self._finished = True
            self._captured = False
            return

        end_pos = start_screen_dialog.convert_end_pos(pos)
        self._close_dialogs()
        self._end_pos = end_pos
        self._finished = True
        self._captured = True

    def _on_mouse_press(self, pos, screen_id):
        self._start_pos = pos
        self._start_screen_id = screen_id

    def _on_mouse_move(self):
        for dialog in self._screens_by_id.values():
            dialog.repaint()

    def start_capture(self):
        for dialog in self._screens_by_id.values():
            dialog.show()
            dialog.setWindowState(Qt.WindowState.WindowActive)

        app = QApplication.instance()
        if app is not None:
            while not self._finished:
                app.processEvents()

            # Give time to close dialogs
            for _ in range(2):
                app.processEvents()
        else:
            logger.error(
                "No QApplication instance found. "
                "Capture may not work properly."
            )
            self._finished = True

        if self._captured:
            self._pix = self.get_desktop_pixmap(self._start_pos, self._end_pos)

    @classmethod
    def get_desktop_pixmap(cls, pos_start, pos_end):
        """Capture screen area between two points."""
        # Unify start and end points
        if pos_start.y() > pos_end.y():
            pos_start, pos_end = pos_end, pos_start

        if pos_start.x() > pos_end.x():
            new_start = QPoint(pos_end.x(), pos_start.y())
            new_end = QPoint(pos_start.x(), pos_end.y())
            pos_start = new_start
            pos_end = new_end

        # Validate rectangle
        rect = QRect(pos_start, pos_end)
        if rect.width() < 1 or rect.height() < 1:
            return QPixmap()

        screen = QApplication.screenAt(pos_start)
        if screen is None:
            return QPixmap()

        return screen.grabWindow(
            0,
            pos_start.x() - screen.geometry().x(),
            pos_start.y() - screen.geometry().y(),
            pos_end.x() - pos_start.x(),
            pos_end.y() - pos_start.y(),
        )

    @classmethod
    def capture_to_pixmap(cls):
        """Take screenshot with marquee into pixmap."""
        tool = cls()
        tool.start_capture()
        return tool.get_captured_pixmap()

capture_to_pixmap() classmethod

Take screenshot with marquee into pixmap.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
289
290
291
292
293
294
@classmethod
def capture_to_pixmap(cls):
    """Take screenshot with marquee into pixmap."""
    tool = cls()
    tool.start_capture()
    return tool.get_captured_pixmap()

get_desktop_pixmap(pos_start, pos_end) classmethod

Capture screen area between two points.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
@classmethod
def get_desktop_pixmap(cls, pos_start, pos_end):
    """Capture screen area between two points."""
    # Unify start and end points
    if pos_start.y() > pos_end.y():
        pos_start, pos_end = pos_end, pos_start

    if pos_start.x() > pos_end.x():
        new_start = QPoint(pos_end.x(), pos_start.y())
        new_end = QPoint(pos_start.x(), pos_end.y())
        pos_start = new_start
        pos_end = new_end

    # Validate rectangle
    rect = QRect(pos_start, pos_end)
    if rect.width() < 1 or rect.height() < 1:
        return QPixmap()

    screen = QApplication.screenAt(pos_start)
    if screen is None:
        return QPixmap()

    return screen.grabWindow(
        0,
        pos_start.x() - screen.geometry().x(),
        pos_start.y() - screen.geometry().y(),
        pos_end.x() - pos_start.x(),
        pos_end.y() - pos_start.y(),
    )

ScreenMarqueeDialog

Bases: QWidget

Dialog for single screen marquee selection.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class ScreenMarqueeDialog(QWidget):
    """Dialog for single screen marquee selection."""

    mouse_moved = Signal()
    mouse_pressed = Signal(QPoint, str)
    mouse_released = Signal(QPoint)
    close_requested = Signal()

    def __init__(self, screen: QScreen, screen_id: str):
        super().__init__()
        self.setWindowFlags(
            Qt.WindowType.Window
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
            | Qt.WindowType.CustomizeWindowHint
        )
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setCursor(Qt.CursorShape.CrossCursor)
        self.setMouseTracking(True)

        screen.geometryChanged.connect(self._fit_screen_geometry)

        self._screen = screen
        self._opacity = 100
        self._click_pos = None
        self._screen_id = screen_id

    def set_click_pos(self, pos):
        self._click_pos = pos
        self.repaint()

    def convert_end_pos(self, pos):
        glob_pos = self.mapFromGlobal(pos)
        new_pos = self._convert_pos(glob_pos)
        return self.mapToGlobal(new_pos)

    def paintEvent(self, event):
        """Paint event"""
        mouse_pos = self._convert_pos(self.mapFromGlobal(QCursor.pos()))
        rect = event.rect()
        fill_path = QPainterPath()
        fill_path.addRect(rect)

        capture_rect = None
        if self._click_pos is not None:
            click_pos = self.mapFromGlobal(self._click_pos)
            capture_rect = QRect(click_pos, mouse_pos)

            # Clear the capture area
            sub_path = QPainterPath()
            sub_path.addRect(capture_rect)
            fill_path = fill_path.subtracted(sub_path)

        painter = QPainter(self)
        painter.setRenderHints(
            QPainter.RenderHint.Antialiasing
            | QPainter.RenderHint.SmoothPixmapTransform
        )

        # Draw background
        painter.setBrush(QColor(0, 0, 0, self._opacity))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawPath(fill_path)

        # Draw cropping markers at current mouse position
        pen_color = QColor(255, 255, 255, self._opacity)
        pen = QPen(pen_color, 1, Qt.PenStyle.DotLine)
        painter.setPen(pen)
        painter.drawLine(
            rect.left(), mouse_pos.y(), rect.right(), mouse_pos.y()
        )
        painter.drawLine(
            mouse_pos.x(), rect.top(), mouse_pos.x(), rect.bottom()
        )

        # Draw rectangle around selection area
        if capture_rect is not None:
            pen_color = QColor(92, 173, 214)
            pen = QPen(pen_color, 2, Qt.PenStyle.SolidLine)
            painter.setPen(pen)
            painter.setBrush(Qt.BrushStyle.NoBrush)
            l_x = capture_rect.left()
            r_x = capture_rect.right()
            if l_x > r_x:
                l_x, r_x = r_x, l_x
            t_y = capture_rect.top()
            b_y = capture_rect.bottom()
            if t_y > b_y:
                t_y, b_y = b_y, t_y

            r_x -= 1
            b_y -= 1
            sel_rect = QRect(QPoint(l_x, t_y), QPoint(r_x, b_y))
            painter.drawRect(sel_rect)

        painter.end()

    def mousePressEvent(self, event):
        """Mouse click event"""
        if event.button() == Qt.MouseButton.LeftButton:
            self._click_pos = event.globalPos()
            self.mouse_pressed.emit(self._click_pos, self._screen_id)

    def mouseReleaseEvent(self, event):
        """Mouse release event"""
        if event.button() == Qt.MouseButton.LeftButton:
            self._click_pos = None
            self.mouse_released.emit(event.globalPos())

    def mouseMoveEvent(self, event):
        """Mouse move event"""
        self.mouse_moved.emit()

    def keyPressEvent(self, event):
        """Key press event"""
        if event.key() == Qt.Key.Key_Escape:
            self._click_pos = None
            event.accept()
            self.close_requested.emit()
            return
        return super().keyPressEvent(event)

    def showEvent(self, event):
        super().showEvent(event)
        self._fit_screen_geometry()

    def closeEvent(self, event):
        self._click_pos = None
        super().closeEvent(event)

    def _convert_pos(self, pos):
        geo = self.geometry()
        if pos.x() > geo.width():
            pos.setX(geo.width() - 1)
        elif pos.x() < 0:
            pos.setX(0)

        if pos.y() > geo.height():
            pos.setY(geo.height() - 1)
        elif pos.y() < 0:
            pos.setY(0)
        return pos

    def _fit_screen_geometry(self):
        if hasattr(self, "setScreen"):
            self.setScreen(self._screen)
        self.setGeometry(self._screen.geometry())

keyPressEvent(event)

Key press event

Source code in client/ayon_ui_qt/components/screenshot_capture.py
142
143
144
145
146
147
148
149
def keyPressEvent(self, event):
    """Key press event"""
    if event.key() == Qt.Key.Key_Escape:
        self._click_pos = None
        event.accept()
        self.close_requested.emit()
        return
    return super().keyPressEvent(event)

mouseMoveEvent(event)

Mouse move event

Source code in client/ayon_ui_qt/components/screenshot_capture.py
138
139
140
def mouseMoveEvent(self, event):
    """Mouse move event"""
    self.mouse_moved.emit()

mousePressEvent(event)

Mouse click event

Source code in client/ayon_ui_qt/components/screenshot_capture.py
126
127
128
129
130
def mousePressEvent(self, event):
    """Mouse click event"""
    if event.button() == Qt.MouseButton.LeftButton:
        self._click_pos = event.globalPos()
        self.mouse_pressed.emit(self._click_pos, self._screen_id)

mouseReleaseEvent(event)

Mouse release event

Source code in client/ayon_ui_qt/components/screenshot_capture.py
132
133
134
135
136
def mouseReleaseEvent(self, event):
    """Mouse release event"""
    if event.button() == Qt.MouseButton.LeftButton:
        self._click_pos = None
        self.mouse_released.emit(event.globalPos())

paintEvent(event)

Paint event

Source code in client/ayon_ui_qt/components/screenshot_capture.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def paintEvent(self, event):
    """Paint event"""
    mouse_pos = self._convert_pos(self.mapFromGlobal(QCursor.pos()))
    rect = event.rect()
    fill_path = QPainterPath()
    fill_path.addRect(rect)

    capture_rect = None
    if self._click_pos is not None:
        click_pos = self.mapFromGlobal(self._click_pos)
        capture_rect = QRect(click_pos, mouse_pos)

        # Clear the capture area
        sub_path = QPainterPath()
        sub_path.addRect(capture_rect)
        fill_path = fill_path.subtracted(sub_path)

    painter = QPainter(self)
    painter.setRenderHints(
        QPainter.RenderHint.Antialiasing
        | QPainter.RenderHint.SmoothPixmapTransform
    )

    # Draw background
    painter.setBrush(QColor(0, 0, 0, self._opacity))
    painter.setPen(Qt.PenStyle.NoPen)
    painter.drawPath(fill_path)

    # Draw cropping markers at current mouse position
    pen_color = QColor(255, 255, 255, self._opacity)
    pen = QPen(pen_color, 1, Qt.PenStyle.DotLine)
    painter.setPen(pen)
    painter.drawLine(
        rect.left(), mouse_pos.y(), rect.right(), mouse_pos.y()
    )
    painter.drawLine(
        mouse_pos.x(), rect.top(), mouse_pos.x(), rect.bottom()
    )

    # Draw rectangle around selection area
    if capture_rect is not None:
        pen_color = QColor(92, 173, 214)
        pen = QPen(pen_color, 2, Qt.PenStyle.SolidLine)
        painter.setPen(pen)
        painter.setBrush(Qt.BrushStyle.NoBrush)
        l_x = capture_rect.left()
        r_x = capture_rect.right()
        if l_x > r_x:
            l_x, r_x = r_x, l_x
        t_y = capture_rect.top()
        b_y = capture_rect.bottom()
        if t_y > b_y:
            t_y, b_y = b_y, t_y

        r_x -= 1
        b_y -= 1
        sel_rect = QRect(QPoint(l_x, t_y), QPoint(r_x, b_y))
        painter.drawRect(sel_rect)

    painter.end()

ScreenshotHandler

Handles screenshot capture for text box.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
class ScreenshotHandler:
    """Handles screenshot capture for text box."""

    def __init__(self, parent_widget, screenshot_button):
        """Initialize screenshot handler.

        Args:
            parent_widget: Parent widget instance (typically AYTextBox)
            screenshot_button: Screenshot button widget
        """
        self._parent = parent_widget
        self.screenshot_btn = screenshot_button
        self._pending_screenshots = []
        # Create a dedicated temp directory for this handler
        self._temp_dir = tempfile.TemporaryDirectory(
            prefix="ayon_screenshots_"
        )

    def launch_capture(self):
        """Launch screenshot capture tool."""
        main_window = self._parent.window()
        if main_window:
            main_window.showMinimized()

        QTimer.singleShot(200, self._show_snipping_widget)

    def _show_snipping_widget(self):
        """Show snipping widget after window is minimized."""
        pixmap = ScreenMarquee.capture_to_pixmap()
        self._on_screenshot_captured(pixmap)

    def _on_screenshot_captured(self, pixmap):
        """Handle captured screenshot."""
        if pixmap and not pixmap.isNull():
            screenshot_num = len(self._pending_screenshots) + 1
            temp_path = os.path.join(
                self._temp_dir.name, f"ss{screenshot_num}.jpg"
            )
            if pixmap.save(temp_path, "JPEG"):
                self._pending_screenshots.append(temp_path)

                # Add to parent's unified attachment system
                if hasattr(self._parent, "add_attachment"):
                    self._parent.add_attachment(temp_path, "screenshot")

                logger.debug("Screenshot attached: %s", temp_path)

        self._restore_window()

    def _restore_window(self):
        """Restore parent window."""
        main_window = self._parent.window()
        if main_window:
            main_window.showNormal()
            main_window.activateWindow()

    def get_screenshot_paths(self):
        """Get list of pending screenshot paths.

        Returns:
            list: List of screenshot file paths
        """
        return self._pending_screenshots.copy()

    def clear_screenshots(self):
        """Clear all pending screenshots and their files."""
        self._pending_screenshots.clear()
        # Recreate the temp dir (deletes old files)
        self._temp_dir.cleanup()
        self._temp_dir = tempfile.TemporaryDirectory(
            prefix="ayon_screenshots_"
        )

    def cleanup(self):
        """Delete all screenshot files. Call when handler is
        no longer needed."""
        self._pending_screenshots.clear()
        self._temp_dir.cleanup()

__init__(parent_widget, screenshot_button)

Initialize screenshot handler.

Parameters:

Name Type Description Default
parent_widget

Parent widget instance (typically AYTextBox)

required
screenshot_button

Screenshot button widget

required
Source code in client/ayon_ui_qt/components/screenshot_capture.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def __init__(self, parent_widget, screenshot_button):
    """Initialize screenshot handler.

    Args:
        parent_widget: Parent widget instance (typically AYTextBox)
        screenshot_button: Screenshot button widget
    """
    self._parent = parent_widget
    self.screenshot_btn = screenshot_button
    self._pending_screenshots = []
    # Create a dedicated temp directory for this handler
    self._temp_dir = tempfile.TemporaryDirectory(
        prefix="ayon_screenshots_"
    )

cleanup()

Delete all screenshot files. Call when handler is no longer needed.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
370
371
372
373
374
def cleanup(self):
    """Delete all screenshot files. Call when handler is
    no longer needed."""
    self._pending_screenshots.clear()
    self._temp_dir.cleanup()

clear_screenshots()

Clear all pending screenshots and their files.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
361
362
363
364
365
366
367
368
def clear_screenshots(self):
    """Clear all pending screenshots and their files."""
    self._pending_screenshots.clear()
    # Recreate the temp dir (deletes old files)
    self._temp_dir.cleanup()
    self._temp_dir = tempfile.TemporaryDirectory(
        prefix="ayon_screenshots_"
    )

get_screenshot_paths()

Get list of pending screenshot paths.

Returns:

Name Type Description
list

List of screenshot file paths

Source code in client/ayon_ui_qt/components/screenshot_capture.py
353
354
355
356
357
358
359
def get_screenshot_paths(self):
    """Get list of pending screenshot paths.

    Returns:
        list: List of screenshot file paths
    """
    return self._pending_screenshots.copy()

launch_capture()

Launch screenshot capture tool.

Source code in client/ayon_ui_qt/components/screenshot_capture.py
315
316
317
318
319
320
321
def launch_capture(self):
    """Launch screenshot capture tool."""
    main_window = self._parent.window()
    if main_window:
        main_window.showMinimized()

    QTimer.singleShot(200, self._show_snipping_widget)