From 40814401ffdf16d7f5e45c7102881935bda642ef Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Mon, 20 Jul 2026 18:50:44 +0200 Subject: [PATCH 01/19] feat: Add image registries (#364) - Add CopilotImages, a facade for the ui plug-in's images - Add CopilotJobsImages, a facade for the jobs plug-in's images - Improve plug-in instantiation - Add unit tests to ensure the resources can be loaded - Add fragment test bundle com.microsoft.copilot.eclipse.ui.jobs.test Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../.classpath | 11 + .../.project | 34 ++ .../.settings/org.eclipse.jdt.core.prefs | 9 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 15 + .../build.properties | 6 + .../fragment.xml | 5 + .../pom.xml | 54 +++ .../ui/jobs/CopilotJobsImagesTest.java | 41 ++ .../copilot/eclipse/ui/jobs/CopilotJobs.java | 19 + .../eclipse/ui/jobs/CopilotJobsImages.java | 99 +++++ .../copilot/eclipse/ui/CopilotImagesTest.java | 42 ++ .../copilot/eclipse/ui/CopilotUiTests.java | 5 +- .../copilot/eclipse/ui/CopilotImages.java | 358 ++++++++++++++++++ .../copilot/eclipse/ui/CopilotUi.java | 18 +- pom.xml | 1 + 16 files changed, 717 insertions(+), 4 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/.classpath create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/.project create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.jdt.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.m2e.core.prefs create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/build.properties create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/fragment.xml create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/pom.xml create mode 100644 com.microsoft.copilot.eclipse.ui.jobs.test/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImagesTest.java create mode 100644 com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java create mode 100644 com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/.classpath b/com.microsoft.copilot.eclipse.ui.jobs.test/.classpath new file mode 100644 index 000000000..38f401ad4 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/.project b/com.microsoft.copilot.eclipse.ui.jobs.test/.project new file mode 100644 index 000000000..9c18f8af5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/.project @@ -0,0 +1,34 @@ + + + com.microsoft.copilot.eclipse.ui.jobs.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.jdt.core.prefs b/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..62ef3488c --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.m2e.core.prefs b/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 000000000..f897a7f1c --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF new file mode 100644 index 000000000..54f8cf3e6 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: com.microsoft.copilot.eclipse.ui.jobs.test +Bundle-SymbolicName: com.microsoft.copilot.eclipse.ui.jobs.test;singleton:=true +Bundle-Version: 0.20.0.qualifier +Bundle-Vendor: GitHub Copilot +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Fragment-Host: com.microsoft.copilot.eclipse.ui.jobs +Automatic-Module-Name: com.microsoft.copilot.eclipse.ui.jobs.test +Import-Package: org.objenesis;version="[3.4.0,4.0.0)" +Require-Bundle: junit-jupiter-api;bundle-version="5.10.1", + junit-jupiter-params;bundle-version="5.10.1", + org.mockito.mockito-core;bundle-version="5.14.2", + org.mockito.junit-jupiter;bundle-version="5.10.2", + com.microsoft.copilot.eclipse.ui.jobs;bundle-version="0.20.0" diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/build.properties b/com.microsoft.copilot.eclipse.ui.jobs.test/build.properties new file mode 100644 index 000000000..07c97efed --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/build.properties @@ -0,0 +1,6 @@ +source.. = src +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + fragment.xml + diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/fragment.xml b/com.microsoft.copilot.eclipse.ui.jobs.test/fragment.xml new file mode 100644 index 000000000..0606478f5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/fragment.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/pom.xml b/com.microsoft.copilot.eclipse.ui.jobs.test/pom.xml new file mode 100644 index 000000000..7515e98e9 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/pom.xml @@ -0,0 +1,54 @@ + + 4.0.0 + + com.microsoft.copilot.eclipse + github-copilot-for-eclipse + ${copilot-plugin-version} + + com.microsoft.copilot.eclipse.ui.jobs.test + eclipse-test-plugin + ${base.name} :: UI Jobs Tests + + + true + + + + + skip-tests-during-ui-probe + + + probe.script + + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho-version} + + true + + + + + + + + + + + org.eclipse.tycho + tycho-surefire-plugin + ${tycho-version} + + true + false + + + + + \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImagesTest.java b/com.microsoft.copilot.eclipse.ui.jobs.test/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImagesTest.java new file mode 100644 index 000000000..1df825c9b --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImagesTest.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.jobs; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Verifies that every public constant in {@link CopilotJobsImages} refers to a bundle resource + * that actually exists on the classpath. + * + *

New constants are discovered automatically via reflection; no separate list needs to be + * maintained. + */ +class CopilotJobsImagesTest { + + static Stream iconPaths() { + return Stream.of(CopilotJobsImages.class.getDeclaredFields()) + .filter(f -> f.getName().startsWith("IMG_")) + .map(f -> { + try { + return Arguments.of(f.getName(), f.get(null)); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot read constant: " + f.getName(), e); + } + }); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("iconPaths") + void testIconFileExists(String fieldName, String path) { + assertNotNull(CopilotJobsImages.class.getResource("/" + path), + "Bundle resource not found for constant " + fieldName + ": " + path); + } +} diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java index ec7829521..899c455e9 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java @@ -3,10 +3,12 @@ package com.microsoft.copilot.eclipse.ui.jobs; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; @@ -17,11 +19,27 @@ */ public class CopilotJobs extends AbstractUIPlugin { + private static volatile CopilotJobs COPILOT_JOBS_PLUGIN; + + /** Returns the shared plugin instance. */ + public static CopilotJobs getPlugin() { + Assert.isNotNull(COPILOT_JOBS_PLUGIN); + return COPILOT_JOBS_PLUGIN; + } + + @Override + protected void initializeImageRegistry(ImageRegistry registry) { + CopilotJobsImages.initialize(registry); + } + @Override public void start(BundleContext context) throws Exception { super.start(context); + COPILOT_JOBS_PLUGIN = this; + // Explicitly call method from core to ensure core is activated CopilotCore.getPlugin(); + Job initWaitJob = new Job("Waiting for Copilot initialization") { @Override protected IStatus run(IProgressMonitor monitor) { @@ -42,5 +60,6 @@ protected IStatus run(IProgressMonitor monitor) { @Override public void stop(BundleContext context) throws Exception { super.stop(context); + COPILOT_JOBS_PLUGIN = null; } } diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java new file mode 100644 index 000000000..0f46a682d --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui.jobs; + +import java.net.URL; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; + +/** + * Centralized access to all static icons in the Copilot Jobs UI bundle. + * + *

Bundle images are owned by the plugin's {@link ImageRegistry} and disposed automatically when + * the plugin stops. Callers must not dispose images returned by {@link #getImage(String)}. + */ +public final class CopilotJobsImages { + + // Path prefixes + private static final String ICONS_ROOT = "icons/"; + private static final String ICONS_STATUS = ICONS_ROOT + "status/"; + + // Icons + public static final String IMG_REPO = ICONS_ROOT + "repo.png"; + public static final String IMG_INFORMATION = ICONS_ROOT + "information.png"; + public static final String IMG_STATUS_LOADING = ICONS_STATUS + "loading.png"; + public static final String IMG_STATUS_COMPLETE = ICONS_STATUS + "complete.png"; + + private CopilotJobsImages() { + // prevent instantiation + } + + /** + * Returns the plugin's image registry. + */ + static ImageRegistry getImageRegistry() { + return CopilotJobs.getPlugin().getImageRegistry(); + } + + /** + * Registers all static icon descriptors. Called once from + * {@link CopilotJobs#initializeImageRegistry(ImageRegistry)}. + */ + static void initialize(ImageRegistry registry) { + register(registry, IMG_REPO); + register(registry, IMG_INFORMATION); + register(registry, IMG_STATUS_LOADING); + register(registry, IMG_STATUS_COMPLETE); + } + + private static void register(ImageRegistry registry, String path) { + URL url = CopilotJobsImages.class.getResource("/" + path); + ImageDescriptor descriptor = url != null + ? ImageDescriptor.createFromURL(url) : ImageDescriptor.getMissingImageDescriptor(); + registry.put(path, descriptor); + } + + /** + * Returns the image for the given key. The returned image is owned by the plugin registry; + * callers must not dispose it. + * + * @param key one of the {@code IMG_*} constants defined in this class + */ + public static Image getImage(String key) { + return getImageRegistry().get(key); + } + + /** + * Returns the {@link ImageDescriptor} for the given key. Useful where a descriptor is required, + * e.g. for {@code Action.setImageDescriptor()}. + * + * @param key one of the {@code IMG_*} constants defined in this class + */ + public static ImageDescriptor getImageDescriptor(String key) { + return getImageRegistry().getDescriptor(key); + } + + /** + * Convenience access to Eclipse's workbench shared images. + * + * @param imageId a constant from {@link ISharedImages}, e.g. {@link ISharedImages#IMG_OBJS_WARN_TSK} + */ + public static Image getSharedImage(String imageId) { + return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); + } + + /** + * Convenience access to Eclipse's workbench shared image descriptors. + * + * @param imageId a constant from {@link ISharedImages} + */ + public static ImageDescriptor getSharedImageDescriptor(String imageId) { + return PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(imageId); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java new file mode 100644 index 000000000..5fda6d5c0 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Verifies that every public constant in {@link CopilotImages} refers to a bundle resource that + * actually exists on the classpath. + * + *

This test is independent of a running Display or plugin registry — it only needs the bundle's + * classpath. It guards against typos in icon paths and missing files caused by renames or deletions. + * New constants are discovered automatically via reflection; no separate list needs to be maintained. + */ +class CopilotImagesTest { + + static Stream iconPaths() { + return Stream.of(CopilotImages.class.getDeclaredFields()) + .filter(f -> f.getName().startsWith("IMG_")) + .map(f -> { + try { + return Arguments.of(f.getName(), f.get(null)); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot read constant: " + f.getName(), e); + } + }); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("iconPaths") + void testIconFileExists(String fieldName, String path) { + assertNotNull(CopilotImages.class.getResource("/" + path), + "Bundle resource not found for constant " + fieldName + ": " + path); + } +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java index de9f18514..82f8891a8 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotUiTests.java @@ -17,8 +17,9 @@ class CopilotUiTests { @Test void testCopilotCoreWakeUp() throws Exception { - CopilotUi ui = new CopilotUi(); - ui.start(null); + Bundle uiBundle = Platform.getBundle("com.microsoft.copilot.eclipse.ui"); + assertNotNull(uiBundle); + uiBundle.start(); Job.getJobManager().join(CopilotCore.INIT_JOB_FAMILY, null); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java new file mode 100644 index 000000000..100f9e180 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui; + +import java.net.URL; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.graphics.Image; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; + +import com.microsoft.copilot.eclipse.ui.utils.UiUtils; + +/** + * Centralized access to all static icons in the Copilot UI bundle and to Eclipse shared images. + * + *

Bundle images are owned by the plugin's {@link ImageRegistry} and disposed automatically when + * the plugin stops. Callers must not dispose images returned by {@link #getImage}. + */ +public final class CopilotImages { + + // Path prefixes + private static final String ICONS_ROOT = "icons/"; + private static final String ICONS_CHAT = ICONS_ROOT + "chat/"; + private static final String ICONS_DROPDOWN = ICONS_ROOT + "dropdown/"; + private static final String ICONS_JOBS = ICONS_ROOT + "jobs/"; + private static final String ICONS_MCP = ICONS_ROOT + "mcp/"; + private static final String ICONS_QUOTA = ICONS_ROOT + "quota/"; + private static final String ICONS_QUICKSTART = "intro/quickstart/"; + private static final String ICONS_SPINNER = ICONS_ROOT + "spinner/"; + + // ---- Root icons ---------------------------------------------------------- + public static final String IMG_BLANK = ICONS_ROOT + "blank.png"; + public static final String IMG_CANCEL_STATUS = ICONS_ROOT + "cancel_status.png"; + public static final String IMG_CLOSE = ICONS_ROOT + "close.png"; + public static final String IMG_COMPLETE_STATUS = ICONS_ROOT + "complete_status.png"; + public static final String IMG_EDIT_KEYBOARD_SHORTCUTS = ICONS_ROOT + "edit_keyboard_shortcuts.png"; + public static final String IMG_EDIT_PREFERENCES = ICONS_ROOT + "edit_preferences.png"; + public static final String IMG_FEEDBACK_FORUM = ICONS_ROOT + "feedback_forum.png"; + public static final String IMG_GITHUB_COPILOT = ICONS_ROOT + "github_copilot.png"; + public static final String IMG_GITHUB_COPILOT_ERROR = ICONS_ROOT + "github_copilot_error.png"; + public static final String IMG_GITHUB_COPILOT_NOT_AUTHORIZED = ICONS_ROOT + "github_copilot_not_authorized.png"; + public static final String IMG_GITHUB_COPILOT_NOT_SIGNED_IN = ICONS_ROOT + "github_copilot_not_signed_in.png"; + public static final String IMG_GITHUB_COPILOT_SIGNED_IN = ICONS_ROOT + "github_copilot_signed_in.png"; + public static final String IMG_INFORMATION = ICONS_ROOT + "information.png"; + public static final String IMG_SIGNIN = ICONS_ROOT + "signin.png"; + public static final String IMG_SIGNOUT = ICONS_ROOT + "signout.png"; + + // ---- Chat icons ---------------------------------------------------------- + public static final String IMG_CHAT_ATTACH_CONTEXT = ICONS_CHAT + "attach_context.png"; + public static final String IMG_CHAT_BACK_ARROW = ICONS_CHAT + "back_arrow.png"; + public static final String IMG_CHAT_BACK_ARROW_GREY = ICONS_CHAT + "back_arrow_grey.png"; + public static final String IMG_CHAT_BREAKPOINT_AUTO = ICONS_CHAT + "breakpoint_auto.png"; + public static final String IMG_CHAT_BREAKPOINT_AUTO_DISABLED = ICONS_CHAT + "breakpoint_auto_disabled.png"; + public static final String IMG_CHAT_HISTORY_EDIT = ICONS_CHAT + "chat_history_edit.png"; + public static final String IMG_CHAT_COPILOT_AVATAR = ICONS_CHAT + "chat_message_copilot_avatar.png"; + public static final String IMG_CHAT_USER_AVATAR = ICONS_CHAT + "chat_message_user_avatar.png"; + public static final String IMG_CHATVIEW_ICON_CHAT = ICONS_CHAT + "chatview_icon_chat.png"; + public static final String IMG_CHATVIEW_ICON_CODE = ICONS_CHAT + "chatview_icon_code.png"; + public static final String IMG_CHATVIEW_ICON_LOADING = ICONS_CHAT + "chatview_icon_loading.png"; + public static final String IMG_CHATVIEW_ICON_NOT_AUTHORIZED = ICONS_CHAT + "chatview_icon_not_authorized.png"; + public static final String IMG_CHATVIEW_ICON_WELCOME = ICONS_CHAT + "chatview_icon_welcome.png"; + public static final String IMG_CLEAR_TODO = ICONS_CHAT + "clear_todo.png"; + public static final String IMG_CLEAR_TODO_DISABLED = ICONS_CHAT + "clear_todo_disable.png"; + public static final String IMG_CHAT_DOWN_ARROW = ICONS_CHAT + "down_arrow.png"; + public static final String IMG_CHAT_ENTER = ICONS_CHAT + "enter.png"; + public static final String IMG_CHAT_EYE = ICONS_CHAT + "eye.png"; + public static final String IMG_CHAT_EYE_DARK = ICONS_CHAT + "eye_dark.png"; + public static final String IMG_CHAT_EYE_CLOSED = ICONS_CHAT + "eye_closed.png"; + public static final String IMG_CHAT_EYE_CLOSED_DARK = ICONS_CHAT + "eye_closed_dark.png"; + public static final String IMG_CHAT_GUTTER_ARROW = ICONS_CHAT + "gutter-arrow.png"; + public static final String IMG_CHAT_KEEP = ICONS_CHAT + "keep.png"; + public static final String IMG_CHAT_KEYBOARD_TAB = ICONS_CHAT + "keyboard-tab.png"; + public static final String IMG_CHAT_RED_NOTICE = ICONS_CHAT + "red_notice.png"; + public static final String IMG_CHAT_RIGHT_ARROW = ICONS_CHAT + "right_arrow.png"; + public static final String IMG_CHAT_SEND = ICONS_CHAT + "send.png"; + public static final String IMG_CHAT_SEND_DISABLED = ICONS_CHAT + "send_disabled.png"; + public static final String IMG_CHAT_SEND_TO_JOB = ICONS_CHAT + "send_to_job.png"; + public static final String IMG_CHAT_SEND_TO_JOB_DISABLED = ICONS_CHAT + "send_to_job_disabled.png"; + public static final String IMG_CHAT_TOOLS = ICONS_CHAT + "tools.png"; + public static final String IMG_CHAT_TOOLS_DETECTED = ICONS_CHAT + "tools_detected.png"; + public static final String IMG_CHAT_TOOLS_DISABLED = ICONS_CHAT + "tools_disabled.png"; + + // ---- Todo icons ---------------------------------------------------------- + public static final String IMG_TODOS_FINISH = ICONS_CHAT + "todos_finish.png"; + public static final String IMG_TODOS_FINISH_DARK = ICONS_CHAT + "todos_finish_dark.png"; + public static final String IMG_TODOS_RUNNING = ICONS_CHAT + "todos_running.png"; + public static final String IMG_TODOS_RUNNING_DARK = ICONS_CHAT + "todos_running_dark.png"; + public static final String IMG_TODOS_WAITING = ICONS_CHAT + "todos_waiting.png"; + public static final String IMG_TODOS_WAITING_DARK = ICONS_CHAT + "todos_waiting_dark.png"; + + // ---- Dropdown icons ------------------------------------------------------ + public static final String IMG_DROPDOWN_DOWN_ARROW = ICONS_DROPDOWN + "down_arrow.png"; + public static final String IMG_DROPDOWN_DOWN_ARROW_DARK = ICONS_DROPDOWN + "down_arrow_dark.png"; + public static final String IMG_DROPDOWN_COMPLETE_STATUS = ICONS_DROPDOWN + "dropdown_complete_status.png"; + public static final String IMG_DROPDOWN_COMPLETE_STATUS_DARK = ICONS_DROPDOWN + "dropdown_complete_status_dark.png"; + public static final String IMG_DROPDOWN_WARNING = ICONS_DROPDOWN + "dropdown_warning.png"; + public static final String IMG_DROPDOWN_WARNING_DARK = ICONS_DROPDOWN + "dropdown_warning_dark.png"; + + // ---- Jobs pull-request icons (used from UI bundle Java code) ------------- + public static final String IMG_JOBS_PULL_REQUEST_BLACK = ICONS_JOBS + "pull_request_black.png"; + public static final String IMG_JOBS_PULL_REQUEST_WHITE = ICONS_JOBS + "pull_request_white.png"; + + // ---- MCP icons ----------------------------------------------------------- + public static final String IMG_MCP_DEFAULT_ICON = ICONS_MCP + "mcp_default_icon.png"; + public static final String IMG_MCP_MARKETPLACE_ICON = ICONS_MCP + "mcp_marketplace_icon.png"; + public static final String IMG_MCP_REGISTRY = ICONS_MCP + "mcp_registry.png"; + public static final String IMG_MCP_REFRESH = ICONS_MCP + "refresh.png"; + public static final String IMG_MCP_HISTORY = ICONS_MCP + "history.png"; + public static final String IMG_MCP_HISTORY_DARK = ICONS_MCP + "history_dark.png"; + public static final String IMG_MCP_REPOSITORY = ICONS_MCP + "repository.png"; + public static final String IMG_MCP_REPOSITORY_DARK = ICONS_MCP + "repository_dark.png"; + public static final String IMG_MCP_UPDATE = ICONS_MCP + "update.png"; + public static final String IMG_MCP_UPDATE_DARK = ICONS_MCP + "update_dark.png"; + public static final String IMG_MCP_VERSIONS = ICONS_MCP + "versions.png"; + public static final String IMG_MCP_VERSIONS_DARK = ICONS_MCP + "versions_dark.png"; + + // ---- Quota icons --------------------------------------------------------- + public static final String IMG_QUOTA_UPGRADE = ICONS_QUOTA + "upgrade.png"; + public static final String IMG_QUOTA_USAGE_BLUE = ICONS_QUOTA + "usage_blue.png"; + public static final String IMG_QUOTA_USAGE_RED = ICONS_QUOTA + "usage_red.png"; + public static final String IMG_QUOTA_USAGE_YELLOW = ICONS_QUOTA + "usage_yellow.png"; + + // ---- Quick Start icons --------------------------------------------------- + public static final String IMG_QUICKSTART_AGENT = ICONS_QUICKSTART + "quick_start_agent.png"; + public static final String IMG_QUICKSTART_ASK = ICONS_QUICKSTART + "quick_start_ask.png"; + public static final String IMG_QUICKSTART_COMPLETION = ICONS_QUICKSTART + "quick_start_completion.png"; + public static final String IMG_QUICKSTART_CLOSE_LIGHT = ICONS_QUICKSTART + "close_light.png"; + public static final String IMG_QUICKSTART_CLOSE_DARK = ICONS_QUICKSTART + "close_dark.png"; + public static final String IMG_QUICKSTART_CLOSE_HOVER_LIGHT = ICONS_QUICKSTART + "close_hover_light.png"; + public static final String IMG_QUICKSTART_CLOSE_HOVER_DARK = ICONS_QUICKSTART + "close_hover_dark.png"; + + // ---- Spinner frames (1-based index, see getSpinnerFrame) ----------------- + public static final String IMG_SPINNER_1 = ICONS_SPINNER + "1.png"; + public static final String IMG_SPINNER_2 = ICONS_SPINNER + "2.png"; + public static final String IMG_SPINNER_3 = ICONS_SPINNER + "3.png"; + public static final String IMG_SPINNER_4 = ICONS_SPINNER + "4.png"; + public static final String IMG_SPINNER_5 = ICONS_SPINNER + "5.png"; + public static final String IMG_SPINNER_6 = ICONS_SPINNER + "6.png"; + public static final String IMG_SPINNER_7 = ICONS_SPINNER + "7.png"; + public static final String IMG_SPINNER_8 = ICONS_SPINNER + "8.png"; + + public static final String[] SPINNER_FRAMES = { + IMG_SPINNER_1, IMG_SPINNER_2, IMG_SPINNER_3, IMG_SPINNER_4, + IMG_SPINNER_5, IMG_SPINNER_6, IMG_SPINNER_7, IMG_SPINNER_8 + }; + + private CopilotImages() { + // prevent instantiation + } + + /** + * Returns the plugin's image registry. + */ + static ImageRegistry getImageRegistry() { + return CopilotUi.getPlugin().getImageRegistry(); + } + + /** + * Registers all static icon descriptors. Called once from + * {@link CopilotUi#initializeImageRegistry(ImageRegistry)}. + */ + static void initialize(ImageRegistry registry) { + register(registry, IMG_BLANK); + register(registry, IMG_CANCEL_STATUS); + register(registry, IMG_CLOSE); + register(registry, IMG_COMPLETE_STATUS); + register(registry, IMG_EDIT_KEYBOARD_SHORTCUTS); + register(registry, IMG_EDIT_PREFERENCES); + register(registry, IMG_FEEDBACK_FORUM); + register(registry, IMG_GITHUB_COPILOT); + register(registry, IMG_GITHUB_COPILOT_ERROR); + register(registry, IMG_GITHUB_COPILOT_NOT_AUTHORIZED); + register(registry, IMG_GITHUB_COPILOT_NOT_SIGNED_IN); + register(registry, IMG_GITHUB_COPILOT_SIGNED_IN); + register(registry, IMG_INFORMATION); + register(registry, IMG_SIGNIN); + register(registry, IMG_SIGNOUT); + register(registry, IMG_CHAT_ATTACH_CONTEXT); + register(registry, IMG_CHAT_BACK_ARROW); + register(registry, IMG_CHAT_BACK_ARROW_GREY); + register(registry, IMG_CHAT_BREAKPOINT_AUTO); + register(registry, IMG_CHAT_BREAKPOINT_AUTO_DISABLED); + register(registry, IMG_CHAT_HISTORY_EDIT); + register(registry, IMG_CHAT_COPILOT_AVATAR); + register(registry, IMG_CHAT_USER_AVATAR); + register(registry, IMG_CHATVIEW_ICON_CHAT); + register(registry, IMG_CHATVIEW_ICON_CODE); + register(registry, IMG_CHATVIEW_ICON_LOADING); + register(registry, IMG_CHATVIEW_ICON_NOT_AUTHORIZED); + register(registry, IMG_CHATVIEW_ICON_WELCOME); + register(registry, IMG_CLEAR_TODO); + register(registry, IMG_CLEAR_TODO_DISABLED); + register(registry, IMG_CHAT_DOWN_ARROW); + register(registry, IMG_CHAT_ENTER); + register(registry, IMG_CHAT_EYE); + register(registry, IMG_CHAT_EYE_DARK); + register(registry, IMG_CHAT_EYE_CLOSED); + register(registry, IMG_CHAT_EYE_CLOSED_DARK); + register(registry, IMG_CHAT_GUTTER_ARROW); + register(registry, IMG_CHAT_KEEP); + register(registry, IMG_CHAT_KEYBOARD_TAB); + register(registry, IMG_CHAT_RED_NOTICE); + register(registry, IMG_CHAT_RIGHT_ARROW); + register(registry, IMG_CHAT_SEND); + register(registry, IMG_CHAT_SEND_DISABLED); + register(registry, IMG_CHAT_SEND_TO_JOB); + register(registry, IMG_CHAT_SEND_TO_JOB_DISABLED); + register(registry, IMG_CHAT_TOOLS); + register(registry, IMG_CHAT_TOOLS_DETECTED); + register(registry, IMG_CHAT_TOOLS_DISABLED); + register(registry, IMG_TODOS_FINISH); + register(registry, IMG_TODOS_FINISH_DARK); + register(registry, IMG_TODOS_RUNNING); + register(registry, IMG_TODOS_RUNNING_DARK); + register(registry, IMG_TODOS_WAITING); + register(registry, IMG_TODOS_WAITING_DARK); + register(registry, IMG_DROPDOWN_DOWN_ARROW); + register(registry, IMG_DROPDOWN_DOWN_ARROW_DARK); + register(registry, IMG_DROPDOWN_COMPLETE_STATUS); + register(registry, IMG_DROPDOWN_COMPLETE_STATUS_DARK); + register(registry, IMG_DROPDOWN_WARNING); + register(registry, IMG_DROPDOWN_WARNING_DARK); + register(registry, IMG_JOBS_PULL_REQUEST_BLACK); + register(registry, IMG_JOBS_PULL_REQUEST_WHITE); + register(registry, IMG_MCP_DEFAULT_ICON); + register(registry, IMG_MCP_MARKETPLACE_ICON); + register(registry, IMG_MCP_REGISTRY); + register(registry, IMG_MCP_REFRESH); + register(registry, IMG_MCP_HISTORY); + register(registry, IMG_MCP_HISTORY_DARK); + register(registry, IMG_MCP_REPOSITORY); + register(registry, IMG_MCP_REPOSITORY_DARK); + register(registry, IMG_MCP_UPDATE); + register(registry, IMG_MCP_UPDATE_DARK); + register(registry, IMG_MCP_VERSIONS); + register(registry, IMG_MCP_VERSIONS_DARK); + register(registry, IMG_QUOTA_UPGRADE); + register(registry, IMG_QUOTA_USAGE_BLUE); + register(registry, IMG_QUOTA_USAGE_RED); + register(registry, IMG_QUOTA_USAGE_YELLOW); + register(registry, IMG_QUICKSTART_AGENT); + register(registry, IMG_QUICKSTART_ASK); + register(registry, IMG_QUICKSTART_COMPLETION); + register(registry, IMG_QUICKSTART_CLOSE_LIGHT); + register(registry, IMG_QUICKSTART_CLOSE_DARK); + register(registry, IMG_QUICKSTART_CLOSE_HOVER_LIGHT); + register(registry, IMG_QUICKSTART_CLOSE_HOVER_DARK); + register(registry, IMG_SPINNER_1); + register(registry, IMG_SPINNER_2); + register(registry, IMG_SPINNER_3); + register(registry, IMG_SPINNER_4); + register(registry, IMG_SPINNER_5); + register(registry, IMG_SPINNER_6); + register(registry, IMG_SPINNER_7); + register(registry, IMG_SPINNER_8); + } + + private static void register(ImageRegistry registry, String path) { + URL url = CopilotImages.class.getResource("/" + path); + ImageDescriptor descriptor = url != null + ? ImageDescriptor.createFromURL(url) : ImageDescriptor.getMissingImageDescriptor(); + registry.put(path, descriptor); + } + + /** + * Returns the image for the given key. The returned image is owned by the plugin registry; + * callers must not dispose it. + * + * @param key one of the {@code IMG_*} constants defined in this class + */ + public static Image getImage(String key) { + return getImageRegistry().get(key); + } + + /** + * Returns the {@link ImageDescriptor} for the given key. Useful where a descriptor is required, + * e.g. for {@code Action.setImageDescriptor()}. + * + * @param key one of the {@code IMG_*} constants defined in this class + */ + public static ImageDescriptor getImageDescriptor(String key) { + return getImageRegistry().getDescriptor(key); + } + + /** + * Returns the theme-correct image for icons that come in light/dark variants. + * Uses {@link UiUtils#isDarkTheme()} to pick the right key. + * + *

Does not track live theme changes; callers re-render on theme-change events as needed. + * + * @param lightKey key to use in light theme + * @param darkKey key to use in dark theme + */ + public static Image getThemedImage(String lightKey, String darkKey) { + return getImage(UiUtils.isDarkTheme() ? darkKey : lightKey); + } + + /** + * Returns the theme-correct descriptor for icons that come in light/dark variants. + * + * @param lightKey key to use in light theme + * @param darkKey key to use in dark theme + * @see #getThemedImage(String, String) + */ + public static ImageDescriptor getThemedImageDescriptor(String lightKey, String darkKey) { + return getImageDescriptor(UiUtils.isDarkTheme() ? darkKey : lightKey); + } + + private static String getSpinnerFrameImageKey(int frame) { + Assert.isLegal(frame >= 1 && frame <= SPINNER_FRAMES.length, + "Spinner frame must be in [1, " + SPINNER_FRAMES.length + "], got: " + frame); + return SPINNER_FRAMES[frame - 1]; + } + + /** + * Returns the image for the given 1-based spinner frame (1–8). + * + * @param frame 1-based index of the spinner frame + * @throws IllegalArgumentException if {@code frame} is outside [1, 8] + */ + public static Image getSpinnerFrame(int frame) { + return getImage(getSpinnerFrameImageKey(frame)); + } + + /** + * Returns the image descriptor for the given 1-based spinner frame (1–8). + * + * @param frame 1-based index of the spinner frame + * @return the image descriptor for the requested spinner frame + * @throws IllegalArgumentException if {@code frame} is outside [1, 8] + */ + public static ImageDescriptor getSpinnerFrameDescriptor(int frame) { + return getImageDescriptor(getSpinnerFrameImageKey(frame)); + } + + /** + * Convenience access to Eclipse's workbench shared images. + * + * @param imageId a constant from {@link ISharedImages}, e.g. {@link ISharedImages#IMG_OBJS_ERROR_TSK} + */ + public static Image getSharedImage(String imageId) { + return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); + } + + /** + * Convenience access to Eclipse's workbench shared image descriptors. + * + * @param imageId a constant from {@link ISharedImages} + */ + public static ImageDescriptor getSharedImageDescriptor(String imageId) { + return PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(imageId); + } + +} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java index 66c472b43..869d09747 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotUi.java @@ -7,6 +7,7 @@ import java.util.concurrent.ExecutionException; import org.eclipse.core.net.proxy.IProxyService; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; @@ -18,6 +19,7 @@ import org.eclipse.core.runtime.preferences.ConfigurationScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; @@ -42,7 +44,7 @@ */ public class CopilotUi extends AbstractUIPlugin { - private static CopilotUi COPILOT_UI_PLUGIN = null; + private static volatile CopilotUi COPILOT_UI_PLUGIN; private CopilotStatusManager copilotStatusManager; private EditorLifecycleListener editorLifecycleListener; @@ -58,15 +60,24 @@ public class CopilotUi extends AbstractUIPlugin { */ public CopilotUi() { super(); - COPILOT_UI_PLUGIN = this; } + /** Returns the shared plugin instance. */ public static CopilotUi getPlugin() { + Assert.isNotNull(COPILOT_UI_PLUGIN); return COPILOT_UI_PLUGIN; } + @Override + protected void initializeImageRegistry(ImageRegistry registry) { + CopilotImages.initialize(registry); + } + @Override public void start(BundleContext context) throws Exception { + super.start(context); + COPILOT_UI_PLUGIN = this; + // Explicitly call method from core to ensure core is activated CopilotCore.getPlugin(); @@ -226,6 +237,9 @@ public void stop(BundleContext context) throws Exception { if (this.chatServiceManager != null) { this.chatServiceManager.dispose(); } + + super.stop(context); + COPILOT_UI_PLUGIN = null; } public EditorsManager getEditorsManager() { diff --git a/pom.xml b/pom.xml index 2a9b33d62..ae073c4a9 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ com.microsoft.copilot.eclipse.core.test com.microsoft.copilot.eclipse.ui.test + com.microsoft.copilot.eclipse.ui.jobs.test com.microsoft.copilot.eclipse.swtbot.test From cd4a0308db0dbac4797f69fe00e1b7898702242a Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Tue, 21 Jul 2026 11:25:33 +0200 Subject: [PATCH 02/19] Migrate image loading and disposal to using image registries (#364) Use image registries to access static images, no longer dispose them, since the registries handle Image disposals. Keep dynamic image loading and disposal as it was. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../eclipse/ui/jobs/views/JobsView.java | 34 ++-------- .../copilot/eclipse/ui/chat/ActionBar.java | 60 ++++++----------- .../eclipse/ui/chat/AddContextButton.java | 8 +-- .../eclipse/ui/chat/AgentMessageWidget.java | 10 +-- .../eclipse/ui/chat/AgentStatusLabel.java | 12 +--- .../eclipse/ui/chat/AgentToolCancelLabel.java | 10 +-- .../ui/chat/CurrentReferencedFile.java | 10 +-- .../ui/chat/ModelPickerGroupsBuilder.java | 23 +------ .../eclipse/ui/chat/ReferencedFile.java | 9 +-- .../eclipse/ui/chat/ThinkingBlock.java | 28 ++------ .../copilot/eclipse/ui/chat/TodoListBar.java | 58 ++++------------ .../eclipse/ui/chat/WorkingSetBar.java | 21 ++---- .../ui/chat/dialogs/DynamicOauthDialog.java | 8 +-- .../ui/chat/services/AvatarService.java | 10 +-- .../chat/tools/RunInTerminalToolAdapter.java | 4 +- .../chat/viewers/AfterLoginWelcomeViewer.java | 16 +---- .../ui/chat/viewers/AgentModeViewer.java | 23 ++----- .../viewers/BeforeLoginWelcomeViewer.java | 38 ++--------- .../ui/chat/viewers/ChatHistoryViewer.java | 20 ++---- .../ui/chat/viewers/LoadingViewer.java | 7 +- .../ui/chat/viewers/NoSubscriptionViewer.java | 7 +- .../MissingTerminalDependenciesDialog.java | 9 +-- .../ui/dialogs/jobs/BaseCopilotDialog.java | 8 +-- .../ui/dialogs/mcp/McpApprovalDialog.java | 9 +-- .../ui/dialogs/mcp/McpRegistryDialog.java | 29 ++------ .../ui/dialogs/mcp/McpServerDetailDialog.java | 38 ++++------- .../eclipse/ui/dialogs/mcp/McpServerItem.java | 25 +++---- .../ui/handlers/ShowContextMenuHandler.java | 6 +- .../ui/handlers/ShowMenuBarMenuHandler.java | 22 +++---- .../ui/handlers/ShowStatusBarMenuHandler.java | 47 +++++++------ .../ToolConfigurationContextMenuHandler.java | 5 +- .../copilot/eclipse/ui/nes/ActionMenu.java | 6 +- .../copilot/eclipse/ui/nes/RulerColumn.java | 3 +- .../ui/preferences/AddApiKeyDialog.java | 16 ++--- .../ui/preferences/AddByokModelDialog.java | 16 ++--- .../ui/preferences/ByokPreferencePage.java | 11 +--- .../ui/preferences/McpPreferencePage.java | 16 +---- .../eclipse/ui/quickstart/FeaturePage.java | 66 +++++-------------- .../eclipse/ui/swt/DropdownButton.java | 27 ++------ .../copilot/eclipse/ui/swt/DropdownPopup.java | 23 ++----- .../ui/swt/ModelHoverContentProvider.java | 26 ++------ .../eclipse/ui/swt/SpinnerAnimator.java | 15 ++--- .../copilot/eclipse/ui/utils/McpUtils.java | 7 +- .../copilot/eclipse/ui/utils/MenuUtils.java | 9 +-- 44 files changed, 229 insertions(+), 626 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java index d1ceb35a3..e5774d3a5 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java @@ -50,9 +50,9 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.githubapi.SearchPrParams; import com.microsoft.copilot.eclipse.core.lsp.protocol.githubapi.SearchPrResponse; import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; +import com.microsoft.copilot.eclipse.ui.jobs.CopilotJobsImages; import com.microsoft.copilot.eclipse.ui.jobs.events.JobsViewEvents; import com.microsoft.copilot.eclipse.ui.jobs.i18n.Messages; -import com.microsoft.copilot.eclipse.ui.jobs.utils.UiUtils; /** * The view to display coding agent jobs with PRs from GitHub. Shows a 2-level @@ -83,8 +83,8 @@ public class JobsView { public void createPartControl(Composite parent) { parent.setLayout(new GridLayout(1, false)); - this.directoryIcon = UiUtils.buildImageFromPngPath("/icons/repo.png"); - this.informationIcon = UiUtils.buildImageFromPngPath("/icons/information.png"); + this.directoryIcon = CopilotJobsImages.getImage(CopilotJobsImages.IMG_REPO); + this.informationIcon = CopilotJobsImages.getImage(CopilotJobsImages.IMG_INFORMATION); treeViewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER); treeViewer.getTree().setLayoutData(new GridData(GridData.FILL_BOTH)); @@ -108,19 +108,6 @@ public void createPartControl(Composite parent) { } }); - // Add dispose listener to clean up images - treeViewer.getTree().addDisposeListener(e -> { - if (directoryIcon != null && !directoryIcon.isDisposed()) { - directoryIcon.dispose(); - } - if (informationIcon != null && !informationIcon.isDisposed()) { - informationIcon.dispose(); - } - if (treeViewer.getLabelProvider() instanceof PullRequestLabelProvider labelProvider) { - labelProvider.disposeImages(); - } - }); - treeViewer.getTree().addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { @@ -572,7 +559,7 @@ public Image getImage(Object element) { private Image getStatusIconForLabel(String nodeText) { if (Messages.jobsView_label_loadingAgentJobs.equals(nodeText)) { if (this.loadingIcon == null) { - loadingIcon = UiUtils.buildImageFromPngPath("/icons/status/loading.png"); + loadingIcon = CopilotJobsImages.getImage(CopilotJobsImages.IMG_STATUS_LOADING); } return this.loadingIcon; } else if (SPECIAL_STATUS_LABELS.contains(nodeText)) { @@ -593,7 +580,7 @@ private Image getStatusIconForPr(GitHubPullRequestItem pr) { switch (pr.copilotWorkStatus()) { case done: if (this.completeIcon == null) { - completeIcon = UiUtils.buildImageFromPngPath("/icons/status/complete.png"); + completeIcon = CopilotJobsImages.getImage(CopilotJobsImages.IMG_STATUS_COMPLETE); } return this.completeIcon; case error: @@ -601,20 +588,11 @@ private Image getStatusIconForPr(GitHubPullRequestItem pr) { case in_progress: default: if (this.loadingIcon == null) { - loadingIcon = UiUtils.buildImageFromPngPath("/icons/status/loading.png"); + loadingIcon = CopilotJobsImages.getImage(CopilotJobsImages.IMG_STATUS_LOADING); } return this.loadingIcon; } } - public void disposeImages() { - if (loadingIcon != null && !loadingIcon.isDisposed()) { - loadingIcon.dispose(); - } - if (completeIcon != null && !completeIcon.isDisposed()) { - completeIcon.dispose(); - } - } } - } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java index 9cd8db3bc..64555a886 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java @@ -70,6 +70,7 @@ import com.microsoft.copilot.eclipse.core.utils.FileUtils; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.chat.contextwindow.ContextSizeDonut; @@ -336,21 +337,6 @@ private void updateTableLayout(Table table) { // Update send to job button and send button together updateButtonsLayout(); - - this.addDisposeListener(e -> { - if (mcpToolImage != null && !mcpToolImage.isDisposed()) { - mcpToolImage.dispose(); - } - if (mcpToolDisabledImage != null && !mcpToolDisabledImage.isDisposed()) { - mcpToolDisabledImage.dispose(); - } - if (mcpToolDetectedImage != null && !mcpToolDetectedImage.isDisposed()) { - mcpToolDetectedImage.dispose(); - } - if (redNoticeImage != null && !redNoticeImage.isDisposed()) { - redNoticeImage.dispose(); - } - }); } /** @@ -437,11 +423,11 @@ public void updateButtonsLayout() { // Add sendToJob button - only visible if clientPreviewFeaturesEnabled if (clientPreviewFeaturesEnabled) { - if (sendToJobImage == null || sendToJobImage.isDisposed()) { - sendToJobImage = UiUtils.buildImageFromPngPath("/icons/chat/send_to_job.png"); + if (sendToJobImage == null) { + sendToJobImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND_TO_JOB); } - if (sendToJobDisabledImage == null || sendToJobDisabledImage.isDisposed()) { - sendToJobDisabledImage = UiUtils.buildImageFromPngPath("/icons/chat/send_to_job_disabled.png"); + if (sendToJobDisabledImage == null) { + sendToJobDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND_TO_JOB_DISABLED); } this.sendToJobButton = UiUtils.createIconButton(this.bottomRightButtonsComposite, SWT.PUSH | SWT.FLAT); @@ -467,8 +453,8 @@ public void widgetSelected(SelectionEvent e) { // Add toggle button for all modes if it has not been created if (btnMsgToggle == null || btnMsgToggle.isDisposed()) { - this.sendImage = UiUtils.buildImageFromPngPath("/icons/chat/send.png"); - this.sendDisabledImage = UiUtils.buildImageFromPngPath("/icons/chat/send_disabled.png"); + this.sendImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND); + this.sendDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_SEND_DISABLED); this.btnMsgToggle = UiUtils.createIconButton(bottomRightButtonsComposite, SWT.PUSH | SWT.FLAT); boolean isEnabled = !StringUtils.isBlank(this.inputTextViewer.getContent()); this.btnMsgToggle.setEnabled(isEnabled); @@ -488,14 +474,6 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent e) { } } }); - this.btnMsgToggle.addDisposeListener(e -> { - if (sendImage != null && !sendImage.isDisposed()) { - sendImage.dispose(); - } - if (sendDisabledImage != null && !sendDisabledImage.isDisposed()) { - sendDisabledImage.dispose(); - } - }); AccessibilityUtils.addAccessibilityNameForUiComponent(this.btnMsgToggle, Messages.chat_actionBar_sendButton_Tooltip); } @@ -539,14 +517,14 @@ private void setUpChatModePicker(Composite parent) { } private void setUpMcpToolButtonInControlBar(Composite parent) { - if (mcpToolImage == null || mcpToolImage.isDisposed()) { - mcpToolImage = UiUtils.buildImageFromPngPath("/icons/chat/tools.png"); + if (mcpToolImage == null) { + mcpToolImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_TOOLS); } - if (mcpToolDisabledImage == null || mcpToolDisabledImage.isDisposed()) { - mcpToolDisabledImage = UiUtils.buildImageFromPngPath("/icons/chat/tools_disabled.png"); + if (mcpToolDisabledImage == null) { + mcpToolDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_TOOLS_DISABLED); } - if (mcpToolDetectedImage == null || mcpToolDetectedImage.isDisposed()) { - mcpToolDetectedImage = UiUtils.buildImageFromPngPath("/icons/chat/tools_detected.png"); + if (mcpToolDetectedImage == null) { + mcpToolDetectedImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_TOOLS_DETECTED); } this.mcpToolButton = UiUtils.createIconButton(parent, SWT.PUSH | SWT.FLAT); @@ -588,11 +566,11 @@ private void setUpContextSizeDonutInControlBar(Composite parent) { } private void setUpAutoBreakpointButtonInControlBar(Composite parent) { - if (autoBreakpointImage == null || autoBreakpointImage.isDisposed()) { - autoBreakpointImage = UiUtils.buildImageFromPngPath("/icons/chat/breakpoint_auto.png"); + if (autoBreakpointImage == null) { + autoBreakpointImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_BREAKPOINT_AUTO); } - if (autoBreakpointDisabledImage == null || autoBreakpointDisabledImage.isDisposed()) { - autoBreakpointDisabledImage = UiUtils.buildImageFromPngPath("/icons/chat/breakpoint_auto_disabled.png"); + if (autoBreakpointDisabledImage == null) { + autoBreakpointDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_BREAKPOINT_AUTO_DISABLED); } this.autoBreakpointButton = UiUtils.createIconButton(parent, SWT.CHECK | SWT.FLAT); @@ -1094,8 +1072,8 @@ private void showMcpToolContextMenu() { // First menu item MenuItem approvalItem = new MenuItem(contextMenu, SWT.NONE); approvalItem.setText(Messages.chat_actionBar_toolButton_detected_toolTip); - if (redNoticeImage == null || redNoticeImage.isDisposed()) { - redNoticeImage = UiUtils.buildImageFromPngPath("/icons/chat/red_notice.png"); + if (redNoticeImage == null) { + redNoticeImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_RED_NOTICE); } approvalItem.setImage(redNoticeImage); approvalItem.addSelectionListener(new SelectionAdapter() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AddContextButton.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AddContextButton.java index a9091824d..3c023fd07 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AddContextButton.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AddContextButton.java @@ -27,6 +27,7 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; import com.microsoft.copilot.eclipse.ui.i18n.Messages; @@ -50,7 +51,7 @@ public AddContextButton(Composite parent) { setLayout(layout); btnAttachIcon = UiUtils.createIconButton(this, SWT.PUSH | SWT.FLAT); - Image attachImage = UiUtils.buildImageFromPngPath("/icons/chat/attach_context.png"); + Image attachImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_ATTACH_CONTEXT); btnAttachIcon.setImage(attachImage); String attachTooltip = Messages.chat_addContext_tooltip; btnAttachIcon.setToolTipText(attachTooltip); @@ -61,11 +62,6 @@ public void widgetSelected(SelectionEvent e) { openFilePickerAndAddFiles(); } }); - btnAttachIcon.addDisposeListener(e -> { - if (attachImage != null && !attachImage.isDisposed()) { - attachImage.dispose(); - } - }); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentMessageWidget.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentMessageWidget.java index 348508c8d..609f74780 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentMessageWidget.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentMessageWidget.java @@ -17,6 +17,7 @@ import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.lsp.protocol.codingagent.CodingAgentMessageRequestParams; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.swt.CssConstants; import com.microsoft.copilot.eclipse.ui.utils.AccessibilityUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -56,15 +57,10 @@ private void buildMessageLabelWithIcon(CodingAgentMessageRequestParams params) { // Pull request icon Label iconLabel = new Label(titleComposite, SWT.TOP); - Image prImage = UiUtils.isDarkTheme() ? UiUtils.buildImageFromPngPath("/icons/jobs/pull_request_white.png") - : UiUtils.buildImageFromPngPath("/icons/jobs/pull_request_black.png"); + Image prImage = CopilotImages.getThemedImage(CopilotImages.IMG_JOBS_PULL_REQUEST_BLACK, + CopilotImages.IMG_JOBS_PULL_REQUEST_WHITE); iconLabel.setImage(prImage); iconLabel.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); - iconLabel.addDisposeListener(e -> { - if (prImage != null && !prImage.isDisposed()) { - prImage.dispose(); - } - }); // Title label ChatMarkupViewer titleLabel = new ChatMarkupViewer(titleComposite, SWT.LEFT | SWT.WRAP); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java index 62adbb022..4c9366c3d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java @@ -16,10 +16,10 @@ import org.osgi.service.event.EventHandler; import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; +import com.microsoft.copilot.eclipse.ui.CopilotImages; 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. @@ -49,12 +49,6 @@ public AgentStatusLabel(Composite parent, int style) { setLayout(layout); setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); this.addDisposeListener(e -> { - if (this.completedIcon != null && !this.completedIcon.isDisposed()) { - this.completedIcon.dispose(); - } - if (this.cancelledIcon != null && !this.cancelledIcon.isDisposed()) { - this.cancelledIcon.dispose(); - } if (this.eventBroker != null) { this.eventBroker.unsubscribe(cancelStatusHandler); } @@ -82,7 +76,7 @@ public void setCompletedStatus(String statusText) { spinner.stop(); if (this.completedIcon == null) { - this.completedIcon = UiUtils.buildImageFromPngPath("/icons/complete_status.png"); + this.completedIcon = CopilotImages.getImage(CopilotImages.IMG_COMPLETE_STATUS); } iconLabel.setImage(completedIcon); @@ -121,7 +115,7 @@ public void setCancelledStatus() { spinner.stop(); if (this.cancelledIcon == null) { - this.cancelledIcon = UiUtils.buildImageFromPngPath("/icons/cancel_status.png"); + this.cancelledIcon = CopilotImages.getImage(CopilotImages.IMG_CANCEL_STATUS); } iconLabel.setImage(cancelledIcon); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentToolCancelLabel.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentToolCancelLabel.java index 4f7b1e5d6..b8cff6195 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentToolCancelLabel.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentToolCancelLabel.java @@ -10,8 +10,8 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.swt.CssConstants; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A label with icon that displays the cancel status of the agent tool. @@ -30,7 +30,7 @@ public AgentToolCancelLabel(Composite parent, int style, String cancelMessage) { setLayout(new GridLayout(2, false)); setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); - this.cancelIcon = UiUtils.buildImageFromPngPath("/icons/cancel_status.png"); + this.cancelIcon = CopilotImages.getImage(CopilotImages.IMG_CANCEL_STATUS); Label iconLabel = new Label(this, SWT.LEFT); iconLabel.setImage(this.cancelIcon); @@ -38,11 +38,5 @@ public AgentToolCancelLabel(Composite parent, int style, String cancelMessage) { textLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); textLabel.setText(cancelMessage); textLabel.setData(CssConstants.CSS_CLASS_NAME_KEY, "text-secondary"); - - this.addDisposeListener(e -> { - if (this.cancelIcon != null && !this.cancelIcon.isDisposed()) { - this.cancelIcon.dispose(); - } - }); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CurrentReferencedFile.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CurrentReferencedFile.java index 19a9dde2a..b9553c457 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CurrentReferencedFile.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/CurrentReferencedFile.java @@ -9,26 +9,22 @@ import org.eclipse.swt.SWT; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; -import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.CssConstants; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A special {@link ReferencedFile} that represents the current file being edited. This is used to represent the current * file in the chat view, when no file is selected. */ public class CurrentReferencedFile extends ReferencedFile { - private static Image visibleImage = UiUtils.buildImageDescriptorFromPngPath("/icons/chat/eye.png").createImage(); - private static Image invisibleImage = UiUtils.buildImageDescriptorFromPngPath("/icons/chat/eye_closed.png") - .createImage(); private Label descriptionLabel; private Label selectionLabel; @@ -81,9 +77,9 @@ public CurrentReferencedFile(Composite parent) { */ public void updateCloseClickBtnIcon(boolean isCurrentFileVisible) { if (isCurrentFileVisible) { - setCloseClickBtnIcon(visibleImage); + setCloseClickBtnIcon(CopilotImages.getImage(CopilotImages.IMG_CHAT_EYE)); } else { - setCloseClickBtnIcon(invisibleImage); + setCloseClickBtnIcon(CopilotImages.getImage(CopilotImages.IMG_CHAT_EYE_CLOSED)); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java index 46a245eef..31ca69a14 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ModelPickerGroupsBuilder.java @@ -12,9 +12,9 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.preferences.ByokPreferencePage; import com.microsoft.copilot.eclipse.ui.swt.DropdownItem; @@ -30,8 +30,6 @@ */ public final class ModelPickerGroupsBuilder { - private static Image warningIcon; - private ModelPickerGroupsBuilder() { } @@ -140,23 +138,8 @@ private static Image resolveModelIcon(CopilotModel model) { if (StringUtils.isBlank(model.getDegradationReason())) { return null; } - if (warningIcon == null || warningIcon.isDisposed()) { - warningIcon = UiUtils.isDarkTheme() - ? UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_warning_dark.png") - : UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_warning.png"); - Display display = SwtUtils.getDisplay(); - if (display != null) { - display.addListener(SWT.Dispose, event -> disposeWarningIcons()); - } - } - return warningIcon; - } - - private static void disposeWarningIcons() { - if (warningIcon != null && !warningIcon.isDisposed()) { - warningIcon.dispose(); - warningIcon = null; - } + return CopilotImages.getThemedImage(CopilotImages.IMG_DROPDOWN_WARNING, + CopilotImages.IMG_DROPDOWN_WARNING_DARK); } private static void openManageModelsPreferences() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java index e1bf9b25e..bbfb0544b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java @@ -28,6 +28,7 @@ import org.eclipse.ui.model.WorkbenchLabelProvider; import com.microsoft.copilot.eclipse.core.Constants; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; import com.microsoft.copilot.eclipse.ui.i18n.Messages; @@ -81,15 +82,9 @@ public void mouseDown(MouseEvent e) { lblClose.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false)); setCloseClickAction(); - lblImage = UiUtils.buildImageFromPngPath("/icons/close.png"); + lblImage = CopilotImages.getImage(CopilotImages.IMG_CLOSE); setFile(file); - - this.addDisposeListener(e -> { - if (lblImage != null && !lblImage.isDisposed()) { - lblImage.dispose(); - } - }); this.setCursor(getDisplay().getSystemCursor(SWT.CURSOR_HAND)); // Add keyboard support for Enter activation diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ThinkingBlock.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ThinkingBlock.java index e273986f2..781172006 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ThinkingBlock.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ThinkingBlock.java @@ -25,6 +25,7 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; import com.microsoft.copilot.eclipse.ui.swt.SpinnerAnimator; @@ -87,8 +88,6 @@ public ThinkingBlock(Composite parent, int style) { createHeader(); createBody(); - addDisposeListener(e -> handleDispose()); - setTitle(Messages.thinking_title); spinner = new SpinnerAnimator(iconLabel); spinner.start(); @@ -135,7 +134,7 @@ public boolean showCancelled() { } stopSpinner(); if (cancelledIcon == null || cancelledIcon.isDisposed()) { - cancelledIcon = UiUtils.buildImageFromPngPath("/icons/cancel_status.png"); + cancelledIcon = CopilotImages.getImage(CopilotImages.IMG_CANCEL_STATUS); } if (!iconLabel.isDisposed()) { iconLabel.setImage(cancelledIcon); @@ -474,14 +473,14 @@ private void updateChevron() { Image image; String tooltip; if (expanded) { - if (downArrowImage == null || downArrowImage.isDisposed()) { - downArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/down_arrow.png"); + if (downArrowImage == null) { + downArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_DOWN_ARROW); } image = downArrowImage; tooltip = Messages.thinking_collapseTooltip; } else { - if (rightArrowImage == null || rightArrowImage.isDisposed()) { - rightArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/right_arrow.png"); + if (rightArrowImage == null) { + rightArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_RIGHT_ARROW); } image = rightArrowImage; tooltip = Messages.thinking_expandTooltip; @@ -505,19 +504,4 @@ private void refreshEnclosingScroller() { } } - private void handleDispose() { - if (cancelledIcon != null && !cancelledIcon.isDisposed()) { - cancelledIcon.dispose(); - cancelledIcon = null; - } - if (downArrowImage != null && !downArrowImage.isDisposed()) { - downArrowImage.dispose(); - downArrowImage = null; - } - if (rightArrowImage != null && !rightArrowImage.isDisposed()) { - rightArrowImage.dispose(); - rightArrowImage = null; - } - } - } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/TodoListBar.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/TodoListBar.java index abaae5199..cf6bcd2cb 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/TodoListBar.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/TodoListBar.java @@ -23,6 +23,7 @@ import org.eclipse.swt.widgets.Label; import com.microsoft.copilot.eclipse.core.lsp.protocol.TodoItem; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ChatFontService; import com.microsoft.copilot.eclipse.ui.chat.services.TodoListService; @@ -59,32 +60,15 @@ public TodoListBar(Composite parent, int style) { this.todoListService = CopilotUi.getPlugin().getChatServiceManager().getTodoListService(); this.chatFontService = CopilotUi.getPlugin().getChatServiceManager().getChatFontService(); loadStatusImages(); - this.addDisposeListener(e -> disposeStatusImages()); - } - - private void disposeStatusImages() { - if (completedImage != null && !completedImage.isDisposed()) { - completedImage.dispose(); - } - if (inProgressImage != null && !inProgressImage.isDisposed()) { - inProgressImage.dispose(); - } - if (notStartedImage != null && !notStartedImage.isDisposed()) { - notStartedImage.dispose(); - } } private void loadStatusImages() { - boolean isDarkTheme = UiUtils.isDarkTheme(); - if (isDarkTheme) { - completedImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_finish_dark.png"); - inProgressImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_running_dark.png"); - notStartedImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_waiting_dark.png"); - } else { - completedImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_finish.png"); - inProgressImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_running.png"); - notStartedImage = UiUtils.buildImageFromPngPath("/icons/chat/todos_waiting.png"); - } + completedImage = CopilotImages.getThemedImage(CopilotImages.IMG_TODOS_FINISH, + CopilotImages.IMG_TODOS_FINISH_DARK); + inProgressImage = CopilotImages.getThemedImage(CopilotImages.IMG_TODOS_RUNNING, + CopilotImages.IMG_TODOS_RUNNING_DARK); + notStartedImage = CopilotImages.getThemedImage(CopilotImages.IMG_TODOS_WAITING, + CopilotImages.IMG_TODOS_WAITING_DARK); } Image getStatusImage(String status) { @@ -228,8 +212,6 @@ public void mouseUp(MouseEvent e) { addMouseListener(clickListener); updateDisplay(); - - addDisposeListener(e -> disposeImages()); } private void createExpandIcon() { @@ -280,8 +262,8 @@ public void widgetSelected(SelectionEvent e) { } private void loadClearButtonImages() { - clearEnabledImage = UiUtils.buildImageFromPngPath("/icons/chat/clear_todo.png"); - clearDisabledImage = UiUtils.buildImageFromPngPath("/icons/chat/clear_todo_disable.png"); + clearEnabledImage = CopilotImages.getImage(CopilotImages.IMG_CLEAR_TODO); + clearDisabledImage = CopilotImages.getImage(CopilotImages.IMG_CLEAR_TODO_DISABLED); } public void updateDisplay() { @@ -322,13 +304,13 @@ private void updateExpandIcon() { if (isExpanded) { if (downArrowImage == null) { - downArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/down_arrow.png"); + downArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_DOWN_ARROW); } expandIcon.setImage(downArrowImage); setToolTipText(Messages.todoList_collapseTooltip); } else { if (rightArrowImage == null) { - rightArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/right_arrow.png"); + rightArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_RIGHT_ARROW); } expandIcon.setImage(rightArrowImage); setToolTipText(Messages.todoList_expandTooltip); @@ -391,24 +373,6 @@ public void updateClearButtonState() { } } - private void disposeImages() { - if (downArrowImage != null && !downArrowImage.isDisposed()) { - downArrowImage.dispose(); - downArrowImage = null; - } - if (rightArrowImage != null && !rightArrowImage.isDisposed()) { - rightArrowImage.dispose(); - rightArrowImage = null; - } - if (clearEnabledImage != null && !clearEnabledImage.isDisposed()) { - clearEnabledImage.dispose(); - clearEnabledImage = null; - } - if (clearDisabledImage != null && !clearDisabledImage.isDisposed()) { - clearDisabledImage.dispose(); - clearDisabledImage = null; - } - } } class TodoListContent extends Composite { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WorkingSetBar.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WorkingSetBar.java index 1c0780a61..d83903d2f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WorkingSetBar.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WorkingSetBar.java @@ -28,6 +28,7 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.model.WorkbenchLabelProvider; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ChatFontService; import com.microsoft.copilot.eclipse.ui.chat.tools.ChangedFile; @@ -210,15 +211,6 @@ public void mouseUp(MouseEvent e) { addMouseListener(clickListener); updateTitleBarButtons(); - - this.addDisposeListener(e -> { - if (downArrowImage != null && !downArrowImage.isDisposed()) { - downArrowImage.dispose(); - } - if (rightArrowImage != null && !rightArrowImage.isDisposed()) { - rightArrowImage.dispose(); - } - }); } /** @@ -234,14 +226,14 @@ private void updateExpandCollapseState() { if (isExpanded) { // Expanded state: show down arrow and collapse tooltip if (downArrowImage == null) { - downArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/down_arrow.png"); + downArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_DOWN_ARROW); } expandIcon.setImage(downArrowImage); tooltipMessage = NLS.bind(Messages.fileChangeSummary_collapseTooltip, changeFilesTitle); } else { // Collapsed state: show right arrow and expand tooltip if (rightArrowImage == null) { - rightArrowImage = UiUtils.buildImageFromPngPath("/icons/chat/right_arrow.png"); + rightArrowImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_RIGHT_ARROW); } expandIcon.setImage(rightArrowImage); tooltipMessage = NLS.bind(Messages.fileChangeSummary_expandTooltip, changeFilesTitle); @@ -488,12 +480,7 @@ public void mouseUp(MouseEvent e) { actionsArea.setVisible(false); keepButton = new Label(actionsArea, SWT.NONE); - Image keepImg = UiUtils.buildImageFromPngPath("/icons/chat/keep.png"); - this.addDisposeListener(e -> { - if (keepImg != null && !keepImg.isDisposed()) { - keepImg.dispose(); - } - }); + Image keepImg = CopilotImages.getImage(CopilotImages.IMG_CHAT_KEEP); keepButton.setImage(keepImg); keepButton.setToolTipText(Messages.fileChangeSummary_keepButton); GridData keepGridData = new GridData(SWT.END, SWT.CENTER, false, false); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/dialogs/DynamicOauthDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/dialogs/DynamicOauthDialog.java index 77edec36b..abfd23cae 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/dialogs/DynamicOauthDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/dialogs/DynamicOauthDialog.java @@ -24,7 +24,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.McpOauthRequest; import com.microsoft.copilot.eclipse.core.lsp.mcp.McpOauthRequest.DynamicOauthInput; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; /** * Dialog for handling Dynamic OAuth requests from MCP servers. @@ -57,7 +57,7 @@ protected void configureShell(Shell newShell) { newShell.setText(request.getTitle()); } - icon = UiUtils.buildImageFromPngPath("/icons/github_copilot.png"); + icon = CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); if (icon != null) { newShell.setImage(icon); } @@ -213,10 +213,6 @@ private void disposeResources() { headerFont.dispose(); headerFont = null; } - if (icon != null && !icon.isDisposed()) { - icon.dispose(); - icon = null; - } } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java index 656a6f621..25edb66bc 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java @@ -23,6 +23,7 @@ import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotStatusResult; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -31,9 +32,6 @@ */ public class AvatarService { private static final String AVATAR_URL = "https://avatars.githubusercontent.com/%s?s=24&v=4"; - private static final String DEFAULT_COPILOT_AVATAR_NAME = "/icons/chat/chat_message_copilot_avatar.png"; - private static final String DEFAULT_USER_AVATAR_NAME = "/icons/chat/chat_message_user_avatar.png"; - private Map avatarCache = new ConcurrentHashMap<>(); private Map jobs = new ConcurrentHashMap<>(); @@ -48,8 +46,8 @@ public class AvatarService { */ public AvatarService(AuthStatusManager authStatusManager) { this.authStatusManager = authStatusManager; - this.defaultGithubAvatar = UiUtils.buildImageFromPngPath(DEFAULT_COPILOT_AVATAR_NAME); - this.defaultUserAvatar = UiUtils.buildImageFromPngPath(DEFAULT_USER_AVATAR_NAME); + this.defaultGithubAvatar = CopilotImages.getImage(CopilotImages.IMG_CHAT_COPILOT_AVATAR); + this.defaultUserAvatar = CopilotImages.getImage(CopilotImages.IMG_CHAT_USER_AVATAR); this.authStatusChangedEventHandler = event -> { Object property = event.getProperty(IEventBroker.DATA); if (property instanceof CopilotStatusResult statusResult && statusResult.isSignedIn()) { @@ -137,8 +135,6 @@ protected IStatus run(IProgressMonitor monitor) { * Disposes the resources. */ public void dispose() { - defaultGithubAvatar.dispose(); - defaultUserAvatar.dispose(); avatarCache.values().forEach(Image::dispose); jobs.values().forEach(Job::cancel); if (this.eventBroker != null) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java index 2bfbdc117..67e1b1ca9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java @@ -25,12 +25,12 @@ import com.microsoft.copilot.eclipse.terminal.api.IRunInTerminalTool; import com.microsoft.copilot.eclipse.terminal.api.TerminalCommandProcessor; import com.microsoft.copilot.eclipse.terminal.api.TerminalServiceManager; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.ChatView; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; import com.microsoft.copilot.eclipse.ui.utils.ResourceUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Adapter that bridges the UI tool interface with SPI-based terminal implementations. @@ -199,7 +199,7 @@ public CompletableFuture invoke(Map i isBackground = Boolean.parseBoolean((String) isBackgroundObj); } - impl.setTerminalIconDescriptor(UiUtils.buildImageDescriptorFromPngPath("/icons/github_copilot.png")); + impl.setTerminalIconDescriptor(CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT)); String workingDirectory = resolveWorkingDirectory(); return impl.executeCommand(command, isBackground, workingDirectory) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AfterLoginWelcomeViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AfterLoginWelcomeViewer.java index bb9ff0337..e7d5241bd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AfterLoginWelcomeViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AfterLoginWelcomeViewer.java @@ -14,9 +14,9 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A widget that displays a initial chat introduction. @@ -82,13 +82,8 @@ private void buildMainIconAndLabel() { iconLabelComposite.setLayout(iconLabelGridlayout); iconLabelComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); - Image mainIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_welcome.png"); + Image mainIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_WELCOME); this.copilotIconLabel = new Label(iconLabelComposite, SWT.NONE); - this.copilotIconLabel.addDisposeListener(e -> { - if (mainIcon != null && !mainIcon.isDisposed()) { - mainIcon.dispose(); - } - }); this.copilotIconLabel.setImage(mainIcon); this.copilotIconLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); @@ -137,12 +132,7 @@ private void buildInstructions() { gridData.minimumHeight = 70; instructionComposite.setLayoutData(gridData); - Image attachContextIcon = UiUtils.buildImageFromPngPath("/icons/chat/attach_context.png"); - instructionComposite.addDisposeListener(e -> { - if (attachContextIcon != null && !attachContextIcon.isDisposed()) { - attachContextIcon.dispose(); - } - }); + Image attachContextIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_ATTACH_CONTEXT); buildLabelWithIcon(instructionComposite, attachContextIcon, Messages.chat_initialChatView_attachContextSuffix); WrapLabel subLabel = new WrapLabel(instructionComposite, SWT.CENTER); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AgentModeViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AgentModeViewer.java index 0d2d0baca..b58728f24 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AgentModeViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/AgentModeViewer.java @@ -14,9 +14,9 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A widget that displays a initial chat introduction. @@ -82,13 +82,8 @@ private void buildMainIconAndLabel() { iconLabelComposite.setLayout(iconLabelGridlayout); iconLabelComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); - Image mainIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_welcome.png"); + Image mainIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_WELCOME); this.copilotIconLabel = new Label(iconLabelComposite, SWT.CENTER); - this.copilotIconLabel.addDisposeListener(e -> { - if (mainIcon != null && !mainIcon.isDisposed()) { - mainIcon.dispose(); - } - }); this.copilotIconLabel.setImage(mainIcon); this.copilotIconLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); @@ -142,21 +137,11 @@ private void buildInstructions() { instructionComposite.setLayoutData(gridData); // configure MCP icon with label - Image configureMcpIcon = UiUtils.buildImageFromPngPath("/icons/chat/tools.png"); - instructionComposite.addDisposeListener(e -> { - if (configureMcpIcon != null && !configureMcpIcon.isDisposed()) { - configureMcpIcon.dispose(); - } - }); + Image configureMcpIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_TOOLS); buildLabelWithIcon(instructionComposite, configureMcpIcon, Messages.chat_agentModeView_configureMcpSuffix); // attach context icon with label - Image attachContextIcon = UiUtils.buildImageFromPngPath("/icons/chat/attach_context.png"); - instructionComposite.addDisposeListener(e -> { - if (attachContextIcon != null && !attachContextIcon.isDisposed()) { - attachContextIcon.dispose(); - } - }); + Image attachContextIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_ATTACH_CONTEXT); buildLabelWithIcon(instructionComposite, attachContextIcon, Messages.chat_agentModeView_attachContextSuffix); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/BeforeLoginWelcomeViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/BeforeLoginWelcomeViewer.java index 26050db8b..8f528ff27 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/BeforeLoginWelcomeViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/BeforeLoginWelcomeViewer.java @@ -19,6 +19,7 @@ import org.eclipse.ui.handlers.IHandlerService; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.CssConstants; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; @@ -113,7 +114,7 @@ private void buildMainIconAndLabel(Composite parent) { iconLabelComposite.setLayout(iconLabelGridlayout); iconLabelComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); - this.mainIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_welcome.png"); + this.mainIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_WELCOME); this.copilotIconLabel = new Label(iconLabelComposite, SWT.CENTER); this.copilotIconLabel.setImage(mainIcon); this.copilotIconLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, true)); @@ -139,28 +140,16 @@ private void buildSubComposite(Composite parent) { subComposite.setLayoutData(gridData); subComposite.setLayout(new GridLayout(1, true)); - if (this.agentIcon != null && !this.agentIcon.isDisposed()) { - this.agentIcon.dispose(); - } - this.agentIcon = UiUtils.buildImageFromPngPath("/icons/github_copilot.png"); + this.agentIcon = CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); buildLabelWithIcon(subComposite, agentIcon, Messages.chat_welcomeView_agentSuffix); - if (this.mcpIcon != null && !this.mcpIcon.isDisposed()) { - this.mcpIcon.dispose(); - } - this.mcpIcon = UiUtils.buildImageFromPngPath("/icons/chat/tools.png"); + this.mcpIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_TOOLS); buildLabelWithIcon(subComposite, mcpIcon, Messages.chat_welcomeView_mcpSuffix); - if (this.chatIcon != null && !this.chatIcon.isDisposed()) { - this.chatIcon.dispose(); - } - this.chatIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_chat.png"); + this.chatIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_CHAT); buildLabelWithIcon(subComposite, chatIcon, Messages.chat_welcomeView_chatSuffix); - if (this.codeIcon != null && !this.codeIcon.isDisposed()) { - this.codeIcon.dispose(); - } - this.codeIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_code.png"); + this.codeIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_CODE); buildLabelWithIcon(subComposite, codeIcon, Messages.chat_welcomeView_completionSuffix); } @@ -242,24 +231,9 @@ private void buildFooterLinks() { @Override public void dispose() { - if (this.mainIcon != null) { - this.mainIcon.dispose(); - } if (this.mainLabelFont != null) { this.mainLabelFont.dispose(); } - if (this.codeIcon != null) { - this.codeIcon.dispose(); - } - if (this.chatIcon != null) { - this.chatIcon.dispose(); - } - if (this.agentIcon != null) { - this.agentIcon.dispose(); - } - if (this.mcpIcon != null) { - this.mcpIcon.dispose(); - } super.dispose(); } } \ No newline at end of file diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/ChatHistoryViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/ChatHistoryViewer.java index 09c8f0f44..bda3896bf 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/ChatHistoryViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/ChatHistoryViewer.java @@ -36,6 +36,7 @@ import com.microsoft.copilot.eclipse.core.persistence.ConversationPersistenceManager; import com.microsoft.copilot.eclipse.core.persistence.ConversationXmlData; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.ConversationUtils; import com.microsoft.copilot.eclipse.ui.chat.services.ChatFontService; @@ -76,10 +77,10 @@ public ChatHistoryViewer(Composite parent, int style, List this.stylingEngine = PlatformUI.getWorkbench().getService(IStylingEngine.class); this.chatFontService = CopilotUi.getPlugin().getChatServiceManager().getChatFontService(); this.currentConversationId = currentConversationId; - this.backImage = UiUtils.buildImageFromPngPath( - UiUtils.isDarkTheme() ? "/icons/chat/back_arrow_grey.png" : "/icons/chat/back_arrow.png"); - this.enterImage = UiUtils.buildImageFromPngPath("/icons/chat/enter.png"); - this.editImage = UiUtils.buildImageFromPngPath("/icons/chat/chat_history_edit.png"); + this.backImage = CopilotImages.getThemedImage(CopilotImages.IMG_CHAT_BACK_ARROW, + CopilotImages.IMG_CHAT_BACK_ARROW_GREY); + this.enterImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_ENTER); + this.editImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_HISTORY_EDIT); // Assign CSS id for styling this.setData(CssConstants.CSS_ID_KEY, "chat-history-viewer"); @@ -91,17 +92,6 @@ public ChatHistoryViewer(Composite parent, int style, List setLayout(layout); createChatHistoryComposite(conversations, currentConversationId); - this.addDisposeListener(e -> { - if (backImage != null && !backImage.isDisposed()) { - backImage.dispose(); - } - if (enterImage != null && !enterImage.isDisposed()) { - enterImage.dispose(); - } - if (editImage != null && !editImage.isDisposed()) { - editImage.dispose(); - } - }); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/LoadingViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/LoadingViewer.java index b5a6ae25c..cf75454f0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/LoadingViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/LoadingViewer.java @@ -13,9 +13,9 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A widget that displays a loading view. @@ -44,7 +44,7 @@ private void buildMainIconAndLabel() { iconLabelComposite.setLayout(iconLabelGridlayout); iconLabelComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); - Image mainIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_loading.png"); + Image mainIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_LOADING); Label icon = new Label(iconLabelComposite, SWT.CENTER); icon.setImage(mainIcon); icon.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); @@ -62,9 +62,6 @@ private void buildMainIconAndLabel() { subLabel.setForeground(this.getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); this.addDisposeListener(e -> { - if (mainIcon != null && !mainIcon.isDisposed()) { - mainIcon.dispose(); - } if (mainLabelFont != null && !mainLabelFont.isDisposed()) { mainLabelFont.dispose(); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/NoSubscriptionViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/NoSubscriptionViewer.java index 01b724d85..332fe7c23 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/NoSubscriptionViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/viewers/NoSubscriptionViewer.java @@ -15,6 +15,7 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -23,7 +24,6 @@ * A widget that displays a no Copilot subscription info. */ public class NoSubscriptionViewer extends BaseViewer { - private Image mainIcon; private Font mainLabelFont; /** @@ -49,7 +49,7 @@ private void buildMainIconAndLabel() { iconLabelComposite.setLayout(iconLabelGridlayout); iconLabelComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); - this.mainIcon = UiUtils.buildImageFromPngPath("/icons/chat/chatview_icon_not_authorized.png"); + Image mainIcon = CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_NOT_AUTHORIZED); Label icon = new Label(iconLabelComposite, SWT.NONE); icon.setImage(mainIcon); icon.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, false, false)); @@ -88,9 +88,6 @@ public void widgetSelected(org.eclipse.swt.events.SelectionEvent event) { @Override public void dispose() { - if (this.mainIcon != null) { - this.mainIcon.dispose(); - } if (this.mainLabelFont != null) { this.mainLabelFont.dispose(); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/MissingTerminalDependenciesDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/MissingTerminalDependenciesDialog.java index 7c2379367..1e87cdfcc 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/MissingTerminalDependenciesDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/MissingTerminalDependenciesDialog.java @@ -29,10 +29,10 @@ import com.microsoft.copilot.eclipse.core.Constants; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Dialog to inform users about missing terminal dependencies. Provides information about what needs to be installed and @@ -66,14 +66,9 @@ protected void configureShell(Shell shell) { } private void loadIcon(Shell shell) { - Image icon = UiUtils.buildImageFromPngPath("/icons/github_copilot.png"); + Image icon = CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); if (icon != null) { shell.setImage(icon); - shell.addDisposeListener(e -> { - if (!icon.isDisposed()) { - icon.dispose(); - } - }); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/jobs/BaseCopilotDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/jobs/BaseCopilotDialog.java index bff5569c2..1f0cba10b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/jobs/BaseCopilotDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/jobs/BaseCopilotDialog.java @@ -17,6 +17,7 @@ import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -46,15 +47,10 @@ protected void configureShell(Shell newShell) { * @param shell the shell to set the icon on */ protected void loadCopilotIcon(Shell shell) { - Image dialogTitleImage = UiUtils.buildImageFromPngPath("/icons/github_copilot.png"); + Image dialogTitleImage = CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); if (dialogTitleImage != null) { shell.setImage(dialogTitleImage); } - shell.addDisposeListener(e -> { - if (dialogTitleImage != null && !dialogTitleImage.isDisposed()) { - dialogTitleImage.dispose(); - } - }); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpApprovalDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpApprovalDialog.java index 97a1a0253..b55b886cf 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpApprovalDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpApprovalDialog.java @@ -40,10 +40,10 @@ import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.chat.services.McpExtensionPointManager.McpRegistrationInfo; import com.microsoft.copilot.eclipse.ui.preferences.WrappableNoteLabel; import com.microsoft.copilot.eclipse.ui.utils.TextMateUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Dialog for approving third-party MCP providers. @@ -85,13 +85,8 @@ protected void configureShell(Shell newShell) { } private void loadImages(Shell shell) { - approvedImage = UiUtils.buildImageFromPngPath("/icons/chat/keep.png"); + approvedImage = CopilotImages.getImage(CopilotImages.IMG_CHAT_KEEP); deniedImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_DELETE); - shell.addDisposeListener(e -> { - if (approvedImage != null && !approvedImage.isDisposed()) { - approvedImage.dispose(); - } - }); } @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpRegistryDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpRegistryDialog.java index d7d3124b2..f315d215e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpRegistryDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpRegistryDialog.java @@ -39,6 +39,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerList; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerResponse; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.preferences.McpPreferencePage; import com.microsoft.copilot.eclipse.ui.swt.CssConstants; @@ -91,15 +92,10 @@ protected void configureShell(Shell newShell) { newShell.setText(Messages.mcpRegistryDialog_mcpRegistry); newShell.setMinimumSize(MIN_WIDTH, SWT.DEFAULT); - Image dialogIcon = UiUtils.buildImageFromPngPath("/icons/mcp/mcp_registry.png"); + Image dialogIcon = CopilotImages.getImage(CopilotImages.IMG_MCP_REGISTRY); if (dialogIcon != null) { newShell.setImage(dialogIcon); } - newShell.addDisposeListener(e -> { - if (dialogIcon != null && !dialogIcon.isDisposed()) { - dialogIcon.dispose(); - } - }); } @Override @@ -443,22 +439,17 @@ private void createHeaderButtons(Composite parent) { private void createRefreshButton(Composite parent) { Button refreshButton = new Button(parent, SWT.PUSH); - Image refreshIcon = UiUtils.buildImageFromPngPath("/icons/mcp/refresh.png"); + Image refreshIcon = CopilotImages.getImage(CopilotImages.IMG_MCP_REFRESH); refreshButton.setImage(refreshIcon); refreshButton.setText(Messages.mcpRegistryDialog_button_refresh); refreshButton.setToolTipText(Messages.mcpRegistryDialog_button_refresh_tooltip); refreshButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false)); refreshButton.addListener(SWT.Selection, e -> refreshServerList()); - refreshButton.addDisposeListener(e -> { - if (refreshIcon != null && !refreshIcon.isDisposed()) { - refreshIcon.dispose(); - } - }); } private void createChangeUrlButton(Composite parent) { Button editButton = new Button(parent, SWT.PUSH); - Image editIcon = UiUtils.buildImageFromPngPath("/icons/edit_preferences.png"); + Image editIcon = CopilotImages.getImage(CopilotImages.IMG_EDIT_PREFERENCES); editButton.setImage(editIcon); editButton.setText(Messages.mcpRegistryDialog_button_changeUrl); editButton.setToolTipText(Messages.mcpRegistryDialog_button_changeUrl_tooltip); @@ -467,11 +458,6 @@ private void createChangeUrlButton(Composite parent) { e -> PreferencesUtil .createPreferenceDialogOn(getShell(), McpPreferencePage.ID, PreferencesUtils.getAllPreferenceIds(), null) .open()); - editButton.addDisposeListener(e -> { - if (editIcon != null && !editIcon.isDisposed()) { - editIcon.dispose(); - } - }); } /** @@ -531,14 +517,9 @@ private void createBanner(Composite parent) { // Create icon label on the right Label iconLabel = new Label(banner, SWT.NONE); - Image bannerIcon = UiUtils.buildImageFromPngPath("/icons/mcp/mcp_marketplace_icon.png"); + Image bannerIcon = CopilotImages.getImage(CopilotImages.IMG_MCP_MARKETPLACE_ICON); if (bannerIcon != null) { iconLabel.setImage(bannerIcon); - iconLabel.addDisposeListener(e -> { - if (!bannerIcon.isDisposed()) { - bannerIcon.dispose(); - } - }); } iconLabel.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java index 6d36fef49..ffcd08f71 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java @@ -48,6 +48,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.Remote; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerDetail; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerResponse; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.McpServerInstallManager.ButtonState; import com.microsoft.copilot.eclipse.ui.utils.McpUtils; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -179,15 +180,10 @@ protected void configureShell(Shell newShell) { : Messages.mcpServerDetailDialog_title; newShell.setText(title); - Image dialogIcon = UiUtils.buildImageFromPngPath("/icons/mcp/mcp_registry.png"); + Image dialogIcon = CopilotImages.getImage(CopilotImages.IMG_MCP_REGISTRY); if (dialogIcon != null) { newShell.setImage(dialogIcon); } - newShell.addDisposeListener(e -> { - if (dialogIcon != null && !dialogIcon.isDisposed()) { - dialogIcon.dispose(); - } - }); } @Override @@ -395,7 +391,8 @@ private void createDescriptionContent(Composite parent) { private void createVersionContent(Composite parent) { ServerDetail serverDetail = getServerDetail(); String version = serverDetail == null ? Messages.mcpServer_unknown : serverDetail.version(); - createIconTextRow(parent, UiUtils.isDarkTheme() ? "/icons/mcp/versions_dark.png" : "/icons/mcp/versions.png", + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_VERSIONS, CopilotImages.IMG_MCP_VERSIONS_DARK); + createIconTextRow(parent, icon, Messages.mcpServerDetailDialog_version + " " + version, Messages.mcpServerDetailDialog_version + " " + version); } @@ -405,8 +402,8 @@ private void createPublishedContent(Composite parent) { String detailedDate = getDetailedFormattedDate(publishedAt); String text = relativeTime != null ? Messages.mcpServerDetailDialog_published + " " + relativeTime : Messages.mcpServerDetailDialog_noPublishedDate; - createIconTextRow(parent, UiUtils.isDarkTheme() ? "/icons/mcp/history_dark.png" : "/icons/mcp/history.png", text, - detailedDate); + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_HISTORY, CopilotImages.IMG_MCP_HISTORY_DARK); + createIconTextRow(parent, icon, text, detailedDate); } private void createUpdatedContent(Composite parent) { @@ -415,13 +412,13 @@ private void createUpdatedContent(Composite parent) { String detailedDate = getDetailedFormattedDate(updatedAt); String text = relativeTime != null ? Messages.mcpServerDetailDialog_updated + " " + relativeTime : Messages.mcpServerDetailDialog_noUpdatedDate; - createIconTextRow(parent, UiUtils.isDarkTheme() ? "/icons/mcp/update_dark.png" : "/icons/mcp/update.png", text, - detailedDate); + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_UPDATE, CopilotImages.IMG_MCP_UPDATE_DARK); + createIconTextRow(parent, icon, text, detailedDate); } private void createRepositoryLink(Composite parent) { - Composite row = createRowWithIcon(parent, - UiUtils.isDarkTheme() ? "/icons/mcp/repository_dark.png" : "/icons/mcp/repository.png"); + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_REPOSITORY, CopilotImages.IMG_MCP_REPOSITORY_DARK); + Composite row = createRowWithIcon(parent, icon); Link repoLink = new Link(row, SWT.NONE); repoLink.setText("" + Messages.mcpServerDetailDialog_repository + ""); @@ -438,11 +435,10 @@ private void createRepositoryLink(Composite parent) { } /** - * Builds a two-column row with an icon on the left and a text label on the right. Disposes the image when the icon - * label is disposed. + * Builds a two-column row with an icon on the left and a text label on the right. */ - private Composite createIconTextRow(Composite parent, String imagePath, String text, String tooltip) { - Composite row = createRowWithIcon(parent, imagePath); + private Composite createIconTextRow(Composite parent, Image icon, String text, String tooltip) { + Composite row = createRowWithIcon(parent, icon); Label textLabel = new Label(row, SWT.NONE); textLabel.setText(text != null ? text : ""); @@ -454,7 +450,7 @@ private Composite createIconTextRow(Composite parent, String imagePath, String t return row; } - private Composite createRowWithIcon(Composite parent, String imagePath) { + private Composite createRowWithIcon(Composite parent, Image icon) { GridLayout rowLayout = new GridLayout(2, false); rowLayout.marginWidth = 0; rowLayout.marginHeight = 0; @@ -462,14 +458,8 @@ private Composite createRowWithIcon(Composite parent, String imagePath) { row.setLayout(rowLayout); Label iconLabel = new Label(row, SWT.NONE); - final Image icon = UiUtils.buildImageFromPngPath(imagePath); iconLabel.setImage(icon); iconLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false)); - iconLabel.addDisposeListener(e -> { - if (icon != null && !icon.isDisposed()) { - icon.dispose(); - } - }); return row; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java index 2c15179e3..923a09e9b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java @@ -41,6 +41,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.Remote; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerDetail; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.ServerResponse; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.McpServerInstallManager.ActionResult; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.McpServerInstallManager.ActionType; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.McpServerInstallManager.ButtonState; @@ -191,32 +192,30 @@ private void createMetaInfoRow(Composite parent, ServerDetail detail) { rowLayout.marginBottom = 0; metaRow.setLayout(rowLayout); - boolean isDark = UiUtils.isDarkTheme(); - // Version if (StringUtils.isNotBlank(version)) { - String iconPath = isDark ? "/icons/mcp/versions_dark.png" : "/icons/mcp/versions.png"; + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_VERSIONS, CopilotImages.IMG_MCP_VERSIONS_DARK); String text = Messages.mcpServerDetailDialog_version + " " + version; - createIconTextLabel(metaRow, iconPath, text, NLS.bind(Messages.mcpServerItem_versionTooltip, version)); + createIconTextLabel(metaRow, icon, text, NLS.bind(Messages.mcpServerItem_versionTooltip, version)); } // Published date if (StringUtils.isNotBlank(publishedAt)) { - String iconPath = isDark ? "/icons/mcp/history_dark.png" : "/icons/mcp/history.png"; + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_HISTORY, CopilotImages.IMG_MCP_HISTORY_DARK); String relativeTime = formatRelativeTime(publishedAt); String text = relativeTime != null ? Messages.mcpServerDetailDialog_published + " " + relativeTime : Messages.mcpServerDetailDialog_noPublishedDate; - createIconTextLabel(metaRow, iconPath, text, + createIconTextLabel(metaRow, icon, text, NLS.bind(Messages.mcpServerItem_publishedTooltip, formatDetailedDate(publishedAt))); } // Updated date if (StringUtils.isNotBlank(updatedAt)) { - String iconPath = isDark ? "/icons/mcp/update_dark.png" : "/icons/mcp/update.png"; + Image icon = CopilotImages.getThemedImage(CopilotImages.IMG_MCP_UPDATE, CopilotImages.IMG_MCP_UPDATE_DARK); String relativeTime = formatRelativeTime(updatedAt); String text = relativeTime != null ? Messages.mcpServerDetailDialog_updated + " " + relativeTime : Messages.mcpServerDetailDialog_noUpdatedDate; - createIconTextLabel(metaRow, iconPath, text, + createIconTextLabel(metaRow, icon, text, NLS.bind(Messages.mcpServerItem_updatedTooltip, formatDetailedDate(updatedAt))); } } @@ -233,24 +232,16 @@ private OfficialMeta getMetaOfficial() { return serverResponse.meta().official(); } - private void createIconTextLabel(Composite parent, String iconPath, String text, String tooltip) { + private void createIconTextLabel(Composite parent, Image icon, String text, String tooltip) { Composite row = new Composite(parent, SWT.NONE); GridLayout rowLayout = new GridLayout(2, false); rowLayout.marginWidth = 0; rowLayout.marginHeight = 0; row.setLayout(rowLayout); - // Icon Label iconLabel = new Label(row, SWT.NONE); - Image icon = UiUtils.buildImageFromPngPath(iconPath); iconLabel.setImage(icon); - iconLabel.addDisposeListener(e -> { - if (icon != null && !icon.isDisposed()) { - icon.dispose(); - } - }); - // Text Label textLabel = new Label(row, SWT.NONE); textLabel.setText(text != null ? text : ""); if (tooltip != null) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowContextMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowContextMenuHandler.java index 7ff74eece..1ebdb04d9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowContextMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowContextMenuHandler.java @@ -18,9 +18,8 @@ import org.eclipse.ui.menus.CommandContributionItem; import org.eclipse.ui.menus.CommandContributionItemParameter; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.ResourceUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; - /** * Handler to show context menu items for adding files or folders to references in package explorer/project explorer. @@ -32,7 +31,6 @@ protected IContributionItem[] getContributionItems() { IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); var sel = win != null ? win.getSelectionService().getSelection() : null; - if (!(sel instanceof IStructuredSelection s) || s.isEmpty()) { return new IContributionItem[0]; } @@ -49,7 +47,7 @@ protected IContributionItem[] getContributionItems() { items.add(new Separator("com.microsoft.copilot.eclipse.ui.contextMenu.start")); - ImageDescriptor menuIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/github_copilot.png"); + ImageDescriptor menuIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT); MenuManager submenu = new MenuManager("Copilot", menuIcon, "com.microsoft.copilot.eclipse.ui.contextMenu"); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowMenuBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowMenuBarMenuHandler.java index 5ea520027..829385dc0 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowMenuBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowMenuBarMenuHandler.java @@ -32,13 +32,13 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CopilotPlan; import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.Quota; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.preferences.LanguageServerSettingManager; import com.microsoft.copilot.eclipse.ui.utils.MenuUtils; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Handler for showing GitHub Copilot menu bar menu. @@ -87,7 +87,7 @@ protected IContributionItem[] getContributionItems() { // menu: username/Sign In if (CopilotStatusResult.NOT_SIGNED_IN.equals(status)) { items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.signIn", Messages.menu_signToGitHub, - UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_SIGNIN))); } else if (CopilotStatusResult.OK.equals(status)) { String userName = authStatusManager.getUserName(); String planLabel = MenuUtils.getPlanLabel(authStatusManager.getQuotaStatus().copilotPlan()); @@ -102,7 +102,7 @@ protected IContributionItem[] getContributionItems() { // menu: openChatView items.add(new Separator()); items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.openChatView", Messages.menu_openChatView, - UiUtils.buildImageDescriptorFromPngPath("/icons/github_copilot.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT))); // menu:(label options) Turn off Completions or Turn on Completions LanguageServerSettingManager languageServerSettingManager = CopilotUi.getPlugin().getLanguageServerSettingManager(); @@ -111,29 +111,29 @@ protected IContributionItem[] getContributionItems() { String label = languageServerSettingManager.isAutoShowCompletionEnabled() ? Messages.menu_turnOffCompletions : Messages.menu_turnOnCompletions; items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.autoShowCompletions", label, - UiUtils.buildImageDescriptorFromPngPath("/icons/blank.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_BLANK))); } // menu: editKeyboardShortcuts items.add(new Separator()); items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.openEditKeyboardShortcuts", Messages.menu_editKeyboardShortcuts, - UiUtils.buildImageDescriptorFromPngPath("/icons/edit_keyboard_shortcuts.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_EDIT_KEYBOARD_SHORTCUTS))); // menu: editPreferences items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.openPreferences", Messages.menu_editPreferences, - UiUtils.buildImageDescriptorFromPngPath("/icons/edit_preferences.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_EDIT_PREFERENCES))); // menu: giveFeedback items.add(new Separator()); Map parameters = Map.of(UiConstants.OPEN_URL_PARAMETER_NAME, UiConstants.COPILOT_FEEDBACK_FORUM_URL); items.add(createCommandItem(UiConstants.OPEN_URL_COMMAND_ID, Messages.menu_giveFeedback, parameters, - UiUtils.buildImageDescriptorFromPngPath("/icons/feedback_forum.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_FEEDBACK_FORUM))); // menu: whatIsNew items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.showWhatIsNew", Messages.menu_whatIsNew, - UiUtils.buildImageDescriptorFromPngPath("/icons/blank.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_BLANK))); // menu: Copilot settings and Sign Out addAuthenticationActions(items, status); @@ -222,7 +222,7 @@ private void addCopilotUsageItemsTbb(List items, CheckQuotaRe } // Upsell actions based on the user's plan - ImageDescriptor upgradeIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/quota/upgrade.png"); + ImageDescriptor upgradeIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_UPGRADE); // For non-free users (excluding org-unlimited business/enterprise): // show "Enable Additional Usage" or "Increase Budget" depending on overage state. @@ -319,7 +319,7 @@ private void addCopilotUsageItemsLegacy(List items, CheckQuot } // Upsell actions based on the user's plan (legacy wording). - ImageDescriptor upgradeIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/quota/upgrade.png"); + ImageDescriptor upgradeIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_UPGRADE); if (plan == CopilotPlan.free) { items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.upgradeCopilotPlan", Messages.menu_quota_updateCopilotToPro, Messages.menu_quota_updateCopilotToProPlus, upgradeIcon)); @@ -339,7 +339,7 @@ private void addAuthenticationActions(List items, String stat Messages.menu_configureGitHubCopilotSettings, null)); } items.add(createCommandItem("com.microsoft.copilot.eclipse.commands.signOut", Messages.menu_signOutOfGitHub, - UiUtils.buildImageDescriptorFromPngPath("/icons/signout.png"))); + CopilotImages.getImageDescriptor(CopilotImages.IMG_SIGNOUT))); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java index 178e23e35..e2db3c69f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ShowStatusBarMenuHandler.java @@ -41,13 +41,13 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CopilotPlan; import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.Quota; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.preferences.LanguageServerSettingManager; import com.microsoft.copilot.eclipse.ui.utils.MenuUtils; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Handler for showing GitHub Copilot status bar menu. @@ -198,36 +198,33 @@ public void updateElement(UIElement element, Map parameters) { return; } else { String copilotStatus = authStatusManager.getCopilotStatus(); - String iconPath = null; + String iconKey = null; switch (copilotStatus) { case CopilotStatusResult.OK: - iconPath = "/icons/github_copilot_signed_in.png"; + iconKey = CopilotImages.IMG_GITHUB_COPILOT_SIGNED_IN; break; case CopilotStatusResult.LOADING: scheduleSpinnerJob(element); return; case CopilotStatusResult.ERROR, CopilotStatusResult.WARNING: - iconPath = "/icons/github_copilot_error.png"; + iconKey = CopilotImages.IMG_GITHUB_COPILOT_ERROR; break; case CopilotStatusResult.NOT_AUTHORIZED: - iconPath = "/icons/github_copilot_not_authorized.png"; + iconKey = CopilotImages.IMG_GITHUB_COPILOT_NOT_AUTHORIZED; break; case CopilotStatusResult.NOT_SIGNED_IN: default: - iconPath = "/icons/github_copilot_not_signed_in.png"; + iconKey = CopilotImages.IMG_GITHUB_COPILOT_NOT_SIGNED_IN; } - setIconOnDisplayThread(element, iconPath); + setIconOnDisplayThread(element, CopilotImages.getImageDescriptor(iconKey)); } } } - private void setIconOnDisplayThread(UIElement element, String iconPath) { - if (iconPath != null) { - SwtUtils.invokeOnDisplayThread(() -> { - ImageDescriptor newIcon = UiUtils.buildImageDescriptorFromPngPath(iconPath); - element.setIcon(newIcon); - }); + private void setIconOnDisplayThread(UIElement element, ImageDescriptor icon) { + if (icon != null) { + SwtUtils.invokeOnDisplayThread(() -> element.setIcon(icon)); } } @@ -236,7 +233,7 @@ private void addSignInOrUsernameAction(MenuManager menuManager) { if (CopilotStatusResult.NOT_SIGNED_IN.equals(status)) { MenuActionFactory.createMenuAction(menuManager, Messages.menu_signToGitHub, - UiUtils.buildImageDescriptorFromPngPath("/icons/signin.png"), handlerService, + CopilotImages.getImageDescriptor(CopilotImages.IMG_SIGNIN), handlerService, "com.microsoft.copilot.eclipse.commands.signIn", true); } else if (CopilotStatusResult.OK.equals(status)) { String userName = authStatusManager.getUserName(); @@ -326,7 +323,7 @@ private void addCopilotUsageActionTbb(MenuManager menuManager, CheckQuotaResult } // Upsell actions based on the user's plan - ImageDescriptor upgradeIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/quota/upgrade.png"); + ImageDescriptor upgradeIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_UPGRADE); // For non-free users (excluding org-unlimited business/enterprise): // show "Enable Additional Usage" or "Increase Budget" depending on overage state. @@ -423,7 +420,7 @@ private void addCopilotUsageActionLegacy(MenuManager menuManager, CheckQuotaResu } // Upsell actions based on the user's plan (legacy wording). - ImageDescriptor upgradeIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/quota/upgrade.png"); + ImageDescriptor upgradeIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_UPGRADE); if (plan == CopilotPlan.free) { MenuActionFactory.createMenuAction(menuManager, Messages.menu_quota_updateCopilotToPro, upgradeIcon, handlerService, "com.microsoft.copilot.eclipse.commands.upgradeCopilotPlan", true); @@ -435,7 +432,7 @@ private void addCopilotUsageActionLegacy(MenuManager menuManager, CheckQuotaResu } private void addOpenChatViewAction(MenuManager menuManager) { - ImageDescriptor icon = UiUtils.buildImageDescriptorFromPngPath("/icons/github_copilot.png"); + ImageDescriptor icon = CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT); MenuActionFactory.createMenuAction(menuManager, Messages.menu_openChatView, icon, handlerService, "com.microsoft.copilot.eclipse.commands.openChatView", true); } @@ -443,26 +440,26 @@ private void addOpenChatViewAction(MenuManager menuManager) { private void addLinkToFeedbackForumAction(MenuManager menuManager) { Map parameters = Map.of(UiConstants.OPEN_URL_PARAMETER_NAME, UiConstants.COPILOT_FEEDBACK_FORUM_URL); - ImageDescriptor feedbackIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/feedback_forum.png"); + ImageDescriptor feedbackIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_FEEDBACK_FORUM); MenuActionFactory.createMenuAction(menuManager, Messages.menu_giveFeedback, feedbackIcon, handlerService, UiConstants.OPEN_URL_COMMAND_ID, parameters, true); } private void addPreferencesAction(MenuManager menuManager) { - ImageDescriptor editPreferencesIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/edit_preferences.png"); + ImageDescriptor editPreferencesIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_EDIT_PREFERENCES); MenuActionFactory.createMenuAction(menuManager, Messages.menu_editPreferences, editPreferencesIcon, handlerService, "com.microsoft.copilot.eclipse.commands.openPreferences", true); } private void addEditKeyboardShortcutsAction(MenuManager menuManager) { - ImageDescriptor editKeyboardShortcutsIcon = UiUtils - .buildImageDescriptorFromPngPath("/icons/edit_keyboard_shortcuts.png"); + ImageDescriptor editKeyboardShortcutsIcon = + CopilotImages.getImageDescriptor(CopilotImages.IMG_EDIT_KEYBOARD_SHORTCUTS); MenuActionFactory.createMenuAction(menuManager, Messages.menu_editKeyboardShortcuts, editKeyboardShortcutsIcon, handlerService, "com.microsoft.copilot.eclipse.commands.openEditKeyboardShortcuts", true); } private void addCompletionSettingsAction(MenuManager menuManager) { - ImageDescriptor placeHolder = UiUtils.buildImageDescriptorFromPngPath("/icons/blank.png"); + ImageDescriptor placeHolder = CopilotImages.getImageDescriptor(CopilotImages.IMG_BLANK); if (languageServerSettingManager.isAutoShowCompletionEnabled()) { MenuActionFactory.createMenuAction(menuManager, Messages.menu_turnOffCompletions, placeHolder, handlerService, "com.microsoft.copilot.eclipse.commands.autoShowCompletions", true); @@ -473,7 +470,7 @@ private void addCompletionSettingsAction(MenuManager menuManager) { } private void addShowWhatIsNewAction(MenuManager menuManager) { - ImageDescriptor placeHolder = UiUtils.buildImageDescriptorFromPngPath("/icons/blank.png"); + ImageDescriptor placeHolder = CopilotImages.getImageDescriptor(CopilotImages.IMG_BLANK); MenuActionFactory.createMenuAction(menuManager, Messages.menu_whatIsNew, placeHolder, handlerService, "com.microsoft.copilot.eclipse.commands.showWhatIsNew", true); } @@ -499,7 +496,7 @@ private void addAuthenticationActions(MenuManager menuManager) { handlerService, "com.microsoft.copilot.eclipse.commands.configureCopilotSettings", true); } // Only show sign out action when the user is in OK, NOT_AUTHORIZED, WARNING, or ERROR state. - ImageDescriptor signOutIcon = UiUtils.buildImageDescriptorFromPngPath("/icons/signout.png"); + ImageDescriptor signOutIcon = CopilotImages.getImageDescriptor(CopilotImages.IMG_SIGNOUT); MenuActionFactory.createMenuAction(menuManager, Messages.menu_signOutOfGitHub, signOutIcon, handlerService, "com.microsoft.copilot.eclipse.commands.signOut", true); } @@ -607,7 +604,7 @@ protected IStatus run(IProgressMonitor monitor) { if (this.uiElement == null) { throw new IllegalStateException("UI element is not set. Spinner cannot be set."); } - setIconOnDisplayThread(this.uiElement, String.format("/icons/spinner/%d.png", currentIconIndex)); + setIconOnDisplayThread(this.uiElement, CopilotImages.getSpinnerFrameDescriptor(currentIconIndex)); currentIconIndex = (currentIconIndex % TOTAL_SPINNER_ICONS) + 1; if (CopilotCore.getPlugin().getAuthStatusManager() != null && CopilotCore.getPlugin().getAuthStatusManager().isLoading()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ToolConfigurationContextMenuHandler.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ToolConfigurationContextMenuHandler.java index 89635a039..1ef1ff156 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ToolConfigurationContextMenuHandler.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/handlers/ToolConfigurationContextMenuHandler.java @@ -19,6 +19,7 @@ import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.CopilotCore; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -41,7 +42,7 @@ protected IContributionItem[] getContributionItems() { // Create submenu for Copilot tool configuration MenuManager submenu = new MenuManager("Copilot", - UiUtils.buildImageDescriptorFromPngPath("/icons/github_copilot.png"), + CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT), "com.microsoft.copilot.eclipse.ui.toolConfigMenu"); // Add "Configure Tools..." command @@ -49,7 +50,7 @@ protected IContributionItem[] getContributionItems() { CommandContributionItemParameter param = new CommandContributionItemParameter(window, null, "com.microsoft.copilot.eclipse.commands.configureTools", CommandContributionItem.STYLE_PUSH); param.label = "Configure Tools..."; - param.icon = UiUtils.buildImageDescriptorFromPngPath("/icons/chat/tools.png"); + param.icon = CopilotImages.getImageDescriptor(CopilotImages.IMG_CHAT_TOOLS); submenu.add(new CommandContributionItem(param)); items.add(submenu); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/ActionMenu.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/ActionMenu.java index 557503c04..ddaea8f2a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/ActionMenu.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/ActionMenu.java @@ -15,7 +15,7 @@ import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; /** * Action menu for accepting/rejecting suggestions. @@ -68,8 +68,8 @@ public void show(int x, int y) { String acceptLabel = buildAlignedMenuLabel(Messages.actionMenu_accept, "Tab"); String rejectLabel = buildAlignedMenuLabel(Messages.actionMenu_reject, "Esc"); - ImageDescriptor acceptImage = UiUtils.buildImageDescriptorFromPngPath("/icons/chat/keyboard-tab.png"); - ImageDescriptor cancelImage = UiUtils.buildImageDescriptorFromPngPath("/icons/close.png"); + ImageDescriptor acceptImage = CopilotImages.getImageDescriptor(CopilotImages.IMG_CHAT_KEYBOARD_TAB); + ImageDescriptor cancelImage = CopilotImages.getImageDescriptor(CopilotImages.IMG_CLOSE); menuManager.add(new Action(acceptLabel, acceptImage) { @Override diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java index bc48eb031..00da9d29f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java @@ -32,6 +32,7 @@ import org.eclipse.ui.texteditor.rulers.IContributedRulerColumn; import org.eclipse.ui.texteditor.rulers.RulerColumnDescriptor; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.completion.EditorsManager; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -263,7 +264,7 @@ private Rectangle getSuggestionIconBounds(int expectedModelLine, int expectedWid private void ensureIcon() { if (icon == null || icon.isDisposed()) { - ImageDescriptor desc = UiUtils.buildImageDescriptorFromPngPath("/icons/chat/gutter-arrow.png"); + ImageDescriptor desc = CopilotImages.getImageDescriptor(CopilotImages.IMG_CHAT_GUTTER_ARROW); if (desc != null) { icon = desc.createImage(true); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddApiKeyDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddApiKeyDialog.java index aa6f8eaa4..6b4b9ff9d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddApiKeyDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddApiKeyDialog.java @@ -22,6 +22,7 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModelProvider; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -93,21 +94,12 @@ protected Control createDialogArea(Composite parent) { apiKeyText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); apiKeyText.addModifyListener(this::onFieldChanged); - eyeOpenImg = UiUtils - .buildImageFromPngPath(UiUtils.isDarkTheme() ? "/icons/chat/eye_dark.png" : "/icons/chat/eye.png"); - eyeClosedImg = UiUtils.buildImageFromPngPath( - UiUtils.isDarkTheme() ? "/icons/chat/eye_closed_dark.png" : "/icons/chat/eye_closed.png"); + eyeOpenImg = CopilotImages.getThemedImage(CopilotImages.IMG_CHAT_EYE, CopilotImages.IMG_CHAT_EYE_DARK); + eyeClosedImg = CopilotImages.getThemedImage(CopilotImages.IMG_CHAT_EYE_CLOSED, + CopilotImages.IMG_CHAT_EYE_CLOSED_DARK); toggleEyeBtn = new Button(apiKeyRow, SWT.PUSH); toggleEyeBtn.setImage(eyeClosedImg); toggleEyeBtn.addListener(SWT.Selection, e -> togglePasswordVisibility()); - toggleEyeBtn.addDisposeListener(e -> { - if (eyeOpenImg != null && !eyeOpenImg.isDisposed()) { - eyeOpenImg.dispose(); - } - if (eyeClosedImg != null && !eyeClosedImg.isDisposed()) { - eyeClosedImg.dispose(); - } - }); return container; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddByokModelDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddByokModelDialog.java index 4d20ddf62..adba96286 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddByokModelDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AddByokModelDialog.java @@ -24,6 +24,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModelCapabilities; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModelProvider; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -135,23 +136,14 @@ private void createAzureSpecificFields(Composite container) { apiKeyText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); apiKeyText.addModifyListener(this::onFieldChanged); - eyeOpenImg = UiUtils - .buildImageFromPngPath(UiUtils.isDarkTheme() ? "/icons/chat/eye_dark.png" : "/icons/chat/eye.png"); - eyeClosedImg = UiUtils.buildImageFromPngPath( - UiUtils.isDarkTheme() ? "/icons/chat/eye_closed_dark.png" : "/icons/chat/eye_closed.png"); + eyeOpenImg = CopilotImages.getThemedImage(CopilotImages.IMG_CHAT_EYE, CopilotImages.IMG_CHAT_EYE_DARK); + eyeClosedImg = CopilotImages.getThemedImage(CopilotImages.IMG_CHAT_EYE_CLOSED, + CopilotImages.IMG_CHAT_EYE_CLOSED_DARK); toggleEyeBtn = new Button(apiKeyRow, SWT.FLAT); toggleEyeBtn.setImage(eyeClosedImg); toggleEyeBtn.setLayoutData(new GridData(SWT.END, SWT.CENTER, false, false)); toggleEyeBtn.addListener(SWT.Selection, e -> togglePasswordVisibility()); - toggleEyeBtn.addDisposeListener(e -> { - if (eyeOpenImg != null && !eyeOpenImg.isDisposed()) { - eyeOpenImg.dispose(); - } - if (eyeClosedImg != null && !eyeClosedImg.isDisposed()) { - eyeClosedImg.dispose(); - } - }); } /** diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java index 1392a5294..fbcc7cb7f 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java @@ -49,10 +49,10 @@ import com.microsoft.copilot.eclipse.core.FeatureFlags; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.byok.ByokModelProvider; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ByokService; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * BYOK (Bring Your Own Key) preference page for configuring AI model providers. This page allows users to add, remove, @@ -269,16 +269,9 @@ private Composite createContentArea(Composite parent) { viewerStack.setLayout(stackLayout); GridData stackData = new GridData(SWT.FILL, SWT.FILL, true, true); viewerStack.setLayoutData(stackData); - enabledIcon = UiUtils.buildImageFromPngPath("/icons/chat/keep.png"); + enabledIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_KEEP); disabledIcon = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ETOOL_DELETE); - viewerStack.addDisposeListener(e -> { - if (enabledIcon != null && !enabledIcon.isDisposed()) { - enabledIcon.dispose(); - enabledIcon = null; - } - }); - // Create tree viewer composite treeComposite = createTreeViewer(viewerStack); // Create loading overlay diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java index c41975d0c..6c5a7ad0c 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java @@ -70,6 +70,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.RegistryAccess; import com.microsoft.copilot.eclipse.core.lsp.protocol.LanguageModelToolInformation; import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.McpExtensionPointManager; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.McpRegistryDialog; @@ -403,20 +404,14 @@ private void createExtMcpRegistrationArea(Composite parent) { var service = CopilotUi.getPlugin().getChatServiceManager().getMcpConfigService(); boolean newExtMcpRegFound = service.isNewExtMcpRegFound(); if (newExtMcpRegFound) { - if (redNotice == null || redNotice.isDisposed()) { - redNotice = UiUtils.buildImageFromPngPath("/icons/chat/red_notice.png"); + if (redNotice == null) { + redNotice = CopilotImages.getImage(CopilotImages.IMG_CHAT_RED_NOTICE); } redNoticeLabel = new Label(extMcpTitleComposite, SWT.NONE); redNoticeLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); redNoticeLabel.setImage(redNotice); redNoticeLabel .setToolTipText(com.microsoft.copilot.eclipse.ui.i18n.Messages.chat_actionBar_toolButton_detected_toolTip); - redNoticeLabel.addDisposeListener(e -> { - if (redNotice != null && !redNotice.isDisposed()) { - redNotice.dispose(); - redNotice = null; - } - }); } // Title label @@ -450,11 +445,6 @@ public void disposeNoticeIcon() { redNoticeLabel = null; } - if (redNotice != null && !redNotice.isDisposed()) { - redNotice.dispose(); - redNotice = null; - } - // Refresh layout to reflect changes immediately extMcpTitleComposite.requestLayout(); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickstart/FeaturePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickstart/FeaturePage.java index 283b736f5..98d0556eb 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickstart/FeaturePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/quickstart/FeaturePage.java @@ -28,6 +28,7 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; @@ -47,7 +48,6 @@ public enum Feature { private Composite selectedCard; private Color normalBackgroundColor; private Color selectedBackgroundColor; - private Image currentContentImage; private Display display; private boolean isDarkTheme; @@ -179,12 +179,12 @@ private void createLeftPanel(Composite parent) { leftPanel.setData(CSSSWTConstants.CSS_ID_KEY, "quick-start-container"); // Create clickable feature cards (Agent selected by default) - selectedCard = createClickableFeatureCard(leftPanel, "/icons/github_copilot.png", Messages.quickStart_agent_title, - Messages.quickStart_agent_description, Feature.AGENT); - createClickableFeatureCard(leftPanel, "/icons/chat/chatview_icon_chat.png", Messages.quickStart_ask_title, - Messages.quickStart_ask_description, Feature.ASK); - createClickableFeatureCard(leftPanel, "/icons/chat/chatview_icon_code.png", Messages.quickStart_completion_title, - Messages.quickStart_completion_description, Feature.COMPLETION); + selectedCard = createClickableFeatureCard(leftPanel, CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT), + Messages.quickStart_agent_title, Messages.quickStart_agent_description, Feature.AGENT); + createClickableFeatureCard(leftPanel, CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_CHAT), + Messages.quickStart_ask_title, Messages.quickStart_ask_description, Feature.ASK); + createClickableFeatureCard(leftPanel, CopilotImages.getImage(CopilotImages.IMG_CHATVIEW_ICON_CODE), + Messages.quickStart_completion_title, Messages.quickStart_completion_description, Feature.COMPLETION); } private void createRightPanel(Composite parent) { @@ -198,11 +198,6 @@ private void createRightPanel(Composite parent) { // Content area that will change based on selection rightPanelContent = new Label(rightPanel, SWT.WRAP | SWT.CENTER); rightPanelContent.setData(CSSSWTConstants.CSS_ID_KEY, "quick-start-feature-card"); - rightPanelContent.addDisposeListener(e -> { - if (currentContentImage != null && !currentContentImage.isDisposed()) { - currentContentImage.dispose(); - } - }); // Add resize listener to properly size the content rightPanel.addListener(SWT.Resize, e -> { @@ -213,7 +208,7 @@ private void createRightPanel(Composite parent) { updateRightPanelContent(Feature.AGENT); } - private Composite createClickableFeatureCard(Composite parent, String imagePath, String title, String description, + private Composite createClickableFeatureCard(Composite parent, Image iconImage, String title, String description, Feature feature) { // Create a canvas for custom rounded painting Canvas cardCanvas = new Canvas(parent, SWT.NONE); @@ -284,8 +279,7 @@ public void mouseDown(MouseEvent e) { iconTitleComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); iconTitleComposite.setData(CSSSWTConstants.CSS_ID_KEY, "quick-start-feature-card"); - // Icon - Load image from path using UiUtils - Image iconImage = UiUtils.buildImageFromPngPath(imagePath); + // Icon Label iconLabel = new Label(iconTitleComposite, SWT.NONE); iconLabel.setImage(iconImage); iconLabel.setData(CSSSWTConstants.CSS_ID_KEY, "quick-start-feature-card"); @@ -294,13 +288,6 @@ public void mouseDown(MouseEvent e) { iconData.heightHint = 20; iconLabel.setLayoutData(iconData); - // Dispose image when label is disposed - iconLabel.addDisposeListener(e -> { - if (iconImage != null && !iconImage.isDisposed()) { - iconImage.dispose(); - } - }); - // Make icon clickable too addClickableToControls(clickHandler, iconLabel); @@ -434,28 +421,20 @@ private void updateChildrenControlBackground(Control control, boolean selected) } private void updateRightPanelContent(Feature feature) { - // Dispose previous image if it exists - if (currentContentImage != null && !currentContentImage.isDisposed()) { - currentContentImage.dispose(); - currentContentImage = null; - } - - String imagePath; + String key; switch (feature) { case ASK: - imagePath = "/intro/quickstart/quick_start_ask.png"; + key = CopilotImages.IMG_QUICKSTART_ASK; break; case COMPLETION: - imagePath = "/intro/quickstart/quick_start_completion.png"; + key = CopilotImages.IMG_QUICKSTART_COMPLETION; break; case AGENT: default: - imagePath = "/intro/quickstart/quick_start_agent.png"; + key = CopilotImages.IMG_QUICKSTART_AGENT; } - // Load and set the new image - currentContentImage = UiUtils.buildImageFromPngPath(imagePath); - rightPanelContent.setImage(currentContentImage); + rightPanelContent.setImage(CopilotImages.getImage(key)); rightPanelContent.requestLayout(); } @@ -483,8 +462,8 @@ private void disposeColors() { * Creates a close button with hover effects. */ private Label createCloseButton(Composite parent) { - Image normalImage = UiUtils - .buildImageFromPngPath(isDarkTheme ? "/intro/quickstart/close_dark.png" : "/intro/quickstart/close_light.png"); + Image normalImage = CopilotImages.getThemedImage( + CopilotImages.IMG_QUICKSTART_CLOSE_LIGHT, CopilotImages.IMG_QUICKSTART_CLOSE_DARK); Label closeButton = new Label(parent, SWT.NONE); closeButton.setImage(normalImage); closeButton.setData(CSSSWTConstants.CSS_ID_KEY, "quick-start-container"); @@ -501,17 +480,8 @@ public void mouseDown(MouseEvent e) { } }); - Image hoverImage = UiUtils.buildImageFromPngPath( - isDarkTheme ? "/intro/quickstart/close_hover_dark.png" : "/intro/quickstart/close_hover_light.png"); - // Add dispose listener for close button images - closeButton.addDisposeListener(e -> { - if (normalImage != null && !normalImage.isDisposed()) { - normalImage.dispose(); - } - if (hoverImage != null && !hoverImage.isDisposed()) { - hoverImage.dispose(); - } - }); + Image hoverImage = CopilotImages.getThemedImage( + CopilotImages.IMG_QUICKSTART_CLOSE_HOVER_LIGHT, CopilotImages.IMG_QUICKSTART_CLOSE_HOVER_DARK); // Add hover effect for close button closeButton.addMouseTrackListener(new MouseTrackAdapter() { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownButton.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownButton.java index 6799c4a01..6b9d47902 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownButton.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownButton.java @@ -19,9 +19,9 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.utils.AccessibilityUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A custom-painted dropdown button composite. @@ -44,8 +44,6 @@ public class DropdownButton extends Composite { private static final int H_PADDING = 4; private static final int ARROW_AREA_WIDTH = 16; - private static Image arrowIcon; - private final DropdownPopup popup; private List itemGroups; private String selectedItemId; @@ -61,13 +59,6 @@ public DropdownButton(Composite parent, int style) { super(parent, style | SWT.NONE); popup = new DropdownPopup(getShell(), this); - if (arrowIcon == null || arrowIcon.isDisposed()) { - arrowIcon = UiUtils.isDarkTheme() - ? UiUtils.buildImageFromPngPath("/icons/dropdown/down_arrow_dark.png") - : UiUtils.buildImageFromPngPath("/icons/dropdown/down_arrow.png"); - getDisplay().addListener(SWT.Dispose, e -> disposeStaticIcons()); - } - addPaintListener(e -> paintControl(e.gc)); addMouseListener(new MouseAdapter() { @@ -158,13 +149,6 @@ public void setAccessibilityName(String name) { AccessibilityUtils.addAccessibilityNameForUiComponent(this, name); } - private static void disposeStaticIcons() { - if (arrowIcon != null && !arrowIcon.isDisposed()) { - arrowIcon.dispose(); - arrowIcon = null; - } - } - private void togglePopup() { if (popup.isOpen()) { popup.close(); @@ -185,6 +169,8 @@ private void openPopup() { private void paintControl(GC gc) { Rectangle bounds = getClientArea(); Display display = getDisplay(); + Image arrowIcon = CopilotImages.getThemedImage(CopilotImages.IMG_DROPDOWN_DOWN_ARROW, + CopilotImages.IMG_DROPDOWN_DOWN_ARROW_DARK); DropdownItem selected = findItemById(selectedItemId); Image selectedIcon = getSelectedItemIcon(selected); Color bg = mouseHover ? CssConstants.getButtonFocusBgColor(display) : getBackground(); @@ -249,9 +235,10 @@ public Point computeSize(int widthHint, int heightHint, boolean changed) { if (selectedIcon != null) { iconWidth = selectedIcon.getBounds().width + ICON_TEXT_GAP; } - Image arrow = arrowIcon; - int arrowWidth = arrowIcon != null && !arrowIcon.isDisposed() - ? arrowIcon.getBounds().width : ARROW_AREA_WIDTH; + Image arrow = CopilotImages.getThemedImage(CopilotImages.IMG_DROPDOWN_DOWN_ARROW, + CopilotImages.IMG_DROPDOWN_DOWN_ARROW_DARK); + int arrowWidth = arrow != null && !arrow.isDisposed() + ? arrow.getBounds().width : ARROW_AREA_WIDTH; int width = H_PADDING + iconWidth + textExtent.x + arrowWidth; int height = getContentHeight(textExtent, selectedIcon, arrow) + 2 * UiConstants.BTN_PADDING; if (widthHint != SWT.DEFAULT) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownPopup.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownPopup.java index 344eb79c2..7f564f53b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownPopup.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/DropdownPopup.java @@ -29,8 +29,8 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.utils.PlatformUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A generic popup shell for {@link DropdownButton}. @@ -51,8 +51,6 @@ class DropdownPopup { private static final int MAX_VISIBLE_ITEMS = 15; private static final int SHORT_POPUP_WIDTH = 250; - private static Image checkIcon; - private Shell shell; private final Shell parentShell; private Consumer selectionListener; @@ -81,26 +79,12 @@ private record ItemEntry(DropdownItem item, Composite composite, ItemController this.parentShell = parentShell; this.anchorControl = anchorControl; this.stylingEngine = PlatformUI.getWorkbench().getService(IStylingEngine.class); - - if (checkIcon == null || checkIcon.isDisposed()) { - checkIcon = UiUtils.isDarkTheme() - ? UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_complete_status_dark.png") - : UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_complete_status.png"); - parentShell.getDisplay().addListener(SWT.Dispose, e -> disposeStaticIcons()); - } } void setSelectionListener(Consumer listener) { this.selectionListener = listener; } - private static void disposeStaticIcons() { - if (checkIcon != null && !checkIcon.isDisposed()) { - checkIcon.dispose(); - checkIcon = null; - } - } - /** * Opens the popup at the given screen location with the provided item groups. * @@ -528,8 +512,9 @@ public void mouseDown(MouseEvent e) { } private Image resolvePopupLeadingIcon(DropdownItem item, boolean selected) { - if (selected && checkIcon != null && !checkIcon.isDisposed()) { - return checkIcon; + if (selected) { + return CopilotImages.getThemedImage(CopilotImages.IMG_DROPDOWN_COMPLETE_STATUS, + CopilotImages.IMG_DROPDOWN_COMPLETE_STATUS_DARK); } Image icon = item.getIcon(); return icon != null && !icon.isDisposed() ? icon : null; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java index 425a6b543..011db3eac 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/ModelHoverContentProvider.java @@ -28,11 +28,11 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCapabilitiesSupports; import com.microsoft.copilot.eclipse.core.lsp.protocol.CopilotModel.CopilotModelCustomModel; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.chat.services.ModelService; import com.microsoft.copilot.eclipse.ui.i18n.Messages; import com.microsoft.copilot.eclipse.ui.utils.ModelUtils; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * Renders the full hover UI for model items in the model picker dropdown. The layout consists of the bold title header, @@ -49,8 +49,6 @@ public class ModelHoverContentProvider implements IDropdownItemHoverProvider { /** Vertical padding inside a thinking effort row, so the hover background has breathing room. */ private static final int THINKING_EFFORT_ROW_V_PADDING = 2; - private static Image effortCheckIcon; - private final CopilotModel model; private final IStylingEngine stylingEngine; @@ -213,7 +211,7 @@ private void addThinkingEffortOption(Composite parent, ModelService modelService row.setLayout(rowLayout); row.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); - Image checkIcon = getCheckIcon(parent); + Image checkIcon = getCheckIcon(); Label iconLabel = new Label(row, SWT.NONE); GridData iconGd = new GridData(SWT.LEFT, SWT.CENTER, false, false); if (checkIcon != null) { @@ -317,28 +315,14 @@ private Composite createKeyValueRow(Composite parent) { return row; } - private static void disposeStaticIcons() { - if (effortCheckIcon != null && !effortCheckIcon.isDisposed()) { - effortCheckIcon.dispose(); - effortCheckIcon = null; - } - } - /** * Returns the cached check-mark image used to indicate the selected thinking effort row, lazily loaded on first * access. The icon shares the asset used by the dropdown popup so the leading column lines up visually with the * checkmarks shown next to selected model items. */ - private static Image getCheckIcon(Composite parent) { - if (effortCheckIcon == null || effortCheckIcon.isDisposed()) { - effortCheckIcon = UiUtils.isDarkTheme() - ? UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_complete_status_dark.png") - : UiUtils.buildImageFromPngPath("/icons/dropdown/dropdown_complete_status.png"); - if (parent != null && !parent.isDisposed()) { - parent.getDisplay().addListener(SWT.Dispose, e -> disposeStaticIcons()); - } - } - return effortCheckIcon; + private static Image getCheckIcon() { + return CopilotImages.getThemedImage(CopilotImages.IMG_DROPDOWN_COMPLETE_STATUS, + CopilotImages.IMG_DROPDOWN_COMPLETE_STATUS_DARK); } private static boolean isPositive(Integer value) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java index 7ebafdb58..4fb7b9a21 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java @@ -7,7 +7,7 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; /** * Drives a rotating spinner animation on a target {@link Label}. @@ -21,8 +21,8 @@ * 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; + /** Total number of frames. */ + private static final int TOTAL_FRAMES = CopilotImages.SPINNER_FRAMES.length; /** Per-frame interval in milliseconds. */ private static final int FRAME_INTERVAL_MS = 100; @@ -58,10 +58,6 @@ 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. @@ -88,13 +84,10 @@ public void stop() { 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)); + return CopilotImages.getSpinnerFrame(frame); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java index debbcbef8..ee4ff4dfd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java @@ -18,6 +18,7 @@ import com.microsoft.copilot.eclipse.core.lsp.mcp.RegistryAccess; import com.microsoft.copilot.eclipse.core.lsp.mcp.registry.Icon; import com.microsoft.copilot.eclipse.core.lsp.protocol.NullParams; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.CopilotUi; import com.microsoft.copilot.eclipse.ui.dialogs.mcp.Messages; import com.microsoft.copilot.eclipse.ui.preferences.CopilotPreferenceInitializer; @@ -157,11 +158,11 @@ public static CompletableFuture loadServerIcon(String iconUrl, int width, } /** - * Builds the default MCP server icon image. + * Loads the shared default MCP server icon. * - * @return the default MCP icon image, or {@code null} if it cannot be loaded + * @return the default MCP icon image; callers must not dispose it */ public static Image loadDefaultServerIcon() { - return UiUtils.buildImageFromPngPath("/icons/mcp/mcp_default_icon.png"); + return CopilotImages.getImage(CopilotImages.IMG_MCP_DEFAULT_ICON); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/MenuUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/MenuUtils.java index 7af8ba999..cf24b4371 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/MenuUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/MenuUtils.java @@ -19,6 +19,7 @@ import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CheckQuotaResult; import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.CopilotPlan; import com.microsoft.copilot.eclipse.core.lsp.protocol.quota.Quota; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.i18n.Messages; /** @@ -82,19 +83,19 @@ public static double calculatePercentRemaining(CheckQuotaResult quotaStatus) { */ public static ImageDescriptor getUsageIcon(double percentRemaining) { if (percentRemaining <= 10) { - return UiUtils.buildImageDescriptorFromPngPath("/icons/quota/usage_red.png"); + return CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_USAGE_RED); } if (percentRemaining <= 25) { - return UiUtils.buildImageDescriptorFromPngPath("/icons/quota/usage_yellow.png"); + return CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_USAGE_YELLOW); } - return UiUtils.buildImageDescriptorFromPngPath("/icons/quota/usage_blue.png"); + return CopilotImages.getImageDescriptor(CopilotImages.IMG_QUOTA_USAGE_BLUE); } /** * Returns the shared blank icon descriptor used for indented usage rows. */ public static ImageDescriptor getBlankIcon() { - return UiUtils.buildImageDescriptorFromPngPath("/icons/blank.png"); + return CopilotImages.getImageDescriptor(CopilotImages.IMG_BLANK); } /** From 2e88e7e5631c5d50f248b8878570e74fc042172e Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Tue, 21 Jul 2026 11:43:37 +0200 Subject: [PATCH 03/19] Use image registries to access shared images from other plug-ins (#364) Re-direct calls of PlatformUI.getWorkbench().getSharedImages().getImage(...) to the image registry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/copilot/eclipse/ui/jobs/views/JobsView.java | 7 +++---- .../com/microsoft/copilot/eclipse/ui/chat/ActionBar.java | 2 +- .../copilot/eclipse/ui/chat/AgentStatusLabel.java | 2 +- .../com/microsoft/copilot/eclipse/ui/chat/ErrorWidget.java | 4 ++-- .../microsoft/copilot/eclipse/ui/chat/ReferencedFile.java | 3 +-- .../copilot/eclipse/ui/chat/SourceViewerComposite.java | 6 +++--- .../microsoft/copilot/eclipse/ui/chat/StaticBanner.java | 6 +++--- .../com/microsoft/copilot/eclipse/ui/chat/WarnWidget.java | 4 ++-- .../microsoft/copilot/eclipse/ui/chat/WorkingSetBar.java | 4 ++-- .../copilot/eclipse/ui/chat/viewers/ChatHistoryViewer.java | 2 +- .../copilot/eclipse/ui/dialogs/mcp/McpApprovalDialog.java | 2 +- .../eclipse/ui/preferences/AutoApprovePreferencePage.java | 5 ++--- .../copilot/eclipse/ui/preferences/ByokPreferencePage.java | 2 +- .../eclipse/ui/preferences/GlobalAutoApproveSection.java | 5 ++--- .../copilot/eclipse/ui/preferences/McpPreferencePage.java | 4 ++-- 15 files changed, 27 insertions(+), 31 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java index e5774d3a5..a3352d738 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/views/JobsView.java @@ -42,7 +42,6 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.lsp.CopilotLanguageServerConnection; @@ -568,13 +567,13 @@ private Image getStatusIconForLabel(String nodeText) { return directoryIcon; } else { // This is an error message or unknown status, use warning icon - return PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_WARN_TSK); + return CopilotJobsImages.getSharedImage(ISharedImages.IMG_OBJS_WARN_TSK); } } private Image getStatusIconForPr(GitHubPullRequestItem pr) { if (pr == null || pr.copilotWorkStatus() == null) { - return PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_WARN_TSK); + return CopilotJobsImages.getSharedImage(ISharedImages.IMG_OBJS_WARN_TSK); } switch (pr.copilotWorkStatus()) { @@ -584,7 +583,7 @@ private Image getStatusIconForPr(GitHubPullRequestItem pr) { } return this.completeIcon; case error: - return PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_WARN_TSK); + return CopilotJobsImages.getSharedImage(ISharedImages.IMG_OBJS_WARN_TSK); case in_progress: default: if (this.loadingIcon == null) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java index 64555a886..43930884d 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ActionBar.java @@ -866,7 +866,7 @@ private void updateButtonState(SendOrCancelButtonStates state) { break; case CANCEL_ENABLED: isSendButton = false; - Image cancelImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_STOP); + Image cancelImage = CopilotImages.getSharedImage(ISharedImages.IMG_ELCL_STOP); updateSendOrCancelMsgBtn(true, cancelImage, Messages.chat_actionBar_cancelButton_Tooltip); updateSendToJobBtn(false); break; diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java index 4c9366c3d..de6ebc8bd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/AgentStatusLabel.java @@ -103,7 +103,7 @@ public void setErrorStatus() { if (this.status == Status.RUNNING) { spinner.stop(); } - iconLabel.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_ERROR_TSK)); + iconLabel.setImage(CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_ERROR_TSK)); this.status = Status.ERROR; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ErrorWidget.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ErrorWidget.java index 9debb663e..26b7fb18b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ErrorWidget.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ErrorWidget.java @@ -9,8 +9,8 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.swt.WrapLabel; /** @@ -40,7 +40,7 @@ private void buildWarnLabelWithIcon(String message) { composite.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); Label icon = new Label(composite, SWT.CENTER); - icon.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_ERROR_TSK)); + icon.setImage(CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_ERROR_TSK)); WrapLabel label = new WrapLabel(composite, SWT.LEFT); label.setText(message); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java index bbfb0544b..050d5d487 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ReferencedFile.java @@ -24,7 +24,6 @@ import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; import org.eclipse.ui.model.WorkbenchLabelProvider; import com.microsoft.copilot.eclipse.core.Constants; @@ -197,7 +196,7 @@ public void getName(AccessibleEvent event) { */ private void setupUnsupportedFileDisplay() { // Set warning icon - lblfileIcon.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJS_WARN_TSK)); + lblfileIcon.setImage(CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_WARN_TSK)); // Set tooltip with model name String modelName = CopilotUi.getPlugin().getChatServiceManager().getModelService().getActiveModel() .getModelName(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java index eb58b39ad..73f475125 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java @@ -29,13 +29,13 @@ import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCodeCopyParams; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; import com.microsoft.copilot.eclipse.ui.utils.AccessibilityUtils; @@ -173,7 +173,7 @@ private Composite createActionsComposite() { layout.pack = true; // Pack the composite tightly result.setLayout(layout); - this.copyIcon = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_COPY); + this.copyIcon = CopilotImages.getSharedImage(ISharedImages.IMG_TOOL_COPY); Button copyButton = createActionButton(result, SWT.PUSH | SWT.FLAT, copyIcon, "Copy", "Copy to clipboard", Messages.sourceViewerComposite_copyButtonAccessibilityName); copyButton.addListener(SWT.Selection, e -> { @@ -187,7 +187,7 @@ private Composite createActionsComposite() { ImageDescriptor insertDesc = AbstractUIPlugin.imageDescriptorFromPlugin(UiConstants.WORKBENCH_TEXTEDITOR, UiConstants.INSERT_ICON); this.insertIcon = Optional.ofNullable(insertDesc).map(desc -> desc.createImage()).orElse(null); - Image pasteIcon = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_PASTE); + Image pasteIcon = CopilotImages.getSharedImage(ISharedImages.IMG_TOOL_PASTE); Image insertButtonIcon = this.insertIcon != null ? this.insertIcon : pasteIcon; Button insertButton = createActionButton(result, SWT.PUSH | SWT.FLAT, insertButtonIcon, "Insert", "Insert into editor", Messages.sourceViewerComposite_insertButtonAccessibilityName); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/StaticBanner.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/StaticBanner.java index ec340b7e4..26a257aff 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/StaticBanner.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/StaticBanner.java @@ -18,8 +18,8 @@ import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** @@ -64,7 +64,7 @@ public StaticBanner(Composite parent, int style, String message, List Date: Tue, 21 Jul 2026 14:14:52 +0200 Subject: [PATCH 04/19] Simplify WrappableIconLink to take only an Image parameter (#364) Drop the icon-path/shared-image factory variants of WrappableIconLink in favor of a single create(...) that accepts an already-resolved Image. Callers now pass registry images (CopilotImages.IMG_INFORMATION) or shared workbench images directly, and the widget no longer owns or disposes the icon. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AutoApprovePreferencePage.java | 2 +- .../ui/preferences/ByokPreferencePage.java | 2 +- .../CustomModesPreferencePage.java | 3 +- .../ui/preferences/McpPreferencePage.java | 6 +- .../ui/preferences/WrappableIconLink.java | 63 +++++-------------- 5 files changed, 22 insertions(+), 54 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AutoApprovePreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AutoApprovePreferencePage.java index ac25e40da..2495a5bdb 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AutoApprovePreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/AutoApprovePreferencePage.java @@ -47,7 +47,7 @@ public void init(IWorkbench workbench) { protected Control createContents(Composite parent) { FeatureFlags flags = CopilotCore.getPlugin().getFeatureFlags(); if (flags != null && !flags.isAutoApprovalEnabled()) { - return WrappableIconLink.createWithSharedImage(parent, + return WrappableIconLink.create(parent, CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_INFO_TSK), Messages.preferences_page_auto_approve_disabled_by_organization); } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java index 95b0ac39d..feb7e2348 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/ByokPreferencePage.java @@ -177,7 +177,7 @@ protected Control createContents(Composite parent) { disabledCompositeLayout.marginWidth = 0; disabledCompositeLayout.marginHeight = 0; disabledComposite.setLayout(disabledCompositeLayout); - WrappableIconLink.createWithCustomizedImage(disabledComposite, "/icons/information.png", + WrappableIconLink.create(disabledComposite, CopilotImages.getImage(CopilotImages.IMG_INFORMATION), Messages.preferences_page_byok_disabled_tip); contentComposite = createByokView(pageStateStack); updatePageState(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CustomModesPreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CustomModesPreferencePage.java index 49657b8ed..b6a98e20a 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CustomModesPreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/CustomModesPreferencePage.java @@ -55,6 +55,7 @@ import com.microsoft.copilot.eclipse.core.chat.CustomChatModeManager; import com.microsoft.copilot.eclipse.core.chat.service.ICustomModeService; import com.microsoft.copilot.eclipse.core.utils.WorkspaceUtils; +import com.microsoft.copilot.eclipse.ui.CopilotImages; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; /** @@ -90,7 +91,7 @@ protected Control createContents(Composite parent) { disabledCompositeLayout.marginHeight = 0; disabledComposite.setLayout(disabledCompositeLayout); disabledComposite.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - WrappableIconLink.createWithCustomizedImage(disabledComposite, "/icons/information.png", + WrappableIconLink.create(disabledComposite, CopilotImages.getImage(CopilotImages.IMG_INFORMATION), Messages.setting_disabled_by_organization); return container; } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java index f3f2c9486..c4424b7b6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/McpPreferencePage.java @@ -168,7 +168,7 @@ protected Control createContents(Composite parent) { // Create a simple note for the feature disabled case FeatureFlags flags = CopilotCore.getPlugin().getFeatureFlags(); if (flags != null && !flags.isMcpEnabled()) { - return WrappableIconLink.createWithSharedImage(parent, + return WrappableIconLink.create(parent, CopilotImages.getSharedImage(ISharedImages.IMG_OBJS_INFO_TSK), Messages.preferences_page_mcp_disabled_tip); } @@ -378,8 +378,8 @@ private void showRegistryInfoMessage(String message) { } // Create info message composite using WrappableIconLink - registryInfoMessageComposite = WrappableIconLink.createWithCustomizedImage(mcpRegistryGroup, - "/icons/information.png", message); + registryInfoMessageComposite = WrappableIconLink.create(mcpRegistryGroup, + CopilotImages.getImage(CopilotImages.IMG_INFORMATION), message); // Trigger layout update on the entire hierarchy to ensure proper sizing mcpRegistryGroup.requestLayout(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java index 591f06179..8466405d7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java @@ -5,7 +5,6 @@ import java.net.URL; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.SelectionAdapter; @@ -19,11 +18,10 @@ import org.eclipse.ui.PlatformUI; import com.microsoft.copilot.eclipse.core.CopilotCore; -import com.microsoft.copilot.eclipse.ui.utils.UiUtils; /** * A composite that displays an icon and a wrappable link. The component automatically resizes when the parent composite - * is resized and handles proper image disposal. + * is resized. The icon image is not owned by this widget and is therefore not disposed by it. */ public class WrappableIconLink extends Composite { @@ -35,61 +33,43 @@ public class WrappableIconLink extends Composite { // Icon private Label iconLabel; - private String iconPath; // only used for customized image case - private Image icon; // holds the created (non-shared) image if any - private boolean isSharedImage; // true when using shared workbench image + private Image icon; // Link private Link linkControl; private String linkText; /** - * Creates a new WrappableIconLink with default layout settings and no icon. + * Creates a new WrappableIconLink. * * @param parent the parent composite + * @param icon the icon image (not owned by this widget) * @param linkText the text for the link (may contain HTML link tags) + * @param widthMargin the horizontal margin to subtract when computing the link width */ - private WrappableIconLink(Composite parent, String iconPath, Image sharedImage, String linkText, int widthMargin) { + private WrappableIconLink(Composite parent, Image icon, String linkText, int widthMargin) { super(parent, SWT.NONE); this.widthMargin = widthMargin; this.parent = parent; this.linkText = linkText; - this.iconPath = iconPath; - this.isSharedImage = sharedImage != null; - this.icon = sharedImage; + this.icon = icon; createControls(); setupResizeListener(); } // ------------- Factory methods ------------- /** - * Creates a WrappableIconLink with a shared workbench image. + * Creates a WrappableIconLink with the given icon image. */ - public static WrappableIconLink createWithSharedImage(Composite parent, Image sharedImage, String linkText) { - return new WrappableIconLink(parent, null, sharedImage, linkText, DEFAULT_MARGIN); + public static WrappableIconLink create(Composite parent, Image icon, String linkText) { + return new WrappableIconLink(parent, icon, linkText, DEFAULT_MARGIN); } /** - * Creates a WrappableIconLink with a shared workbench image and custom width margin. + * Creates a WrappableIconLink with the given icon image and custom width margin. */ - public static WrappableIconLink createWithSharedImage(Composite parent, Image sharedImage, String linkText, - int widthMargin) { - return new WrappableIconLink(parent, null, sharedImage, linkText, widthMargin); - } - - /** - * Creates a WrappableIconLink with a customized image from the given path. - */ - public static WrappableIconLink createWithCustomizedImage(Composite parent, String iconPath, String linkText) { - return new WrappableIconLink(parent, iconPath, null, linkText, DEFAULT_MARGIN); - } - - /** - * Creates a WrappableIconLink with a customized image from the given path and custom width margin. - */ - public static WrappableIconLink createWithCustomizedImage(Composite parent, String iconPath, String linkText, - int widthMargin) { - return new WrappableIconLink(parent, iconPath, null, linkText, widthMargin); + public static WrappableIconLink create(Composite parent, Image icon, String linkText, int widthMargin) { + return new WrappableIconLink(parent, icon, linkText, widthMargin); } /** @@ -103,14 +83,8 @@ private void createControls() { // Create icon label iconLabel = new Label(this, SWT.NONE); iconLabel.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); - if (isSharedImage && icon != null) { - iconLabel.setImage(icon); // shared image - } else if (iconPath != null) { - ImageDescriptor imageDescriptor = UiUtils.buildImageDescriptorFromPngPath(iconPath); - if (imageDescriptor != null) { - icon = imageDescriptor.createImage(); - iconLabel.setImage(icon); - } + if (icon != null) { + iconLabel.setImage(icon); } // Create link control @@ -130,13 +104,6 @@ public void widgetSelected(SelectionEvent e) { } } }); - - // Dispose image when composite is disposed - addDisposeListener(e -> { - if (!isSharedImage && icon != null && !icon.isDisposed()) { - icon.dispose(); - } - }); } /** From 942725647cbe21ae2bfd5cf6e39fa90797b30294 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Tue, 21 Jul 2026 14:17:59 +0200 Subject: [PATCH 05/19] Remove unused buildImage(Descriptor)FromPngPath helpers (#364) All icon lookups now go through the image registries (CopilotImages / CopilotJobsImages), so the raw PNG-path helpers are no longer referenced. Drop them from the ui-bundle UiUtils and delete the jobs-bundle UiUtils, whose sole purpose was these two methods. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../eclipse/ui/jobs/utils/UiUtils.java | 38 ------------------- .../copilot/eclipse/ui/utils/UiUtils.java | 14 ------- 2 files changed, 52 deletions(-) delete mode 100644 com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/utils/UiUtils.java diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/utils/UiUtils.java deleted file mode 100644 index fd2220aba..000000000 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/utils/UiUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package com.microsoft.copilot.eclipse.ui.jobs.utils; - -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.swt.graphics.Image; - -/** - * Utilities for Eclipse UI in the jobs package. - */ -public class UiUtils { - - private UiUtils() { - // prevent instantiation - } - - /** - * Builds an image descriptor from a PNG file at the given path. - * - * @param path the path to the PNG file - * @return the image descriptor, or null if the resource is not found - */ - public static ImageDescriptor buildImageDescriptorFromPngPath(String path) { - return ImageDescriptor.createFromURL(UiUtils.class.getResource(path)); - } - - /** - * Builds an image from a PNG file at the given path. - * - * @param path the path to the PNG file - * @return the image, or null if the resource is not found - */ - public static Image buildImageFromPngPath(String path) { - ImageDescriptor descriptor = buildImageDescriptorFromPngPath(path); - return descriptor != null ? descriptor.createImage() : null; - } -} diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index 5d1ab2a96..a7a765fdd 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -406,20 +406,6 @@ public static int modelLine2WidgetLine(ITextViewer viewer, int modelLine) { return modelLine; } - /** - * Builds an image descriptor from a PNG file at the given path. - */ - public static ImageDescriptor buildImageDescriptorFromPngPath(String path) { - return ImageDescriptor.createFromURL(UiUtils.class.getResource(path)); - } - - /** - * Builds an image from a PNG file at the given path. - */ - public static Image buildImageFromPngPath(String path) { - return buildImageDescriptorFromPngPath(path).createImage(); - } - /** * Refreshes the elements of the command with the given ID. */ From d7d4e2b28087c699493cfe43542530c847e60786 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Tue, 21 Jul 2026 16:00:22 +0200 Subject: [PATCH 06/19] Document image registry usage and disposal rules in copilot instructions (#364) Add an Image Registries subsection under Resource Management. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 872bcc089..24d0c2d2c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -301,7 +301,14 @@ CompletableFuture.supplyAsync(() -> { #### Resource Management -**When** creating SWT resources (fonts, images) → dispose them when done; Colors do not need disposal in modern SWT +**When** accessing SWT resources (fonts, images) → prefer accessing them via a plug-in's registry; in that case never dispose them + +**When** accessing SWT resources via a registry is not possible → you must dispose the resources you create +yourself: `Image`, `GC`, `Cursor`, `Region`, `Path`, `Pattern`. `Color` and `Font` are auto-disposed in recent +SWT. Never dispose system colors/fonts, registry-owned images/fonts, or `Display`/`Shell` (framework-managed) + +**When** designing a registry → store `ImageDescriptor`s (not eagerly created `Image`s); the registry creates +and caches the `Image` lazily on first access and disposes it at display shutdown **When** using shared resources → do not dispose resources from other bundles @@ -309,6 +316,18 @@ CompletableFuture.supplyAsync(() -> { **When** using streams/files → use try-with-resources for Eclipse resources (IFile, IDocument) +##### Image Registries + +A plug-in's images can be offered using an `ImageRegistry`. We have `CopilotImages` in +`com.microsoft.copilot.eclipse.ui` and `CopilotJobsImages` in `com.microsoft.copilot.eclipse.ui.jobs`. +The registry loads each image once, caches it, and disposes it automatically when the workbench shuts down. + +**When** accessing *static* images, always prefer using image registries. Do NOT pass image path `"/icons/...png"` +string literals around the code base. + +**When** disposing → do NOT dispose images obtained from a registry or from shared images; the registry owns +them. Only dispose images you constructed yourself with `new Image(...)`. + #### Error Handling **When** reporting errors → use Eclipse `IStatus` and `Status` objects From 0d6ad79bb3d9a0b8ec2a2c26d7913aecaeecb987 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 15:20:14 +0200 Subject: [PATCH 07/19] Improve javadoc: clarify image disposal --- .../copilot/eclipse/ui/chat/services/AvatarService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java index 25edb66bc..31e89cd60 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/AvatarService.java @@ -65,7 +65,8 @@ public AvatarService(AuthStatusManager authStatusManager) { } /** - * Gets the avatar for the current user. + * Gets the avatar for the current user or a default user icon. + * The returned {@link Image} must never be disposed by the caller. * * @param display the display * @return the avatar @@ -83,7 +84,8 @@ public Image getAvatarForCopilot() { } /** - * Gets the avatar for a user. + * Gets the avatar icon for a user or a default user icon. + * The returned {@link Image} must never be disposed by the caller. * * @param display the display * @param user the user From 0a2880a3e13c72c60254966fe20dbc39827dad44 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 15:24:01 +0200 Subject: [PATCH 08/19] Fix mixed image ownership in McpUtils.loadServerIcon Rename loadServerIcon to loadDisposableServerIcon and return null instead of the shared default icon when an icon cannot be loaded. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ui/dialogs/mcp/McpServerDetailDialog.java | 14 +++++++++----- .../eclipse/ui/dialogs/mcp/McpServerItem.java | 14 +++++++++----- .../copilot/eclipse/ui/utils/McpUtils.java | 17 ++++++++++------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java index ffcd08f71..0797f115c 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerDetailDialog.java @@ -284,17 +284,21 @@ private void loadServerIconForHeader(Label iconLabel) { List icons = serverDetail != null ? serverDetail.icons() : null; String iconUrl = McpUtils.getPreferredIconUrl(icons); - McpUtils.loadServerIcon(iconUrl, iconSize, iconSize).thenAccept(image -> { - if (image == null) { - return; - } + McpUtils.loadDisposableServerIcon(iconUrl, iconSize, iconSize).thenAccept(image -> { if (iconLabel == null || iconLabel.isDisposed()) { - if (!image.isDisposed()) { + // label disposed -> also dispose freshly loaded image + if (image != null && !image.isDisposed()) { image.dispose(); } return; } + if (image == null) { + iconLabel.setImage(McpUtils.loadDefaultServerIcon()); + // never dispose image-registry-owned iamges; thus, return here + return; + } iconLabel.setImage(image); + // we need to dispose all images loaded via loadDisposableServerIcon(...) iconLabel.addDisposeListener(e -> { if (!image.isDisposed()) { image.dispose(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java index 923a09e9b..50d7e5d4b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/dialogs/mcp/McpServerItem.java @@ -476,17 +476,21 @@ private void loadServerIcon() { List icons = detail != null ? detail.icons() : null; String iconUrl = McpUtils.getPreferredIconUrl(icons); - McpUtils.loadServerIcon(iconUrl, iconSize, iconSize).thenAccept(image -> { - if (image == null) { - return; - } + McpUtils.loadDisposableServerIcon(iconUrl, iconSize, iconSize).thenAccept(image -> { if (iconLabel == null || iconLabel.isDisposed()) { - if (!image.isDisposed()) { + // label disposed -> also dispose freshly loaded image + if (image != null && !image.isDisposed()) { image.dispose(); } return; } + if (image == null) { + iconLabel.setImage(McpUtils.loadDefaultServerIcon()); + // never dispose image-registry-owned iamges; thus, return here + return; + } iconLabel.setImage(image); + // we need to dispose all images loaded via loadDisposableServerIcon(...) iconLabel.addDisposeListener(e -> { if (!image.isDisposed()) { image.dispose(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java index ee4ff4dfd..635727e36 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/McpUtils.java @@ -140,25 +140,28 @@ public static String getPreferredIconUrl(List icons) { /** * Loads the server icon from the given URL, scaled to the requested size. * - *

The caller is responsible for attaching the returned image to a widget and disposing it when the widget is - * disposed.

+ *

The returned image must be disposed by the caller, i.e. by attaching it to a widget and disposing the image + * using a widget dispose listener. When no icon can be loaded (blank URL or load failure) the future completes + * with {@code null}. In that case callers should fall back to {@link #loadDefaultServerIcon()}, + * whose image is registry-owned and must never be disposed.

* - * @param iconUrl the icon URL to load; if blank, the returned future completes with the default MCP icon image + * @param iconUrl the icon URL to load * @param width desired icon width in pixels * @param height desired icon height in pixels - * @return a future that completes with the loaded and scaled image, or {@code null} if the image cannot be loaded + * @return a future that completes with the loaded, caller-owned image, or {@code null} if the image cannot be loaded */ - public static CompletableFuture loadServerIcon(String iconUrl, int width, int height) { + public static CompletableFuture loadDisposableServerIcon(String iconUrl, int width, int height) { if (StringUtils.isBlank(iconUrl)) { - return CompletableFuture.completedFuture(loadDefaultServerIcon()); + return CompletableFuture.completedFuture(null); } return UiUtils.loadImageFromUrl(iconUrl, width, height) - .exceptionally(e -> loadDefaultServerIcon()); + .exceptionally(e -> null); } /** * Loads the shared default MCP server icon. + * The image must not be disposed by the caller. * * @return the default MCP icon image; callers must not dispose it */ From dc5f82281e87e46dacb8c52cdee90f1f4acf193f Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 15:51:04 +0200 Subject: [PATCH 09/19] Fix stale SpinnerAnimator Javadoc on frame ownership Update docs about image registry usage and disposal responsibility. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../eclipse/ui/swt/SpinnerAnimator.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java index 4fb7b9a21..a13e348b9 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java @@ -12,13 +12,12 @@ /** * Drives a rotating spinner animation on a target {@link Label}. * - *

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. + *

Frames are taken from the image registry ({@link CopilotImages}) and must never be disposed. + * {@link #stop()} only detaches the current frame from the label so the caller can swap + * in a final image (e.g. a "completed" icon) on the same label. * - *

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. + *

The animator hooks the target label's dispose listener so the animation is cancelled + * automatically when the label goes away. */ public final class SpinnerAnimator { /** Total number of frames. */ @@ -70,17 +69,16 @@ public void run() { } /** - * 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. + * Stop the animation and detach the current frame from the target label. + * Frames owned by the image registry are never disposed. 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. + // Detach the registry-owned frame from the label. 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); } From d580feade950ae09ec0a4861a2a79636cfe0dc92 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 16:02:30 +0200 Subject: [PATCH 10/19] fix: Remove unused dependencies --- .../META-INF/MANIFEST.MF | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF b/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF index 54f8cf3e6..2469b2894 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF +++ b/com.microsoft.copilot.eclipse.ui.jobs.test/META-INF/MANIFEST.MF @@ -7,9 +7,5 @@ Bundle-Vendor: GitHub Copilot Bundle-RequiredExecutionEnvironment: JavaSE-17 Fragment-Host: com.microsoft.copilot.eclipse.ui.jobs Automatic-Module-Name: com.microsoft.copilot.eclipse.ui.jobs.test -Import-Package: org.objenesis;version="[3.4.0,4.0.0)" Require-Bundle: junit-jupiter-api;bundle-version="5.10.1", - junit-jupiter-params;bundle-version="5.10.1", - org.mockito.mockito-core;bundle-version="5.14.2", - org.mockito.junit-jupiter;bundle-version="5.10.2", - com.microsoft.copilot.eclipse.ui.jobs;bundle-version="0.20.0" + junit-jupiter-params;bundle-version="5.10.1" From 980d83fd0f49b6ea4ec5c6a116905c5f103b30d0 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 16:19:14 +0200 Subject: [PATCH 11/19] Remove unused image registry methods returning ImageDescriptor --- .../eclipse/ui/jobs/CopilotJobsImages.java | 9 --------- .../copilot/eclipse/ui/CopilotImages.java | 20 ------------------- 2 files changed, 29 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java index 0f46a682d..c2dc0bc6c 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java @@ -87,13 +87,4 @@ public static Image getSharedImage(String imageId) { return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); } - /** - * Convenience access to Eclipse's workbench shared image descriptors. - * - * @param imageId a constant from {@link ISharedImages} - */ - public static ImageDescriptor getSharedImageDescriptor(String imageId) { - return PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(imageId); - } - } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java index 100f9e180..6fe779df6 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java @@ -299,17 +299,6 @@ public static Image getThemedImage(String lightKey, String darkKey) { return getImage(UiUtils.isDarkTheme() ? darkKey : lightKey); } - /** - * Returns the theme-correct descriptor for icons that come in light/dark variants. - * - * @param lightKey key to use in light theme - * @param darkKey key to use in dark theme - * @see #getThemedImage(String, String) - */ - public static ImageDescriptor getThemedImageDescriptor(String lightKey, String darkKey) { - return getImageDescriptor(UiUtils.isDarkTheme() ? darkKey : lightKey); - } - private static String getSpinnerFrameImageKey(int frame) { Assert.isLegal(frame >= 1 && frame <= SPINNER_FRAMES.length, "Spinner frame must be in [1, " + SPINNER_FRAMES.length + "], got: " + frame); @@ -346,13 +335,4 @@ public static Image getSharedImage(String imageId) { return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); } - /** - * Convenience access to Eclipse's workbench shared image descriptors. - * - * @param imageId a constant from {@link ISharedImages} - */ - public static ImageDescriptor getSharedImageDescriptor(String imageId) { - return PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(imageId); - } - } From e820a33cc50190d5b19cc76f316e89deea5c807f Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 16:31:45 +0200 Subject: [PATCH 12/19] Complete Javadoc for public image registry APIs Add missing @return and @param tags to newly introduced public methods. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../copilot/eclipse/ui/jobs/CopilotJobs.java | 6 +++++- .../copilot/eclipse/ui/jobs/CopilotJobsImages.java | 3 +++ .../microsoft/copilot/eclipse/ui/CopilotImages.java | 11 +++++++++++ .../eclipse/ui/preferences/WrappableIconLink.java | 11 +++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java index 899c455e9..20b0ee798 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobs.java @@ -21,7 +21,11 @@ public class CopilotJobs extends AbstractUIPlugin { private static volatile CopilotJobs COPILOT_JOBS_PLUGIN; - /** Returns the shared plugin instance. */ + /** + * Returns the shared plugin instance. + * + * @return the shared plugin instance + */ public static CopilotJobs getPlugin() { Assert.isNotNull(COPILOT_JOBS_PLUGIN); return COPILOT_JOBS_PLUGIN; diff --git a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java index c2dc0bc6c..f3e0ceb17 100644 --- a/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java +++ b/com.microsoft.copilot.eclipse.ui.jobs/src/com/microsoft/copilot/eclipse/ui/jobs/CopilotJobsImages.java @@ -63,6 +63,7 @@ private static void register(ImageRegistry registry, String path) { * callers must not dispose it. * * @param key one of the {@code IMG_*} constants defined in this class + * @return the registry-owned image for the given key */ public static Image getImage(String key) { return getImageRegistry().get(key); @@ -73,6 +74,7 @@ public static Image getImage(String key) { * e.g. for {@code Action.setImageDescriptor()}. * * @param key one of the {@code IMG_*} constants defined in this class + * @return the image descriptor for the given key */ public static ImageDescriptor getImageDescriptor(String key) { return getImageRegistry().getDescriptor(key); @@ -82,6 +84,7 @@ public static ImageDescriptor getImageDescriptor(String key) { * Convenience access to Eclipse's workbench shared images. * * @param imageId a constant from {@link ISharedImages}, e.g. {@link ISharedImages#IMG_OBJS_WARN_TSK} + * @return the shared workbench image for the given id */ public static Image getSharedImage(String imageId) { return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java index 6fe779df6..271e14027 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java @@ -271,6 +271,7 @@ private static void register(ImageRegistry registry, String path) { * callers must not dispose it. * * @param key one of the {@code IMG_*} constants defined in this class + * @return the registry-owned image for the given key */ public static Image getImage(String key) { return getImageRegistry().get(key); @@ -281,6 +282,7 @@ public static Image getImage(String key) { * e.g. for {@code Action.setImageDescriptor()}. * * @param key one of the {@code IMG_*} constants defined in this class + * @return the image descriptor for the given key */ public static ImageDescriptor getImageDescriptor(String key) { return getImageRegistry().getDescriptor(key); @@ -289,11 +291,14 @@ public static ImageDescriptor getImageDescriptor(String key) { /** * Returns the theme-correct image for icons that come in light/dark variants. * Uses {@link UiUtils#isDarkTheme()} to pick the right key. + * The returned image is owned by the plugin registry; + * callers must not dispose it. * *

Does not track live theme changes; callers re-render on theme-change events as needed. * * @param lightKey key to use in light theme * @param darkKey key to use in dark theme + * @return the registry-owned image for the current theme */ public static Image getThemedImage(String lightKey, String darkKey) { return getImage(UiUtils.isDarkTheme() ? darkKey : lightKey); @@ -307,8 +312,11 @@ private static String getSpinnerFrameImageKey(int frame) { /** * Returns the image for the given 1-based spinner frame (1–8). + * The returned image is owned by the plugin registry; + * callers must not dispose it. * * @param frame 1-based index of the spinner frame + * @return the registry-owned image for the requested spinner frame * @throws IllegalArgumentException if {@code frame} is outside [1, 8] */ public static Image getSpinnerFrame(int frame) { @@ -328,8 +336,11 @@ public static ImageDescriptor getSpinnerFrameDescriptor(int frame) { /** * Convenience access to Eclipse's workbench shared images. + * The returned image is owned by the owning plugin's registry; + * callers must not dispose it. * * @param imageId a constant from {@link ISharedImages}, e.g. {@link ISharedImages#IMG_OBJS_ERROR_TSK} + * @return the shared workbench image for the given id */ public static Image getSharedImage(String imageId) { return PlatformUI.getWorkbench().getSharedImages().getImage(imageId); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java index 8466405d7..fec79127e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/preferences/WrappableIconLink.java @@ -60,6 +60,11 @@ private WrappableIconLink(Composite parent, Image icon, String linkText, int wid // ------------- Factory methods ------------- /** * Creates a WrappableIconLink with the given icon image. + * + * @param parent the parent composite + * @param icon the icon image (not owned by this widget) + * @param linkText the text for the link (may contain HTML link tags) + * @return the created {@link WrappableIconLink} */ public static WrappableIconLink create(Composite parent, Image icon, String linkText) { return new WrappableIconLink(parent, icon, linkText, DEFAULT_MARGIN); @@ -67,6 +72,12 @@ public static WrappableIconLink create(Composite parent, Image icon, String link /** * Creates a WrappableIconLink with the given icon image and custom width margin. + * + * @param parent the parent composite + * @param icon the icon image (not owned by this widget) + * @param linkText the text for the link (may contain HTML link tags) + * @param widthMargin the horizontal margin to subtract when computing the link width + * @return the created {@link WrappableIconLink} */ public static WrappableIconLink create(Composite parent, Image icon, String linkText, int widthMargin) { return new WrappableIconLink(parent, icon, linkText, widthMargin); From 49bee87b9e978c7c9b68b10d02449e400e54e258 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 16:51:16 +0200 Subject: [PATCH 13/19] Hide spinner frame constants Make IMG_SPINNER_* keys and the SPINNER_FRAMES array private, since they are meant for internal use only. Expose only SPINNER_FRAME_COUNT and use it from SpinnerAnimator. Adapt test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../copilot/eclipse/ui/CopilotImagesTest.java | 5 ++-- .../copilot/eclipse/ui/CopilotImages.java | 29 ++++++++++--------- .../eclipse/ui/swt/SpinnerAnimator.java | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java index 5fda6d5c0..4f4aba8b0 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/CopilotImagesTest.java @@ -12,8 +12,8 @@ import org.junit.jupiter.params.provider.MethodSource; /** - * Verifies that every public constant in {@link CopilotImages} refers to a bundle resource that - * actually exists on the classpath. + * Verifies that every {@code IMG_*} constant in {@link CopilotImages} refers to a bundle resource + * that actually exists on the classpath. * *

This test is independent of a running Display or plugin registry — it only needs the bundle's * classpath. It guards against typos in icon paths and missing files caused by renames or deletions. @@ -26,6 +26,7 @@ static Stream iconPaths() { .filter(f -> f.getName().startsWith("IMG_")) .map(f -> { try { + f.setAccessible(true); return Arguments.of(f.getName(), f.get(null)); } catch (IllegalAccessException e) { throw new IllegalStateException("Cannot read constant: " + f.getName(), e); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java index 271e14027..b69a25d8e 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java @@ -134,20 +134,23 @@ public final class CopilotImages { public static final String IMG_QUICKSTART_CLOSE_HOVER_DARK = ICONS_QUICKSTART + "close_hover_dark.png"; // ---- Spinner frames (1-based index, see getSpinnerFrame) ----------------- - public static final String IMG_SPINNER_1 = ICONS_SPINNER + "1.png"; - public static final String IMG_SPINNER_2 = ICONS_SPINNER + "2.png"; - public static final String IMG_SPINNER_3 = ICONS_SPINNER + "3.png"; - public static final String IMG_SPINNER_4 = ICONS_SPINNER + "4.png"; - public static final String IMG_SPINNER_5 = ICONS_SPINNER + "5.png"; - public static final String IMG_SPINNER_6 = ICONS_SPINNER + "6.png"; - public static final String IMG_SPINNER_7 = ICONS_SPINNER + "7.png"; - public static final String IMG_SPINNER_8 = ICONS_SPINNER + "8.png"; + private static final String IMG_SPINNER_1 = ICONS_SPINNER + "1.png"; + private static final String IMG_SPINNER_2 = ICONS_SPINNER + "2.png"; + private static final String IMG_SPINNER_3 = ICONS_SPINNER + "3.png"; + private static final String IMG_SPINNER_4 = ICONS_SPINNER + "4.png"; + private static final String IMG_SPINNER_5 = ICONS_SPINNER + "5.png"; + private static final String IMG_SPINNER_6 = ICONS_SPINNER + "6.png"; + private static final String IMG_SPINNER_7 = ICONS_SPINNER + "7.png"; + private static final String IMG_SPINNER_8 = ICONS_SPINNER + "8.png"; - public static final String[] SPINNER_FRAMES = { + private static final String[] SPINNER_FRAMES = { IMG_SPINNER_1, IMG_SPINNER_2, IMG_SPINNER_3, IMG_SPINNER_4, IMG_SPINNER_5, IMG_SPINNER_6, IMG_SPINNER_7, IMG_SPINNER_8 }; + /** Number of spinner animation frames (1-based indices run from 1 to this value). */ + public static final int SPINNER_FRAME_COUNT = SPINNER_FRAMES.length; + private CopilotImages() { // prevent instantiation } @@ -311,24 +314,24 @@ private static String getSpinnerFrameImageKey(int frame) { } /** - * Returns the image for the given 1-based spinner frame (1–8). + * Returns the image for the given 1-based spinner frame (1–{@link SPINNER_FRAME_COUNT}). * The returned image is owned by the plugin registry; * callers must not dispose it. * * @param frame 1-based index of the spinner frame * @return the registry-owned image for the requested spinner frame - * @throws IllegalArgumentException if {@code frame} is outside [1, 8] + * @throws IllegalArgumentException if {@code frame} is outside [1, {@link SPINNER_FRAME_COUNT}] */ public static Image getSpinnerFrame(int frame) { return getImage(getSpinnerFrameImageKey(frame)); } /** - * Returns the image descriptor for the given 1-based spinner frame (1–8). + * Returns the image descriptor for the given 1-based spinner frame (1–{@link SPINNER_FRAME_COUNT}). * * @param frame 1-based index of the spinner frame * @return the image descriptor for the requested spinner frame - * @throws IllegalArgumentException if {@code frame} is outside [1, 8] + * @throws IllegalArgumentException if {@code frame} is outside [1, {@link SPINNER_FRAME_COUNT}] */ public static ImageDescriptor getSpinnerFrameDescriptor(int frame) { return getImageDescriptor(getSpinnerFrameImageKey(frame)); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java index a13e348b9..80b3b0cda 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/swt/SpinnerAnimator.java @@ -21,7 +21,7 @@ */ public final class SpinnerAnimator { /** Total number of frames. */ - private static final int TOTAL_FRAMES = CopilotImages.SPINNER_FRAMES.length; + private static final int TOTAL_FRAMES = CopilotImages.SPINNER_FRAME_COUNT; /** Per-frame interval in milliseconds. */ private static final int FRAME_INTERVAL_MS = 100; From 4cd7c6c7b84013a7130c56fd7cf51cc18b0155f9 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 17:32:36 +0200 Subject: [PATCH 14/19] Use shared registry image for NES ruler column icon --- .../copilot/eclipse/ui/nes/RulerColumn.java | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java index 00da9d29f..d0dd21d93 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/nes/RulerColumn.java @@ -7,7 +7,6 @@ import java.util.Collections; import java.util.List; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.IInformationControlCreator; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.source.AbstractRulerColumn; @@ -264,28 +263,22 @@ private Rectangle getSuggestionIconBounds(int expectedModelLine, int expectedWid private void ensureIcon() { if (icon == null || icon.isDisposed()) { - ImageDescriptor desc = CopilotImages.getImageDescriptor(CopilotImages.IMG_CHAT_GUTTER_ARROW); - if (desc != null) { - icon = desc.createImage(true); - - // Update column width based on actual icon size - if (icon != null && !icon.isDisposed()) { - Rectangle bounds = icon.getBounds(); - int iconSize = Math.max(bounds.width, bounds.height); - int requiredWidth = iconSize + COLUMN_PADDING; - if (getWidth() != requiredWidth) { - setWidth(requiredWidth); - } + icon = CopilotImages.getImage(CopilotImages.IMG_CHAT_GUTTER_ARROW); + + // Update column width based on actual icon size + if (icon != null && !icon.isDisposed()) { + Rectangle bounds = icon.getBounds(); + int iconSize = Math.max(bounds.width, bounds.height); + int requiredWidth = iconSize + COLUMN_PADDING; + if (getWidth() != requiredWidth) { + setWidth(requiredWidth); } } } } private void disposeResources() { - if (icon != null && !icon.isDisposed()) { - icon.dispose(); - } - icon = null; + icon = null; // the icon is disposed by the image-registry RenderManager manager = resolveNesRenderManager(); if (manager != null) { manager.detachColumn(this); From 4d0b6dc22863769f62369ba08e5353653f23c66d Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Wed, 29 Jul 2026 18:21:29 +0200 Subject: [PATCH 15/19] Add insert icon to copilot's image registry Register the insert icon (icons/full/elcl16/insert_template.png) from org.eclipse.ui.workbench.texteditor bundle in our own image registry, CopilotImages. Remove image disposal (now handled by the registry) and unused code. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../icons/chat/insert_template.png | Bin 0 -> 503 bytes .../copilot/eclipse/ui/CopilotImages.java | 2 ++ .../copilot/eclipse/ui/UiConstants.java | 3 --- .../eclipse/ui/chat/SourceViewerComposite.java | 15 ++------------- 4 files changed, 4 insertions(+), 16 deletions(-) create mode 100644 com.microsoft.copilot.eclipse.ui/icons/chat/insert_template.png diff --git a/com.microsoft.copilot.eclipse.ui/icons/chat/insert_template.png b/com.microsoft.copilot.eclipse.ui/icons/chat/insert_template.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f536d85507c8d11a4f215faec424a8ee1af50a GIT binary patch literal 503 zcmVt_vKSn^>%y^tH(mpo zOj0PUwi8f#a(=n@=!iOcpM0St6bszq(`XB0vhnDM>@hKEzsP%YBROprc|1x}mxo-w z#N5Qa{m7Jb9Yr$z76v@-2dHf|#J@Zxps#0jF-m4*>yW*@{exGn)t7x8r&g33H5_i#rW0t)y|h{D`(*#Nm8W4oI!F>CA!$Ha-ijeS3y-`y8?9 z5Uc58MZaU99B?5i{QQvuHmi+1}n^DzboWaDwGj0sDXh z;TLSSKD-_0(ZbzGH_s3a2U$uM4zct*#eqKSV_etjaO>*$ITOVC`~#hq*XZ;Ti`r&| t>Cuol(ED(h`NZa5?d=Hzddmp^Y8MZ+Bf0$moAUqw002ovPDHLkV1g-l@NfVC literal 0 HcmV?d00001 diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java index b69a25d8e..3cc024f46 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/CopilotImages.java @@ -72,6 +72,7 @@ public final class CopilotImages { public static final String IMG_CHAT_EYE_CLOSED = ICONS_CHAT + "eye_closed.png"; public static final String IMG_CHAT_EYE_CLOSED_DARK = ICONS_CHAT + "eye_closed_dark.png"; public static final String IMG_CHAT_GUTTER_ARROW = ICONS_CHAT + "gutter-arrow.png"; + public static final String IMG_CHAT_INSERT_TEMPLATE = ICONS_CHAT + "insert_template.png"; public static final String IMG_CHAT_KEEP = ICONS_CHAT + "keep.png"; public static final String IMG_CHAT_KEYBOARD_TAB = ICONS_CHAT + "keyboard-tab.png"; public static final String IMG_CHAT_RED_NOTICE = ICONS_CHAT + "red_notice.png"; @@ -204,6 +205,7 @@ static void initialize(ImageRegistry registry) { register(registry, IMG_CHAT_EYE_CLOSED); register(registry, IMG_CHAT_EYE_CLOSED_DARK); register(registry, IMG_CHAT_GUTTER_ARROW); + register(registry, IMG_CHAT_INSERT_TEMPLATE); register(registry, IMG_CHAT_KEEP); register(registry, IMG_CHAT_KEYBOARD_TAB); register(registry, IMG_CHAT_RED_NOTICE); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java index f1fb54d52..6cfbb8371 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/UiConstants.java @@ -8,9 +8,6 @@ */ public class UiConstants { - public static final String WORKBENCH_TEXTEDITOR = "org.eclipse.ui.workbench.texteditor"; - public static final String INSERT_ICON = "icons/full/elcl16/insert_template.png"; - private UiConstants() { // prevent instantiation } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java index 73f475125..4e15e8381 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/SourceViewerComposite.java @@ -7,7 +7,6 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; @@ -29,14 +28,12 @@ import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConversationCodeCopyParams; import com.microsoft.copilot.eclipse.ui.CopilotImages; -import com.microsoft.copilot.eclipse.ui.UiConstants; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; import com.microsoft.copilot.eclipse.ui.utils.AccessibilityUtils; import com.microsoft.copilot.eclipse.ui.utils.SwtUtils; @@ -57,7 +54,6 @@ public class SourceViewerComposite extends Composite { private Composite actionsComposite; private Image copyIcon; - private Image insertIcon; private Runnable fontChangeCallback; /** @@ -184,12 +180,8 @@ private Composite createActionsComposite() { } }); - ImageDescriptor insertDesc = AbstractUIPlugin.imageDescriptorFromPlugin(UiConstants.WORKBENCH_TEXTEDITOR, - UiConstants.INSERT_ICON); - this.insertIcon = Optional.ofNullable(insertDesc).map(desc -> desc.createImage()).orElse(null); - Image pasteIcon = CopilotImages.getSharedImage(ISharedImages.IMG_TOOL_PASTE); - Image insertButtonIcon = this.insertIcon != null ? this.insertIcon : pasteIcon; - Button insertButton = createActionButton(result, SWT.PUSH | SWT.FLAT, insertButtonIcon, "Insert", + Image insertIcon = CopilotImages.getImage(CopilotImages.IMG_CHAT_INSERT_TEMPLATE); + Button insertButton = createActionButton(result, SWT.PUSH | SWT.FLAT, insertIcon, "Insert", "Insert into editor", Messages.sourceViewerComposite_insertButtonAccessibilityName); insertButton.addListener(SWT.Selection, this::insert); @@ -331,9 +323,6 @@ public void dispose() { if (fontChangeCallback != null) { serviceManager.getChatFontService().unregisterCallback(fontChangeCallback); } - if (insertIcon != null) { - insertIcon.dispose(); - } } } From c333bd7a41e9b244a34b8d3d6ec3c96c0b46e8f0 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Thu, 30 Jul 2026 09:31:33 +0200 Subject: [PATCH 16/19] Remove unused method See review comment https://github.com/microsoft/copilot-for-eclipse/pull/365#pullrequestreview-4814515310 --- .../copilot/eclipse/ui/utils/UiUtils.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java index a7a765fdd..6f8594ab7 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/utils/UiUtils.java @@ -41,7 +41,6 @@ import org.eclipse.e4.ui.workbench.modeling.EPartService; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.resource.FontRegistry; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.jface.viewers.ISelectionProvider; @@ -329,21 +328,6 @@ public static boolean openE4Part(String partId) { return false; } - /** - * Resizes the icon at the given path to the given width and height. Icon size is 16x16 by default, which is the - * recommended size for toolbar icons. For more details: https://eclipse-platform.github.io/ui-best-practices/#toolbar - */ - public static ImageDescriptor resizeIcon(String path, int width, int height) { - ImageLoader loader = new ImageLoader(); - ImageData[] imageDataArray = loader.load(UiUtils.class.getResourceAsStream(path)); - if (imageDataArray.length > 0) { - ImageData imageData = imageDataArray[0].scaledTo(width, height); - Image image = new Image(Display.getDefault(), imageData); - return ImageDescriptor.createFromImage(image); - } - return null; - } - /** * Resizes the given image to the given width and height. */ From 31dfca0db22faf27126619643f4af7facdb42c48 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Thu, 30 Jul 2026 09:47:47 +0200 Subject: [PATCH 17/19] Use the image registry in McpRuntimeLogger --- .../ui/chat/services/McpRuntimeLogger.java | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/McpRuntimeLogger.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/McpRuntimeLogger.java index 0023907ab..f97c531f8 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/McpRuntimeLogger.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/services/McpRuntimeLogger.java @@ -9,9 +9,6 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.Path; -import org.eclipse.core.runtime.Platform; import org.eclipse.e4.core.services.events.IEventBroker; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.ui.PlatformUI; @@ -20,12 +17,12 @@ import org.eclipse.ui.console.IConsoleManager; import org.eclipse.ui.console.MessageConsole; import org.eclipse.ui.console.MessageConsoleStream; -import org.osgi.framework.Bundle; import org.osgi.service.event.EventHandler; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.events.CopilotEventConstants; import com.microsoft.copilot.eclipse.core.lsp.mcp.McpRuntimeLog; +import com.microsoft.copilot.eclipse.ui.CopilotImages; /** * Console handler for MCP Runtime logs that writes to a dedicated Eclipse console. @@ -34,8 +31,6 @@ public class McpRuntimeLogger { private final String consoleName = "Copilot (MCP)"; private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - private final String uiPluginId = "com.microsoft.copilot.eclipse.ui"; - private final String iconPath = "icons/github_copilot.png"; private MessageConsole mcpConsole; private MessageConsoleStream mcpConsoleStream; @@ -95,16 +90,7 @@ private MessageConsole getConsole() { * @return ImageDescriptor for the console icon */ private ImageDescriptor getIconImageDescriptor() { - try { - Bundle bundle = Platform.getBundle(uiPluginId); - if (bundle != null) { - return ImageDescriptor.createFromURL( - FileLocator.find(bundle, new Path(iconPath), null)); - } - } catch (Exception e) { - // Fall back to null icon if there's any problem loading - } - return null; + return CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT); } /** From 0f0f128a7c95937cea398480bb04fe0faeec4389 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Thu, 30 Jul 2026 10:26:59 +0200 Subject: [PATCH 18/19] Use the image registry for accessing the terminal icon Adapt the terminal SPI's API and both terminal implementations to retrieve the icon from an image registry instead of creating and disposing own copies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../terminal/api/IRunInTerminalTool.java | 14 ++++++++-- .../ui/terminal/tm/RunInTerminalTool.java | 27 +++++++++--------- .../ui/terminal/RunInTerminalTool.java | 28 +++++++++---------- .../chat/tools/RunInTerminalToolAdapter.java | 5 +++- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/com.microsoft.copilot.eclipse.terminal.api/src/com/microsoft/copilot/eclipse/terminal/api/IRunInTerminalTool.java b/com.microsoft.copilot.eclipse.terminal.api/src/com/microsoft/copilot/eclipse/terminal/api/IRunInTerminalTool.java index b91429e18..e43422e31 100644 --- a/com.microsoft.copilot.eclipse.terminal.api/src/com/microsoft/copilot/eclipse/terminal/api/IRunInTerminalTool.java +++ b/com.microsoft.copilot.eclipse.terminal.api/src/com/microsoft/copilot/eclipse/terminal/api/IRunInTerminalTool.java @@ -5,8 +5,9 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; -import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; /** @@ -50,7 +51,14 @@ public Map prepareTerminalProperties(boolean runInBackground, St public void cancelCurrentCommand(); /** - * Sets the terminal icon descriptor for the tool. + * Sets the supplier that provides the terminal icon for the tool. + * + *

The supplier must return a shared {@link Image} owned by a plug-in's image registry. + * The image must never be disposed (this is done by the registry). + * + *

The run-in-terminal implementation needs to invoke the supplier on the UI thread. + * + * @param terminalIconSupplier supplier of a shared, non-disposable terminal icon */ - public void setTerminalIconDescriptor(ImageDescriptor terminalIconDescriptor); + public void setTerminalIconSupplier(Supplier terminalIconSupplier); } diff --git a/com.microsoft.copilot.eclipse.ui.terminal.tm/src/com/microsoft/copilot/eclipse/ui/terminal/tm/RunInTerminalTool.java b/com.microsoft.copilot.eclipse.ui.terminal.tm/src/com/microsoft/copilot/eclipse/ui/terminal/tm/RunInTerminalTool.java index 28849b6e1..4e27a0d30 100644 --- a/com.microsoft.copilot.eclipse.ui.terminal.tm/src/com/microsoft/copilot/eclipse/ui/terminal/tm/RunInTerminalTool.java +++ b/com.microsoft.copilot.eclipse.ui.terminal.tm/src/com/microsoft/copilot/eclipse/ui/terminal/tm/RunInTerminalTool.java @@ -9,10 +9,10 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; import org.eclipse.core.runtime.Platform; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.events.DisposeListener; @@ -61,8 +61,7 @@ public class RunInTerminalTool implements IRunInTerminalTool { private ITerminalControl terminalControl; private CTabFolder tabFolder; private CTabItem copilotTabItem; - private Image terminalIcon; - private ImageDescriptor terminalIconDescriptor; + private Supplier terminalIconSupplier; // Output and command state private StringBuilder sb; @@ -298,10 +297,8 @@ private ITerminalControl getTerminalControl(String terminalTitle, boolean isBack if (tabFolder != null) { for (CTabItem item : tabFolder.getItems()) { if (terminalTitle.equals(item.getText())) { - if (terminalIconDescriptor != null) { - if (terminalIcon == null || terminalIcon.isDisposed()) { - terminalIcon = terminalIconDescriptor.createImage(); - } + Image terminalIcon = resolveTerminalIcon(); + if (terminalIcon != null && !terminalIcon.isDisposed()) { item.setImage(terminalIcon); } item.addDisposeListener( @@ -381,10 +378,6 @@ private DisposeListener buildDisposeListener(String executionId, boolean isBackg if (backgroundCommandOutputs.isEmpty() && persistentTerminalViewControl == null) { terminalControl = null; - if (terminalIcon != null && !terminalIcon.isDisposed()) { - terminalIcon.dispose(); - terminalIcon = null; - } } }; } @@ -430,7 +423,15 @@ private IWorkbenchPage getActivePage() { } @Override - public void setTerminalIconDescriptor(ImageDescriptor iconDescriptor) { - this.terminalIconDescriptor = iconDescriptor; + public void setTerminalIconSupplier(Supplier terminalIconSupplier) { + this.terminalIconSupplier = terminalIconSupplier; + } + + /** Call this method from UI thread only. */ + private Image resolveTerminalIcon() { + if (terminalIconSupplier == null) { + return null; + } + return terminalIconSupplier.get(); } } diff --git a/com.microsoft.copilot.eclipse.ui.terminal/src/com/microsoft/copilot/eclipse/ui/terminal/RunInTerminalTool.java b/com.microsoft.copilot.eclipse.ui.terminal/src/com/microsoft/copilot/eclipse/ui/terminal/RunInTerminalTool.java index dfe44701b..0655e5c8f 100644 --- a/com.microsoft.copilot.eclipse.ui.terminal/src/com/microsoft/copilot/eclipse/ui/terminal/RunInTerminalTool.java +++ b/com.microsoft.copilot.eclipse.ui.terminal/src/com/microsoft/copilot/eclipse/ui/terminal/RunInTerminalTool.java @@ -9,10 +9,10 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; import org.eclipse.core.runtime.Platform; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.events.DisposeListener; @@ -61,8 +61,7 @@ public class RunInTerminalTool implements IRunInTerminalTool { private ITerminalControl terminalControl; private CTabFolder tabFolder; private CTabItem copilotTabItem; - private Image terminalIcon; - private ImageDescriptor terminalIconDescriptor; + private Supplier terminalIconSupplier; // Output and command state private StringBuilder sb; @@ -299,10 +298,8 @@ private ITerminalControl getTerminalControl(String terminalTitle, boolean isBack if (tabFolder != null) { for (CTabItem item : tabFolder.getItems()) { if (terminalTitle.equals(item.getText())) { - if (terminalIconDescriptor != null) { - if (terminalIcon == null || terminalIcon.isDisposed()) { - terminalIcon = terminalIconDescriptor.createImage(); - } + Image terminalIcon = resolveTerminalIcon(); + if (terminalIcon != null && !terminalIcon.isDisposed()) { item.setImage(terminalIcon); } item.addDisposeListener( @@ -380,10 +377,6 @@ private DisposeListener buildDisposeListener(String executionId, boolean isBackg if (backgroundCommandOutputs.isEmpty() && persistentTerminalViewControl == null) { terminalControl = null; - if (terminalIcon != null && !terminalIcon.isDisposed()) { - terminalIcon.dispose(); - terminalIcon = null; - } } }; } @@ -428,9 +421,16 @@ private IWorkbenchPage getActivePage() { return null; } - @Override - public void setTerminalIconDescriptor(ImageDescriptor iconDescriptor) { - this.terminalIconDescriptor = iconDescriptor; + public void setTerminalIconSupplier(Supplier terminalIconSupplier) { + this.terminalIconSupplier = terminalIconSupplier; + } + + /** Call this method from UI thread only. */ + private Image resolveTerminalIcon() { + if (terminalIconSupplier == null) { + return null; + } + return terminalIconSupplier.get(); } } diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java index 67e1b1ca9..86409d08c 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/tools/RunInTerminalToolAdapter.java @@ -10,10 +10,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import org.apache.commons.lang3.StringUtils; import org.eclipse.core.resources.IResource; import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.swt.graphics.Image; import com.microsoft.copilot.eclipse.core.lsp.protocol.ConfirmationMessages; import com.microsoft.copilot.eclipse.core.lsp.protocol.InputSchema; @@ -199,7 +201,8 @@ public CompletableFuture invoke(Map i isBackground = Boolean.parseBoolean((String) isBackgroundObj); } - impl.setTerminalIconDescriptor(CopilotImages.getImageDescriptor(CopilotImages.IMG_GITHUB_COPILOT)); + Supplier terminalIconSupplier = () -> CopilotImages.getImage(CopilotImages.IMG_GITHUB_COPILOT); + impl.setTerminalIconSupplier(terminalIconSupplier); String workingDirectory = resolveWorkingDirectory(); return impl.executeCommand(command, isBackground, workingDirectory) From 9766881f0eef2279137807b207adc47d42fe3982 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Thu, 30 Jul 2026 11:15:30 +0200 Subject: [PATCH 19/19] Provide a populated image registry to turn widget tests The image-registry refactor routes SourceViewerComposite's insert icon through CopilotUi.getPlugin().getImageRegistry(). The shared turn-widget test fixture mocked getPlugin() but not getImageRegistry(), so rendering a code block returned a null registry and NPE'd. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../eclipse/ui/TestImageRegistrySupport.java | 33 +++++++++++++++++++ .../ui/chat/BaseTurnWidgetTestSupport.java | 9 +++++ 2 files changed, 42 insertions(+) create mode 100644 com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/TestImageRegistrySupport.java diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/TestImageRegistrySupport.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/TestImageRegistrySupport.java new file mode 100644 index 000000000..7cc6ba5d5 --- /dev/null +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/TestImageRegistrySupport.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package com.microsoft.copilot.eclipse.ui; + +import org.eclipse.jface.resource.ImageRegistry; +import org.eclipse.swt.widgets.Display; + +/** + * Test helper that builds a real {@link ImageRegistry} populated with the Copilot UI bundle icons. + * + *

Lives in the {@code com.microsoft.copilot.eclipse.ui} package so it can call the package-private + * {@link CopilotImages#initialize(ImageRegistry)}. The test bundle is a fragment of the UI bundle, so + * this class shares the host package at runtime. + */ +public final class TestImageRegistrySupport { + + private TestImageRegistrySupport() { + // prevent instantiation + } + + /** + * Creates an {@link ImageRegistry} pre-populated with all Copilot UI icon descriptors. Must be + * called on the UI thread. + * + * @return a populated image registry; callers own it and must {@link ImageRegistry#dispose()} it + */ + public static ImageRegistry createCopilotImageRegistry() { + ImageRegistry registry = new ImageRegistry(Display.getDefault()); + CopilotImages.initialize(registry); + return registry; + } +} diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidgetTestSupport.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidgetTestSupport.java index 26fe91d0a..6f59e1ec7 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidgetTestSupport.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidgetTestSupport.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import org.eclipse.jface.resource.ImageRegistry; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.junit.jupiter.api.AfterEach; @@ -15,6 +16,7 @@ import org.mockito.MockedStatic; import com.microsoft.copilot.eclipse.ui.CopilotUi; +import com.microsoft.copilot.eclipse.ui.TestImageRegistrySupport; import com.microsoft.copilot.eclipse.ui.chat.services.AvatarService; import com.microsoft.copilot.eclipse.ui.chat.services.ChatFontService; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; @@ -37,6 +39,7 @@ abstract class BaseTurnWidgetTestSupport { private ChatFontService mockChatFontService; private MockedStatic copilotUiMock; + private ImageRegistry imageRegistry; @BeforeEach void setUpBaseTurnWidget() { @@ -50,6 +53,8 @@ void setUpBaseTurnWidget() { CopilotUi mockPlugin = mock(CopilotUi.class); copilotUiMock.when(CopilotUi::getPlugin).thenReturn(mockPlugin); lenient().when(mockPlugin.getChatServiceManager()).thenReturn(mockChatServiceManager); + imageRegistry = TestImageRegistrySupport.createCopilotImageRegistry(); + lenient().when(mockPlugin.getImageRegistry()).thenReturn(imageRegistry); }); } @@ -63,6 +68,10 @@ void tearDownBaseTurnWidget() { if (shell != null && !shell.isDisposed()) { shell.dispose(); } + if (imageRegistry != null) { + imageRegistry.dispose(); + imageRegistry = null; + } }); } }