Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,26 @@
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.osgi.service.event.EventHandler;

import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants;
import com.microsoft.copilot.eclipse.ui.swt.CssConstants;
import com.microsoft.copilot.eclipse.ui.swt.SpinnerAnimator;
import com.microsoft.copilot.eclipse.ui.utils.AccessibilityUtils;
import com.microsoft.copilot.eclipse.ui.utils.UiUtils;

/**
* A label with icon that displays the running status of the agent.
*/
public class AgentStatusLabel extends Composite {
private static final int TOTAL_FRAMES = 8; // Adjust based on actual number of spinner images

private Image runningIcon;
private Image completedIcon;
private Image cancelledIcon;
private Label iconLabel;
private ChatMarkupViewer textLabel;
private int currentFrame = 1;
private Runnable animationRunnable;
private SpinnerAnimator spinner;
private Status status;
private EventHandler cancelStatusHandler;
private IEventBroker eventBroker;
Expand All @@ -47,14 +43,12 @@ public class AgentStatusLabel extends Composite {
public AgentStatusLabel(Composite parent, int style) {
super(parent, style);
GridLayout layout = new GridLayout(2, false);
layout.marginWidth = 0;
layout.marginHeight = 0;
layout.horizontalSpacing = 0;
setLayout(layout);
setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
this.addDisposeListener(e -> {
stopAnimation();
if (this.runningIcon != null && !this.runningIcon.isDisposed()) {
this.runningIcon.dispose();
}
if (this.completedIcon != null && !this.completedIcon.isDisposed()) {
this.completedIcon.dispose();
}
Expand All @@ -66,6 +60,7 @@ public AgentStatusLabel(Composite parent, int style) {
}
});
iconLabel = new Label(this, SWT.LEFT);
spinner = new SpinnerAnimator(iconLabel);

this.status = Status.RUNNING;
this.cancelStatusHandler = new EventHandler() {
Expand All @@ -84,7 +79,7 @@ public void handleEvent(org.osgi.service.event.Event event) {
* @param statusText the text to display when the agent is completed
*/
public void setCompletedStatus(String statusText) {
stopAnimation();
spinner.stop();

if (this.completedIcon == null) {
this.completedIcon = UiUtils.buildImageFromPngPath("/icons/complete_status.png");
Expand All @@ -101,11 +96,7 @@ public void setCompletedStatus(String statusText) {
* @param statusText the text to display when the agent is running
*/
public void setRunningStatus(String statusText) {
// Stop any existing animation
stopAnimation();

// Start new animation
startAnimation();
spinner.start();

setText(statusText);
this.status = Status.RUNNING;
Expand All @@ -116,7 +107,7 @@ public void setRunningStatus(String statusText) {
*/
public void setErrorStatus() {
if (this.status == Status.RUNNING) {
stopAnimation();
spinner.stop();
}
iconLabel.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_ERROR_TSK));
this.status = Status.ERROR;
Expand All @@ -127,7 +118,7 @@ public void setErrorStatus() {
*/
public void setCancelledStatus() {
if (this.status == Status.RUNNING) {
stopAnimation();
spinner.stop();

if (this.cancelledIcon == null) {
this.cancelledIcon = UiUtils.buildImageFromPngPath("/icons/cancel_status.png");
Expand All @@ -138,47 +129,6 @@ public void setCancelledStatus() {
}
}

private void startAnimation() {
final Display display = getDisplay();

animationRunnable = new Runnable() {
@Override
public void run() {
if (isDisposed() || iconLabel.isDisposed()) {
return;
}

// Dispose previous image
if (runningIcon != null && !runningIcon.isDisposed()) {
runningIcon.dispose();
}

// Load the next frame
String imagePath = String.format("/icons/spinner/%d.png", currentFrame);
runningIcon = UiUtils.buildImageFromPngPath(imagePath);
iconLabel.setImage(runningIcon);
// request layout to update the icon, otherwise the scale of the spinner will be wrong
iconLabel.requestLayout();

// Update frame counter
currentFrame = (currentFrame % TOTAL_FRAMES) + 1;

// Schedule next frame
display.timerExec(100, this);
}
};

// Start the animation
display.timerExec(0, animationRunnable);
}

private void stopAnimation() {
if (animationRunnable != null) {
getDisplay().timerExec(-1, animationRunnable);
animationRunnable = null;
}
}

/**
* Set the text to display next to the icon.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.ui.swt;

import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;

import com.microsoft.copilot.eclipse.ui.utils.UiUtils;

/**
* Drives a rotating spinner animation on a target {@link Label}.
*
* <p>The animator owns the lifecycle of the per-frame {@link Image} resources: each new frame is
* loaded, the previous one is disposed, and {@link #stop()} guarantees that the label no longer
* holds a reference to a disposed image. After {@link #stop()} the caller is free to swap in a
* final image (e.g. a "completed" icon) on the same label.
*
* <p>The animator hooks the target label's dispose listener so the animation is cancelled and the
* running frame is freed automatically when the label goes away.
*/
public final class SpinnerAnimator {
/** Total number of frames in the spinner animation under {@code /icons/spinner/}. */
private static final int TOTAL_FRAMES = 8;
/** Per-frame interval in milliseconds. */
private static final int FRAME_INTERVAL_MS = 100;

private final Label target;
private Image currentFrameImage;
private int currentFrame = 1;
private Runnable animationRunnable;

/**
* Create an animator that will rotate spinner frames on the given label.
*
* @param target the label to update with each frame; must not be {@code null}
*/
public SpinnerAnimator(Label target) {
this.target = target;
target.addDisposeListener(e -> stop());
}
Comment thread
ethanyhou marked this conversation as resolved.

/**
* Start (or restart) the animation. Safe to call when already running — the existing animation
* is cancelled first.
*/
public void start() {
if (target.isDisposed()) {
return;
}
stop();
Comment thread
ethanyhou marked this conversation as resolved.
currentFrame = 1;
final Display display = target.getDisplay();
animationRunnable = new Runnable() {
@Override
public void run() {
if (target.isDisposed()) {
return;
}
// Dispose the previous frame before loading the next one.
if (currentFrameImage != null && !currentFrameImage.isDisposed()) {
currentFrameImage.dispose();
}
currentFrameImage = buildFrame(currentFrame);
target.setImage(currentFrameImage);
// Request layout so the icon scale stays correct as frames change.
target.requestLayout();
currentFrame = (currentFrame % TOTAL_FRAMES) + 1;
display.timerExec(FRAME_INTERVAL_MS, this);
}
};
display.timerExec(0, animationRunnable);
}

/**
* Stop the animation and release the frame image. Detaches the image from the target label
* before disposing it so the label never points at a disposed image. Safe to call repeatedly.
*/
public void stop() {
if (animationRunnable != null && !target.isDisposed()) {
target.getDisplay().timerExec(-1, animationRunnable);
}
animationRunnable = null;
// Detach the image from the label before disposing it so the label never points at a
// disposed image. Callers that want a final icon (completed/cancelled/error) set it
// immediately after stop(), avoiding any visible flicker.
if (!target.isDisposed() && target.getImage() == currentFrameImage) {
target.setImage(null);
}
if (currentFrameImage != null && !currentFrameImage.isDisposed()) {
currentFrameImage.dispose();
}
currentFrameImage = null;
}

private static Image buildFrame(int frame) {
return UiUtils.buildImageFromPngPath(String.format("/icons/spinner/%d.png", frame));
}
}
Loading