Skip to content

Commit ed26902

Browse files
authored
fix(capture-tool): Fix a delay after capture when has annotations in screenshot (#76)
1 parent 9cc4883 commit ed26902

2 files changed

Lines changed: 109 additions & 22 deletions

File tree

src/capture/captureTools.ts

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,25 @@ import { gettext as _ } from '~/shared/i18n.ts';
55
import * as Main from '@girs/gnome-shell/ui/main';
66
import { Slider } from '@girs/gnome-shell/ui/slider';
77
import { AnnotationCanvas } from '~/capture/annotationCanvas.ts';
8-
import { AnnotationModel, type AnnotationTool, type Point } from '~/capture/annotationModel.ts';
8+
import {
9+
AnnotationModel,
10+
type Annotation,
11+
type AnnotationTool,
12+
type Point,
13+
} from '~/capture/annotationModel.ts';
914
import { OcrController, OcrUnavailableError } from '~/capture/ocrController.ts';
1015
import { buildWebSearchUri, placeOcrActionBelow, type OcrWord } from '~/capture/ocrLogic.ts';
1116
import { CaptureTooltip } from '~/capture/captureTooltip.ts';
12-
import { captureScreenshot, exportAnnotatedScreenshot } from '~/capture/screenshotCapture.ts';
13-
import { getScreenshotUi, type ScreenshotUi } from '~/capture/screenshotUiAdapter.ts';
17+
import {
18+
captureScreenshot,
19+
exportAnnotatedScreenshot,
20+
type CapturedScreenshot,
21+
} from '~/capture/screenshotCapture.ts';
22+
import {
23+
getScreenshotUi,
24+
type Geometry,
25+
type ScreenshotUi,
26+
} from '~/capture/screenshotUiAdapter.ts';
1427
import type { ExtensionContext } from '~/core/context.ts';
1528
import { LifecycleScope } from '~/core/lifecycleScope.ts';
1629
import { logger } from '~/core/logger.ts';
@@ -99,6 +112,7 @@ export class CaptureTools extends Module {
99112
private _textPoint: Point | null = null;
100113
private _toolbarDrag: ToolbarDrag | null = null;
101114
private _toolbarGrab: Clutter.Grab | null = null;
115+
private _toolbarDraggedByUser = false;
102116
private _ocrText = '';
103117
private _ocrBusy = false;
104118
private _devOcrAvailable: boolean | null = null;
@@ -222,6 +236,7 @@ export class CaptureTools extends Module {
222236
this._portalMode = false;
223237
this._toolbarDrag = null;
224238
this._toolbarGrab = null;
239+
this._toolbarDraggedByUser = false;
225240
this._originalOpen = null;
226241
this._openWrapper = null;
227242
this._originalSaveScreenshot = null;
@@ -235,11 +250,17 @@ export class CaptureTools extends Module {
235250
scope.connect(button, 'notify::checked', () => this._syncVisibility());
236251
for (const button of [ui._selectionButton, ui._screenButton, ui._windowButton])
237252
scope.connect(button, 'notify::checked', () => this._clearOcr());
253+
scope.connect(ui._selectionButton, 'notify::checked', () => this._syncToolbarPlacement());
238254
scope.connect(ui._areaSelector, 'drag-started', () => {
239255
this._clearOcr();
240256
this._setInteractionState('selection');
241257
});
242-
scope.connect(ui._areaSelector, 'drag-ended', () => this._setInteractionState('idle'));
258+
scope.connect(ui._areaSelector, 'drag-ended', () => {
259+
this._setInteractionState('idle');
260+
this._syncToolbarPlacement();
261+
});
262+
if (this._toolbar)
263+
scope.connect(this._toolbar, 'notify::allocation', () => this._syncToolbarPlacement());
243264

244265
const monitorsChangedId = Main.layoutManager.connect('monitors-changed', () => {
245266
this._endToolbarDrag();
@@ -289,18 +310,12 @@ export class CaptureTools extends Module {
289310
const model = this._model;
290311
if (this._portalMode || !model?.hasAnnotations) return original.call(ui);
291312

313+
const annotations = [...model.annotations];
292314
try {
293315
const capture = await captureScreenshot(ui);
294-
const file = await exportAnnotatedScreenshot(
295-
capture.pixbuf,
296-
model.annotations,
297-
{ origin: capture.origin, scale: capture.scale },
298-
{ copy: true, save: true },
299-
);
300-
if (file) ui.emit('screenshot-taken', file);
301-
logger.debug(`Saved screenshot with ${model.annotations.length} annotation(s)`, {
302-
prefix: LOG_PREFIX,
303-
});
316+
// Return now so the caller closes the UI with the default animation;
317+
// the annotated export finishes in the background.
318+
void this._exportAnnotatedScreenshot(ui, capture, annotations);
304319
} catch (error) {
305320
logger.warn(`[CaptureTools] Annotated screenshot export failed: ${String(error)}`);
306321
Main.notify(_('Screenshot failed'), _('Could not export the annotated screenshot'));
@@ -310,6 +325,28 @@ export class CaptureTools extends Module {
310325
ui._saveScreenshot = wrapper;
311326
}
312327

328+
private async _exportAnnotatedScreenshot(
329+
ui: ScreenshotUi,
330+
capture: CapturedScreenshot,
331+
annotations: readonly Annotation[],
332+
): Promise<void> {
333+
try {
334+
const file = await exportAnnotatedScreenshot(
335+
capture.pixbuf,
336+
annotations,
337+
{ origin: capture.origin, scale: capture.scale },
338+
{ copy: true, save: true },
339+
);
340+
if (file) ui.emit('screenshot-taken', file);
341+
logger.debug(`Saved screenshot with ${annotations.length} annotation(s)`, {
342+
prefix: LOG_PREFIX,
343+
});
344+
} catch (error) {
345+
logger.warn(`[CaptureTools] Annotated screenshot export failed: ${String(error)}`);
346+
Main.notify(_('Screenshot failed'), _('Could not export the annotated screenshot'));
347+
}
348+
}
349+
313350
private _buildToolbar(): St.BoxLayout {
314351
const toolbar = new St.BoxLayout({
315352
style_class: 'screenshot-ui-panel capture-tools-toolbar',
@@ -496,6 +533,7 @@ export class CaptureTools extends Module {
496533
private _releaseToolbar(event: Clutter.Event): boolean {
497534
if (!this._toolbarDrag || event.get_button() !== Clutter.BUTTON_PRIMARY)
498535
return Clutter.EVENT_PROPAGATE;
536+
this._toolbarDraggedByUser = true;
499537
this._endToolbarDrag();
500538
return Clutter.EVENT_STOP;
501539
}
@@ -697,6 +735,51 @@ export class CaptureTools extends Module {
697735
this._canvas?.setDrawingEnabled(this._model ? this._isDrawingTool(this._model.tool) : false);
698736
}
699737
this._syncOcrButton();
738+
if (visible) this._syncToolbarPlacement();
739+
}
740+
741+
private _syncToolbarPlacement(): void {
742+
const toolbar = this._toolbar;
743+
const ui = this._ui;
744+
if (!toolbar || !ui || !toolbar.visible || toolbar.width === 0) return;
745+
// A position picked manually through the drag handle wins for the session.
746+
if (this._toolbarDraggedByUser) return;
747+
748+
const monitor = Main.layoutManager.primaryMonitor;
749+
if (!monitor) return;
750+
751+
let selection: Geometry | null = null;
752+
if (!this._portalMode && ui._shotButton.checked && ui._selectionButton.checked) {
753+
try {
754+
const [x, y, width, height] = ui._areaSelector.getGeometry();
755+
if (width > 0 && height > 0) selection = [x, y, width, height];
756+
} catch {
757+
// The selector may not have geometry before its first allocation.
758+
}
759+
}
760+
761+
if (!selection) {
762+
toolbar.translation_x = 0;
763+
toolbar.translation_y = 0;
764+
return;
765+
}
766+
767+
// Anchor to the aligned top-center slot the toolbar is allocated in, then
768+
// translate so it floats just above the selection rectangle.
769+
const [stageX, stageY] = toolbar.get_transformed_position();
770+
const anchorX = stageX - toolbar.translation_x;
771+
const anchorY = stageY - toolbar.translation_y;
772+
const [x, y, width, height] = selection;
773+
const margin = 12;
774+
const desiredX = Math.max(
775+
monitor.x,
776+
Math.min(x + width / 2 - toolbar.width / 2, monitor.x + monitor.width - toolbar.width),
777+
);
778+
let desiredY = y - toolbar.height - margin;
779+
if (desiredY < monitor.y) desiredY = y + height + margin;
780+
desiredY = Math.max(monitor.y, Math.min(desiredY, monitor.y + monitor.height - toolbar.height));
781+
toolbar.translation_x = Math.round(desiredX - anchorX);
782+
toolbar.translation_y = Math.round(desiredY - anchorY);
700783
}
701784

702785
private _setControlsOpacity(opacity: number): void {
@@ -742,6 +825,7 @@ export class CaptureTools extends Module {
742825
this._portalMode = false;
743826
this._selectTool('select');
744827
this._resetControlsOpacity();
828+
this._toolbarDraggedByUser = false;
745829
if (this._toolbar) {
746830
this._toolbar.translation_x = 0;
747831
this._toolbar.translation_y = 0;

src/capture/screenshotCapture.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ async function compositeAnnotations(
223223
container.add_child(baseActor);
224224
container.add_child(overlay);
225225
global.stage.add_child(container);
226+
let texture: Cogl.Texture | null;
226227
try {
227228
global.stage.paint_to_content(
228229
new Mtk.Rectangle({
@@ -235,18 +236,20 @@ async function compositeAnnotations(
235236
null,
236237
Clutter.PaintFlag.NONE,
237238
);
238-
const texture = effect.get_texture();
239+
texture = effect.get_texture();
239240
if (!texture) throw new Error('Failed to render the annotated screenshot texture');
240-
return await compositeTarget({
241-
texture,
242-
geometry: null,
243-
logicalOrigin: { x: 0, y: 0 },
244-
scale: 1,
245-
cursor: { texture: null, x: 0, y: 0, scale: 1 },
246-
});
247241
} finally {
242+
// Destroy synchronously so the container never reaches a presented frame;
243+
// the grabbed Cogl texture stays alive through its own reference.
248244
container.destroy();
249245
}
246+
return compositeTarget({
247+
texture,
248+
geometry: null,
249+
logicalOrigin: { x: 0, y: 0 },
250+
scale: 1,
251+
cursor: { texture: null, x: 0, y: 0, scale: 1 },
252+
});
250253
}
251254

252255
function saveScreenshot(bytes: GLib.Bytes): Gio.File | null {

0 commit comments

Comments
 (0)