diff --git a/ISSUE_UI_SCREENSHOT_IMPROVEMENT.md b/ISSUE_UI_SCREENSHOT_IMPROVEMENT.md new file mode 100644 index 00000000000..251d9a8818d --- /dev/null +++ b/ISSUE_UI_SCREENSHOT_IMPROVEMENT.md @@ -0,0 +1,49 @@ +# Improve UI screenshot generation for PR documentation + +## Problem + +The current approach to generating screenshots for UI components using Python/PIL produces white/blank images that don't accurately represent the Eclipse UI. + +## Proposed Solutions + +### 1. SWT Snippet Approach +Create small SWT snippets that can render the UI components. However, this requires complex setup for many components that depend on Eclipse platform infrastructure. + +### 2. MCP Server with Eclipse Install (Recommended) +Create a Model Context Protocol (MCP) server that includes a full Eclipse installation. This would allow: +- Programmatic access to Eclipse UI components +- Automated screenshot capture of actual rendered UI +- Better representation of Eclipse styling and themes +- Reusable infrastructure for future UI documentation + +## Context + +This issue was identified while implementing the Source Lookups preference page (see related PR for #2073). The generated screenshot using Python/PIL doesn't accurately show how the preference page would look in a real Eclipse environment. + +## Benefits of MCP Server Approach + +- **Accuracy**: Screenshots show actual Eclipse UI rendering with proper fonts, colors, and styling +- **Reusability**: Can be used for all future UI-related PRs and documentation +- **Automation**: Can be integrated into CI/CD pipeline for automatic screenshot generation +- **Quality**: Better documentation quality improves contributor and user experience + +## Implementation Ideas + +1. Create a headless Eclipse installation in a container +2. Build an MCP server that can: + - Launch Eclipse workbench programmatically + - Navigate to specific UI components (dialogs, preference pages, views, etc.) + - Capture screenshots using SWT/platform APIs + - Return screenshots as base64 or file paths +3. Integrate with Copilot workflow for automatic screenshot generation + +## Labels + +- `enhancement` +- `documentation` +- `tooling` + +## Related + +- Source Lookups preference page PR +- Issue #2073 diff --git a/source_lookups_preference_page.png b/source_lookups_preference_page.png new file mode 100644 index 00000000000..9abf876f961 Binary files /dev/null and b/source_lookups_preference_page.png differ diff --git a/ui/org.eclipse.pde.core/plugin.xml b/ui/org.eclipse.pde.core/plugin.xml index 9d0eb5530f7..494c61532cf 100644 --- a/ui/org.eclipse.pde.core/plugin.xml +++ b/ui/org.eclipse.pde.core/plugin.xml @@ -430,6 +430,9 @@ + diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ExternalPluginSourcePathLocator.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ExternalPluginSourcePathLocator.java new file mode 100644 index 00000000000..8e09bca0a0f --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/ExternalPluginSourcePathLocator.java @@ -0,0 +1,446 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.core; + +import java.io.File; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Platform; +import org.eclipse.equinox.p2.metadata.IArtifactKey; +import org.eclipse.equinox.p2.metadata.IInstallableUnit; +import org.eclipse.equinox.p2.metadata.MetadataFactory; +import org.eclipse.equinox.p2.metadata.MetadataFactory.InstallableUnitDescription; +import org.eclipse.equinox.p2.metadata.Version; +import org.eclipse.equinox.p2.query.IQueryResult; +import org.eclipse.equinox.p2.query.QueryUtil; +import org.eclipse.equinox.p2.repository.IRepository; +import org.eclipse.equinox.p2.repository.IRepositoryManager; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRequest; +import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; +import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager; +import org.eclipse.equinox.spi.p2.publisher.PublisherHelper; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.pde.core.IPluginSourcePathLocator; +import org.eclipse.pde.core.plugin.IPluginBase; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetHandle; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.ITargetPlatformService; +import org.eclipse.pde.internal.core.copyfrom.oomph.P2Index; +import org.eclipse.pde.internal.core.copyfrom.oomph.P2Index.Repository; +import org.eclipse.pde.internal.core.target.IUBundleContainer; +import org.eclipse.pde.internal.core.target.P2TargetUtils; +import org.eclipse.pde.internal.core.target.TargetPlatformService; +import org.eclipse.pde.internal.ui.IPreferenceConstants; +import org.eclipse.pde.internal.ui.PDEPlugin; + +/** + * A plugin source path locator that queries external repositories and the + * Eclipse index for source bundles based on the configured preferences. + *

+ * This locator checks the Source Lookups preference page settings and performs + * lookups according to the configured order. + *

+ * + * @since 3.17 + */ +public class ExternalPluginSourcePathLocator implements IPluginSourcePathLocator { + + // Source lookup strategy identifiers + private static final String STRATEGY_REPOSITORIES = "REPOSITORIES"; //$NON-NLS-1$ + private static final String STRATEGY_TARGETS = "TARGETS"; //$NON-NLS-1$ + private static final String STRATEGY_INDEX = "INDEX"; //$NON-NLS-1$ + private static final String STRATEGY_SITES = "SITES"; //$NON-NLS-1$ + + private P2Index p2Index; + + @Override + public IPath locateSource(IPluginBase plugin) { + IPreferenceStore store = PDEPlugin.getDefault().getPreferenceStore(); + + // Check if source lookup is enabled + if (!store.getBoolean(IPreferenceConstants.SOURCE_LOOKUP_ENABLED)) { + return null; + } + + // Get the lookup order + List lookupOrder = getLookupOrder(store); + + // Execute lookups in the configured order + for (String strategy : lookupOrder) { + IPath result = null; + switch (strategy) { + case STRATEGY_REPOSITORIES: + result = searchInRepositories(plugin, getConfiguredRepositories(store)); + break; + case STRATEGY_TARGETS: + result = searchInTargets(plugin, getSelectedTargets(store)); + break; + case STRATEGY_INDEX: + result = queryEclipseIndex(plugin); + break; + case STRATEGY_SITES: + result = queryAvailableSoftwareSites(plugin); + break; + default: + // Unknown strategy, skip + break; + } + + if (result != null) { + return result; + } + } + + return null; + } + + /** + * Gets the configured lookup order from preferences. + * + * @param store the preference store + * @return the list of strategy identifiers in order + */ + private List getLookupOrder(IPreferenceStore store) { + String order = store.getString(IPreferenceConstants.SOURCE_LOOKUP_ORDER); + if (order == null || order.trim().isEmpty()) { + // Use default order + return Arrays.asList(STRATEGY_REPOSITORIES, STRATEGY_TARGETS, STRATEGY_INDEX, STRATEGY_SITES); + } + return Arrays.asList(order.split(",")); //$NON-NLS-1$ + } + + /** + * Searches for the source bundle in the configured repositories. + * + * @param plugin the plugin to locate sources for + * @param repositories the list of repository URLs + * @return the path to the source bundle or null if not found + */ + private IPath searchInRepositories(IPluginBase plugin, List repositories) { + if (repositories == null || repositories.isEmpty()) { + return null; + } + + String pluginId = plugin.getId(); + String sourcePluginId = pluginId + ".source"; //$NON-NLS-1$ + String pluginVersion = plugin.getVersion(); + + // Loop through all configured repository URLs + for (String repositoryURL : repositories) { + try { + // Try to download the source bundle from this repository + URI repoURI = URI.create(repositoryURL.trim()); + IPath result = downloadArtifact(repoURI, sourcePluginId, pluginVersion); + if (result != null) { + // Found and downloaded successfully + return result; + } + } catch (Exception e) { + // Log and continue to next repository + PDECore.log("Failed to search for source in repository " + repositoryURL + ": " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + return null; + } + + /** + * Searches for the source bundle in the specified target platforms. + * + * @param plugin the plugin to locate sources for + * @param selectedTargets the list of selected target platform names + * @return the path to the source bundle or null if not found + */ + private IPath searchInTargets(IPluginBase plugin, List selectedTargets) { + if (selectedTargets == null || selectedTargets.isEmpty()) { + return null; + } + + String pluginId = plugin.getId(); + String sourcePluginId = pluginId + ".source"; //$NON-NLS-1$ + String pluginVersion = plugin.getVersion(); + + try { + // Get the target platform service + ITargetPlatformService service = TargetPlatformService.getDefault(); + if (service == null) { + return null; + } + + // Get all available target definitions + ITargetHandle[] targetHandles = service.getTargets(null); + if (targetHandles == null) { + return null; + } + + // Loop through all target handles + for (ITargetHandle targetHandle : targetHandles) { + try { + ITargetDefinition targetDef = targetHandle.getTargetDefinition(); + if (targetDef == null || targetDef.getName() == null) { + continue; + } + + // Check if this target is in the selected list + if (!selectedTargets.contains(targetDef.getName())) { + continue; + } + + // Get all target locations from this target + ITargetLocation[] locations = targetDef.getTargetLocations(); + if (locations == null) { + continue; + } + + // Loop through locations and look for IUBundleContainer (IU locations) + for (ITargetLocation location : locations) { + if (location instanceof IUBundleContainer) { + IUBundleContainer iuLocation = (IUBundleContainer) location; + + // Get the repository URIs from this IU location + List repositories = iuLocation.getRepositories(); + if (repositories == null || repositories.isEmpty()) { + continue; + } + + // Search each repository for the source bundle + for (URI repoURI : repositories) { + IPath result = downloadArtifact(repoURI, sourcePluginId, pluginVersion); + if (result != null) { + // Found and downloaded successfully + return result; + } + } + } + } + } catch (CoreException e) { + // Log and continue to next target + PDECore.log("Failed to process target definition: " + e.getMessage()); //$NON-NLS-1$ + } + } + } catch (Exception e) { + // Log but don't fail - this is just one lookup strategy + PDECore.log(e); + } + + return null; + } + + /** + * Queries the Eclipse index for the source bundle. + * + * @param plugin the plugin to locate sources for + * @return the path to the source bundle or null if not found + */ + private IPath queryEclipseIndex(IPluginBase plugin) { + try { + // Lazy initialization of P2Index + if (p2Index == null) { + File indexCacheDir = new File(Platform.getStateLocation(PDECore.getDefault().getBundle()).toFile(), + "index"); //$NON-NLS-1$ + p2Index = new P2Index(indexCacheDir); + } + + String pluginId = plugin.getId(); + String sourcePluginId = pluginId + ".source"; //$NON-NLS-1$ + + // Query the index for the source bundle using OSGi bundle capability + Map> repositories = p2Index + .lookupCapabilities(PublisherHelper.CAPABILITY_NS_OSGI_BUNDLE, sourcePluginId); + + if (repositories != null && !repositories.isEmpty()) { + // Find the repository with the matching version + String pluginVersion = plugin.getVersion(); + for (Entry> entry : repositories.entrySet()) { + for (Version version : entry.getValue()) { + if (version.toString().equals(pluginVersion)) { + // Found matching source bundle in the index + URI repositoryURI = URI.create(entry.getKey().getLocation().toString()); + return downloadArtifact(repositoryURI, sourcePluginId, pluginVersion); + } + } + } + } + } catch (Exception e) { + // Log but don't fail - this is just one lookup strategy + PDECore.log(e); + } + return null; + } + + /** + * Downloads an artifact from a P2 repository to the bundle pool. + * + * @param repositoryURL the repository URL + * @param artifactId the artifact ID (bundle symbolic name) + * @param version the artifact version + * @return the file location where it was downloaded or null if download failed + */ + private IPath downloadArtifact(URI repositoryURL, String artifactId, String version) { + try { + IProgressMonitor monitor = new NullProgressMonitor(); + + // Get P2 managers + IMetadataRepositoryManager metadataManager = P2TargetUtils.getMetadataRepositoryManager(); + IArtifactRepositoryManager artifactManager = P2TargetUtils.getArtifactRepositoryManager(); + + // Load the repository + IMetadataRepository metadataRepo = metadataManager.loadRepository(repositoryURL, + IRepository.REPOSITORY_HINT_MODIFIABLE, monitor); + + // Find the installable unit for the source bundle + IQueryResult queryResult = metadataRepo.query( + QueryUtil.createIUQuery(artifactId, Version.create(version)), monitor); + + if (queryResult.isEmpty()) { + PDECore.log("Source bundle " + artifactId + " version " + version + " not found in repository " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + repositoryURL); + return null; + } + + IInstallableUnit iu = queryResult.iterator().next(); + + // Get the artifact key + IArtifactKey artifactKey = iu.getArtifacts().iterator().next(); + + // Get the bundle pool (destination) + IArtifactRepository bundlePool = P2TargetUtils.getBundlePool(); + + // Check if already in bundle pool + if (bundlePool.contains(artifactKey)) { + // Already downloaded, find the file + File bundlePoolLocation = bundlePool.getLocation(); + File artifactFile = new File(bundlePoolLocation, + "plugins/" + artifactId + "_" + version + ".jar"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (artifactFile.exists()) { + return new Path(artifactFile.getAbsolutePath()); + } + } + + // Download the artifact to the bundle pool + IArtifactRepository sourceRepo = artifactManager.loadRepository(repositoryURL, + IRepository.REPOSITORY_HINT_MODIFIABLE, monitor); + + IArtifactRequest request = artifactManager.createMirrorRequest(artifactKey, bundlePool, null, null); + IStatus status = artifactManager.perform(new IArtifactRequest[] { request }, monitor); + + if (status.isOK()) { + // Find the downloaded file in the bundle pool + File bundlePoolLocation = bundlePool.getLocation(); + File artifactFile = new File(bundlePoolLocation, + "plugins/" + artifactId + "_" + version + ".jar"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (artifactFile.exists()) { + PDECore.log("Downloaded source bundle " + artifactId + " version " + version + " from " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + repositoryURL); + return new Path(artifactFile.getAbsolutePath()); + } + } else { + PDECore.log(status); + } + + } catch (Exception e) { + PDECore.log(e); + } + return null; + } + + /** + * Queries available software sites for the source bundle. + * + * @param plugin the plugin to locate sources for + * @return the path to the source bundle or null if not found + */ + private IPath queryAvailableSoftwareSites(IPluginBase plugin) { + String pluginId = plugin.getId(); + String sourcePluginId = pluginId + ".source"; //$NON-NLS-1$ + String pluginVersion = plugin.getVersion(); + + try { + // Get the metadata repository manager + IMetadataRepositoryManager metadataManager = P2TargetUtils.getMetadataRepositoryManager(); + if (metadataManager == null) { + return null; + } + + // Get all known repositories from P2 + URI[] knownRepositories = metadataManager.getKnownRepositories(IRepositoryManager.REPOSITORIES_ALL); + if (knownRepositories == null || knownRepositories.length == 0) { + return null; + } + + // Loop through all known repositories + for (URI repositoryURI : knownRepositories) { + try { + // Try to download the source bundle from this repository + IPath result = downloadArtifact(repositoryURI, sourcePluginId, pluginVersion); + if (result != null) { + // Found and downloaded successfully + return result; + } + } catch (Exception e) { + // Log and continue to next repository + PDECore.log("Failed to search for source in known repository " + repositoryURI + ": " //$NON-NLS-1$ //$NON-NLS-2$ + + e.getMessage()); + } + } + } catch (Exception e) { + // Log but don't fail - this is just one lookup strategy + PDECore.log(e); + } + + return null; + } + + /** + * Gets the list of selected target platform names from preferences. + * + * @param store the preference store + * @return the list of selected target names + */ + private List getSelectedTargets(IPreferenceStore store) { + String selectedTargets = store.getString(IPreferenceConstants.SOURCE_LOOKUP_SELECTED_TARGETS); + if (selectedTargets == null || selectedTargets.trim().isEmpty()) { + return List.of(); + } + return Arrays.asList(selectedTargets.split(",")); //$NON-NLS-1$ + } + + /** + * Gets the list of configured repository URLs from preferences. + * + * @param store the preference store + * @return the list of repository URLs + */ + private List getConfiguredRepositories(IPreferenceStore store) { + String repositories = store.getString(IPreferenceConstants.SOURCE_LOOKUP_REPOSITORIES); + if (repositories == null || repositories.trim().isEmpty()) { + return List.of(); + } + return Arrays.asList(repositories.split(",")); //$NON-NLS-1$ + } +} diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/copyfrom/oomph/P2Index.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/copyfrom/oomph/P2Index.java new file mode 100644 index 00000000000..3644296e239 --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/copyfrom/oomph/P2Index.java @@ -0,0 +1,553 @@ +/* + * Copyright (c) 2014, 2016-2018 Eike Stepper (Loehne, Germany) and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Eike Stepper - initial API and implementation + */ +package org.eclipse.pde.internal.core.copyfrom.oomph; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl; +import org.eclipse.emf.ecore.resource.impl.BinaryResourceImpl.EObjectInputStream; +import org.eclipse.equinox.p2.metadata.Version; + +/** + * @author Eike Stepper + */ +public class P2Index { + + private static final String INDEX_BASE = "https://download.eclipse.org/oomph/index/"; //$NON-NLS-1$ + + private long timeStamp; + + private Map repositories; + + private Repository[] repositoriesArray; + + private Map> capabilitiesMap; + + private File repositoriesCacheFile; + + private File capabilitiesCacheFile; + + private int capabilitiesRefreshHours = -1; + + private int repositoriesRefreshHours = -1; + + private File basedir; + + public P2Index(File basedir) { + this.basedir = basedir; + basedir.mkdirs(); + } + + private synchronized void initCapabilities() { + if (capabilitiesMap == null || capabilitiesCacheFile.lastModified() + + capabilitiesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + capabilitiesMap = new LinkedHashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initCapabilitiesCacheFile(); + + zipFile = new ZipFile(capabilitiesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("capabilities"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + capabilitiesRefreshHours = stream.readInt(); + + int mapSize = stream.readCompressedInt(); + for (int i = 0; i < mapSize; ++i) { + String key = stream.readSegmentedString(); + int valuesSize = stream.readCompressedInt(); + for (int j = 0; j < valuesSize; ++j) { + String value = stream.readSegmentedString(); + add(capabilitiesMap, key, value); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + closeSilent(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private synchronized void initRepositories(boolean force) { + if (repositories == null || force || repositoriesCacheFile.lastModified() + + repositoriesRefreshHours * 60 * 60 * 1000 < System.currentTimeMillis()) { + repositories = new HashMap<>(); + + ZipFile zipFile = null; + InputStream inputStream = null; + + try { + initRepositoriesCacheFile(); + + zipFile = new ZipFile(repositoriesCacheFile); + ZipEntry zipEntry = zipFile.getEntry("repositories"); //$NON-NLS-1$ + + inputStream = zipFile.getInputStream(zipEntry); + + Map options = new HashMap<>(); + options.put(BinaryResourceImpl.OPTION_VERSION, BinaryResourceImpl.BinaryIO.Version.VERSION_1_1); + options.put(BinaryResourceImpl.OPTION_STYLE_DATA_CONVERTER, Boolean.TRUE); + options.put(BinaryResourceImpl.OPTION_BUFFER_CAPACITY, 8192); + + EObjectInputStream stream = new BinaryResourceImpl.EObjectInputStream(inputStream, options); + + timeStamp = stream.readLong(); + repositoriesRefreshHours = stream.readInt(); + int repositoryCount = stream.readInt(); + + Map> composedRepositories = new HashMap<>(); + for (int id = 1; id <= repositoryCount; id++) { + RepositoryImpl repository = new RepositoryImpl(stream, id, composedRepositories); + repositories.put(id, repository); + } + + for (Map.Entry> entry : composedRepositories.entrySet()) { + RepositoryImpl repository = entry.getKey(); + for (int compositeID : entry.getValue()) { + RepositoryImpl composite = repositories.get(compositeID); + if (composite != null) { + composite.addChild(repository); + repository.addComposite(composite); + } + } + } + + try { + int problematicRepositories = stream.readInt(); + for (int i = 0; i < problematicRepositories; i++) { + int id = stream.readInt(); + int unresolvedChildren = stream.readInt(); + + RepositoryImpl repository = repositories.get(id); + repository.unresolvedChildren = unresolvedChildren; + } + } catch (Exception ex) { + } + + repositoriesArray = repositories.values().toArray(new Repository[repositories.size()]); + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(inputStream); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + } + } + } + } + } + + private boolean initRepositoriesCacheFile() throws Exception { + if (repositoriesCacheFile == null) { + repositoriesCacheFile = new File(basedir, "repositories"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "repositories"), repositoriesCacheFile); //$NON-NLS-1$ + + return true; + } + + private boolean initCapabilitiesCacheFile() throws Exception { + if (capabilitiesCacheFile == null) { + capabilitiesCacheFile = new File(basedir, "capabilities"); //$NON-NLS-1$ + } + + downloadIfModifiedSince(new URL(INDEX_BASE + "capabilities"), capabilitiesCacheFile); //$NON-NLS-1$ + + return true; + } + + @Override + public Repository[] getRepositories() { + initRepositories(false); + return repositoriesArray; + } + + @Override + public Map> getCapabilities() { + initCapabilities(); + return Collections.unmodifiableMap(capabilitiesMap); + } + + @Override + public Map> lookupCapabilities(String namespace, String name) { + initCapabilities(); + Map> capabilities = new HashMap<>(); + if (!isEmpty(namespace) && !isEmpty(name)) { + namespace = URI.encodeSegment(namespace, false); + name = URI.encodeSegment(name, false); + + BufferedReader reader = null; + + try { + InputStream inputStream = new URL(INDEX_BASE + namespace + "/" + name).openStream(); //$NON-NLS-1$ + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String line = reader.readLine(); + if (line == null) { + return capabilities; + } + + long timeStamp = Long.parseLong(line); + initRepositories(timeStamp != this.timeStamp); + + while ((line = reader.readLine()) != null) { + String[] tokens = line.split(","); //$NON-NLS-1$ + int repositoryID = Integer.parseInt(tokens[0]); + Repository repository = repositories.get(repositoryID); + if (repository != null) { + Set versions = new HashSet<>(); + for (int i = 1; i < tokens.length; i++) { + versions.add(Version.parseVersion(tokens[i])); + } + + capabilities.put(repository, versions); + } + } + } catch (FileNotFoundException ex) { + // Ignore. + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + close(reader); + } + } + + return capabilities; + } + + @Override + public Map> generateCapabilitiesFromComposedRepositories( + Map> capabilitiesFromSimpleRepositories) { + Map> capabilities = new HashMap<>(); + for (Map.Entry> entry : capabilitiesFromSimpleRepositories.entrySet()) { + Repository repository = entry.getKey(); + Set versions = entry.getValue(); + recurseComposedRepositories(capabilities, repository, versions); + } + + return capabilities; + } + + private void recurseComposedRepositories(Map> capabilities, Repository repository, + Set versions) { + for (Repository composite : repository.getComposites()) { + Set set = capabilities.get(composite); + if (set == null) { + set = new HashSet<>(); + capabilities.put(composite, set); + } + + set.addAll(versions); + recurseComposedRepositories(capabilities, composite, versions); + } + } + + private static void downloadIfModifiedSince(URL url, File file) throws IOException { + long lastModified = -1L; + if (file.isFile()) { + lastModified = file.lastModified(); + } + + InputStream inputStream = null; + OutputStream outputStream = null; + + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + if (lastModified != -1) { + connection.setIfModifiedSince(lastModified); + } + + connection.connect(); + inputStream = connection.getInputStream(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return; + } + + outputStream = new FileOutputStream(file); + copy(inputStream, outputStream); + outputStream.close(); + file.setLastModified(connection.getLastModified()); + } finally { + close(outputStream); + close(inputStream); + } + } + + /** + * @author Eike Stepper + */ + public static final class RepositoryImpl implements Repository { + public static final int UNINITIALIZED = -1; + + private static final Repository[] NO_REPOSITORIES = {}; + + private final URI location; + + private final int id; + + private final boolean composed; + + private final boolean compressed; + + private final long timestamp; + + private int capabilityCount; + + private int unresolvedChildren; + + private Repository[] children; + + private Repository[] composites; + + public RepositoryImpl(EObjectInputStream stream, int id, + Map> composedRepositories) throws IOException { + this.id = id; + location = stream.readURI(); + composed = stream.readBoolean(); + compressed = stream.readBoolean(); + timestamp = stream.readLong(); + + if (composed) { + capabilityCount = UNINITIALIZED; + } else { + capabilityCount = stream.readInt(); + } + + List composites = null; + while (stream.readBoolean()) { + if (composites == null) { + composites = new ArrayList<>(); + composedRepositories.put(this, composites); + } + + int composite = stream.readInt(); + composites.add(composite); + } + } + + @Override + public URI getLocation() { + return location; + } + + @Override + public int getID() { + return id; + } + + @Override + public boolean isComposed() { + return composed; + } + + @Override + public boolean isCompressed() { + return compressed; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public int getCapabilityCount() { + if (composed && capabilityCount == UNINITIALIZED) { + capabilityCount = 0; + for (Repository child : getChildren()) { + capabilityCount += child.getCapabilityCount(); + } + } + + return capabilityCount; + } + + @Override + public int getUnresolvedChildren() { + return unresolvedChildren; + } + + @Override + public Repository[] getChildren() { + if (children == null) { + return NO_REPOSITORIES; + } + + return children; + } + + @Override + public Repository[] getComposites() { + if (composites == null) { + return NO_REPOSITORIES; + } + + return composites; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + RepositoryImpl other = (RepositoryImpl) obj; + if (id != other.id) { + return false; + } + + return true; + } + + @Override + public int compareTo(Repository o) { + return location.toString().compareTo(o.getLocation().toString()); + } + + @Override + public String toString() { + return location.toString(); + } + + public void addChild(Repository child) { + children = addRepository(children, child); + } + + public void addComposite(Repository composite) { + composites = addRepository(composites, composite); + } + + private Repository[] addRepository(Repository[] repositories, Repository repository) { + if (repositories == null) { + return new Repository[] { repository }; + } + + int length = repositories.length; + Repository[] newRepositories = new Repository[length + 1]; + System.arraycopy(repositories, 0, newRepositories, 0, length); + newRepositories[length] = repository; + return newRepositories; + } + } + + public static boolean add(Map> map, K key, V value) { + Set set = getSet(map, key); + return set.add(value); + } + + public static Set getSet(Map> map, K key) { + Set set = map.get(key); + if (set == null) { + set = new LinkedHashSet<>(); + map.put(key, set); + } + + return set; + } + + public static void close(Closeable closeable) throws RuntimeException { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static Exception closeSilent(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + + return null; + } catch (Exception ex) { + return ex; + } + } + + public static long copy(InputStream input, OutputStream output) { + byte buffer[] = new byte[8192]; + try { + long length = 0; + int n; + + while ((n = input.read(buffer)) != -1) { + output.write(buffer, 0, n); + length += n; + } + + return length; + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + +} \ No newline at end of file diff --git a/ui/org.eclipse.pde.ui/plugin.properties b/ui/org.eclipse.pde.ui/plugin.properties index 66cb7c89f73..717aeb57d09 100644 --- a/ui/org.eclipse.pde.ui/plugin.properties +++ b/ui/org.eclipse.pde.ui/plugin.properties @@ -41,6 +41,7 @@ preferences.target.name = Target Platform preferences.compilers.name = Compilers preferences.editor.name = Editors preferences.bnd.templaterepo.name = Bnd Template Repositories +preferences.sourcelookup.name = Source Lookups preferenceKeywords.PDE=Plug-in plugin Development PDE preferenceKeywords.MainPreferencePage=java search target source dependencies JUnit launch workspace configuration plug-in fragment bundle diff --git a/ui/org.eclipse.pde.ui/plugin.xml b/ui/org.eclipse.pde.ui/plugin.xml index 4a34c0b6acd..c3bda3264b3 100644 --- a/ui/org.eclipse.pde.ui/plugin.xml +++ b/ui/org.eclipse.pde.ui/plugin.xml @@ -96,6 +96,13 @@ id="org.eclipse.pde.ui.LaunchingPreferencePage" name="Launching"> + + +
diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/IPreferenceConstants.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/IPreferenceConstants.java index b20fdcbe945..1ecf1a1b77e 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/IPreferenceConstants.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/IPreferenceConstants.java @@ -66,4 +66,23 @@ public interface IPreferenceConstants extends ILaunchingPreferenceConstants { */ public static final String TEST_PLUGIN_PATTERN = "Preferences.MainPage.testPluginPattern";//$NON-NLS-1$ + // Source Lookup preference page + /** + * Boolean preference whether additional source lookups are enabled + */ + public static final String SOURCE_LOOKUP_ENABLED = "Preferences.SourceLookup.enabled"; //$NON-NLS-1$ + /** + * String preference storing comma-separated ordered list of source lookup strategies + * Possible values: REPOSITORIES, TARGETS, INDEX, SITES + */ + public static final String SOURCE_LOOKUP_ORDER = "Preferences.SourceLookup.order"; //$NON-NLS-1$ + /** + * String preference storing comma-separated list of selected target platform names for source lookup + */ + public static final String SOURCE_LOOKUP_SELECTED_TARGETS = "Preferences.SourceLookup.selectedTargets"; //$NON-NLS-1$ + /** + * String preference storing comma-separated list of repository URLs for source lookup + */ + public static final String SOURCE_LOOKUP_REPOSITORIES = "Preferences.SourceLookup.repositories"; //$NON-NLS-1$ + } diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java index 575ac6fd8fa..57f07b74aaa 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDEUIMessages.java @@ -3444,4 +3444,22 @@ public class PDEUIMessages extends NLS { public static String AddMatchingVersion_RequireBundle; + public static String SourceLookupPreferencePage_description; + public static String SourceLookupPreferencePage_enableSourceLookup; + public static String SourceLookupPreferencePage_orderGroup; + public static String SourceLookupPreferencePage_orderDescription; + public static String SourceLookupPreferencePage_up; + public static String SourceLookupPreferencePage_down; + public static String SourceLookupPreferencePage_targetsGroup; + public static String SourceLookupPreferencePage_repositoriesGroup; + public static String SourceLookupPreferencePage_addRepository; + public static String SourceLookupPreferencePage_removeRepository; + public static String SourceLookupPreferencePage_addRepositoryTitle; + public static String SourceLookupPreferencePage_addRepositoryMessage; + public static String SourceLookupPreferencePage_unnamedTarget; + public static String SourceLookupPreferencePage_strategy_REPOSITORIES; + public static String SourceLookupPreferencePage_strategy_TARGETS; + public static String SourceLookupPreferencePage_strategy_INDEX; + public static String SourceLookupPreferencePage_strategy_SITES; + } diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties index 8b45f594b30..d4d284e9ade 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/pderesources.properties @@ -2728,4 +2728,22 @@ ProjectUpdateChange_configure_nature_and_builder=Update natures and builder ProjectUpdateChange_convert_manifest_to_bnd=Convert MANIFEST.MF to bnd instructions ProjectUpdateChange_convert_build_to_bnd=Convert build.properties to bnd instructions ProjectUpdateChange_set_pde_preference=Set {0} in preferences -StatePage_title=Target State \ No newline at end of file +StatePage_title=Target State +# Source Lookup Preference Page +SourceLookupPreferencePage_description=Configure source lookup settings for resolving source code +SourceLookupPreferencePage_enableSourceLookup=Enable additional source lookups +SourceLookupPreferencePage_orderGroup=Source Lookup Order +SourceLookupPreferencePage_orderDescription=Configure the order in which source lookups are performed (top to bottom): +SourceLookupPreferencePage_up=Up +SourceLookupPreferencePage_down=Down +SourceLookupPreferencePage_targetsGroup=Search in locations of the selected Targets +SourceLookupPreferencePage_repositoriesGroup=Search in these repositories +SourceLookupPreferencePage_addRepository=Add... +SourceLookupPreferencePage_removeRepository=Remove +SourceLookupPreferencePage_addRepositoryTitle=Add Repository +SourceLookupPreferencePage_addRepositoryMessage=Enter repository URL: +SourceLookupPreferencePage_unnamedTarget= +SourceLookupPreferencePage_strategy_REPOSITORIES=Configured Repositories +SourceLookupPreferencePage_strategy_TARGETS=Selected Target Platforms +SourceLookupPreferencePage_strategy_INDEX=Eclipse Index +SourceLookupPreferencePage_strategy_SITES=Available Software Sites diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/PreferenceInitializer.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/PreferenceInitializer.java index 28a89439567..6d5df6aae92 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/PreferenceInitializer.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/PreferenceInitializer.java @@ -32,6 +32,13 @@ public void initializeDefaultPreferences() { store.setDefault(IPreferenceConstants.WORKSPACE_PLUGINS_OVERRIDE_TARGET, true); store.setDefault(IPreferenceConstants.DISABLE_API_ANALYSIS_BUILDER, false); store.setDefault(IPreferenceConstants.TEST_PLUGIN_PATTERN, ICoreConstants.TEST_PLUGIN_PATTERN_DEFAULTVALUE); + + // Source Lookup defaults + store.setDefault(IPreferenceConstants.SOURCE_LOOKUP_ENABLED, false); + // Default order: configured repositories, selected target, eclipse index, available software sites + store.setDefault(IPreferenceConstants.SOURCE_LOOKUP_ORDER, "REPOSITORIES,TARGETS,INDEX,SITES"); //$NON-NLS-1$ + store.setDefault(IPreferenceConstants.SOURCE_LOOKUP_SELECTED_TARGETS, ""); //$NON-NLS-1$ + store.setDefault(IPreferenceConstants.SOURCE_LOOKUP_REPOSITORIES, ""); //$NON-NLS-1$ } } diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/SourceLookupPreferencePage.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/SourceLookupPreferencePage.java new file mode 100644 index 00000000000..ee2464354e9 --- /dev/null +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/preferences/SourceLookupPreferencePage.java @@ -0,0 +1,390 @@ +/******************************************************************************* + * Copyright (c) 2025 Christoph Läubrich and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.ui.preferences; + +import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.InputDialog; +import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.window.Window; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetHandle; +import org.eclipse.pde.core.target.ITargetPlatformService; +import org.eclipse.pde.internal.core.target.TargetPlatformService; +import org.eclipse.pde.internal.ui.IHelpContextIds; +import org.eclipse.pde.internal.ui.IPreferenceConstants; +import org.eclipse.pde.internal.ui.PDEPlugin; +import org.eclipse.pde.internal.ui.PDEUIMessages; +import org.eclipse.pde.internal.ui.SWTFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.eclipse.ui.PlatformUI; + +/** + * Preference page for managing source lookup settings + */ +public class SourceLookupPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { + + public static final String ID = "org.eclipse.pde.ui.SourceLookupPreferencePage"; //$NON-NLS-1$ + + // Source lookup strategy identifiers + private static final String STRATEGY_REPOSITORIES = "REPOSITORIES"; //$NON-NLS-1$ + private static final String STRATEGY_TARGETS = "TARGETS"; //$NON-NLS-1$ + private static final String STRATEGY_INDEX = "INDEX"; //$NON-NLS-1$ + private static final String STRATEGY_SITES = "SITES"; //$NON-NLS-1$ + + private Button fEnableSourceLookup; + private TableViewer fOrderTableViewer; + private Button fUpButton; + private Button fDownButton; + private CheckboxTableViewer fTargetsTableViewer; + private CheckboxTableViewer fRepositoriesTableViewer; + private Button fAddRepositoryButton; + private Button fRemoveRepositoryButton; + + private List fLookupOrder = new ArrayList<>(); + private List fTargets = new ArrayList<>(); + private List fRepositories = new ArrayList<>(); + + public SourceLookupPreferencePage() { + setPreferenceStore(PDEPlugin.getDefault().getPreferenceStore()); + setDescription(PDEUIMessages.SourceLookupPreferencePage_description); + } + + @Override + protected Control createContents(Composite parent) { + Composite composite = SWTFactory.createComposite(parent, 1, 1, GridData.FILL_BOTH, 0, 0); + ((GridLayout) composite.getLayout()).verticalSpacing = 15; + ((GridLayout) composite.getLayout()).marginTop = 15; + + // Enable checkbox + fEnableSourceLookup = new Button(composite, SWT.CHECK); + fEnableSourceLookup.setText(PDEUIMessages.SourceLookupPreferencePage_enableSourceLookup); + fEnableSourceLookup.setSelection(getPreferenceStore().getBoolean(IPreferenceConstants.SOURCE_LOOKUP_ENABLED)); + fEnableSourceLookup.addSelectionListener(widgetSelectedAdapter(e -> updateEnablement())); + + // Lookup order group + Group orderGroup = SWTFactory.createGroup(composite, PDEUIMessages.SourceLookupPreferencePage_orderGroup, 2, 1, GridData.FILL_HORIZONTAL); + + Label orderDescription = new Label(orderGroup, SWT.WRAP); + orderDescription.setText(PDEUIMessages.SourceLookupPreferencePage_orderDescription); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + gd.widthHint = 400; + orderDescription.setLayoutData(gd); + + fOrderTableViewer = new TableViewer(orderGroup, SWT.BORDER | SWT.V_SCROLL); + gd = new GridData(GridData.FILL_BOTH); + gd.heightHint = 100; + fOrderTableViewer.getControl().setLayoutData(gd); + fOrderTableViewer.setLabelProvider(new LabelProvider() { + @Override + public String getText(Object element) { + return getStrategyLabel((String) element); + } + }); + fOrderTableViewer.setContentProvider(ArrayContentProvider.getInstance()); + fOrderTableViewer.addSelectionChangedListener(event -> updateOrderButtons()); + + // Up/Down buttons + Composite buttonComposite = SWTFactory.createComposite(orderGroup, 1, 1, GridData.FILL_VERTICAL | GridData.VERTICAL_ALIGN_BEGINNING, 0, 0); + + fUpButton = SWTFactory.createPushButton(buttonComposite, PDEUIMessages.SourceLookupPreferencePage_up, null); + fUpButton.addSelectionListener(widgetSelectedAdapter(e -> handleMoveUp())); + + fDownButton = SWTFactory.createPushButton(buttonComposite, PDEUIMessages.SourceLookupPreferencePage_down, null); + fDownButton.addSelectionListener(widgetSelectedAdapter(e -> handleMoveDown())); + + // Load lookup order + loadLookupOrder(); + + // Target platforms group + Group targetsGroup = SWTFactory.createGroup(composite, PDEUIMessages.SourceLookupPreferencePage_targetsGroup, 1, 1, GridData.FILL_BOTH); + targetsGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + + fTargetsTableViewer = CheckboxTableViewer.newCheckList(targetsGroup, SWT.BORDER | SWT.V_SCROLL); + gd = new GridData(GridData.FILL_BOTH); + gd.heightHint = 120; + fTargetsTableViewer.getControl().setLayoutData(gd); + fTargetsTableViewer.setLabelProvider(new LabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof ITargetDefinition) { + String name = ((ITargetDefinition) element).getName(); + return name != null ? name : PDEUIMessages.SourceLookupPreferencePage_unnamedTarget; + } + return super.getText(element); + } + }); + fTargetsTableViewer.setContentProvider(ArrayContentProvider.getInstance()); + + // Load available targets + loadTargets(); + + // Repositories group + Group repositoriesGroup = SWTFactory.createGroup(composite, PDEUIMessages.SourceLookupPreferencePage_repositoriesGroup, 2, 1, GridData.FILL_BOTH); + repositoriesGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + + fRepositoriesTableViewer = CheckboxTableViewer.newCheckList(repositoriesGroup, SWT.BORDER | SWT.V_SCROLL); + gd = new GridData(GridData.FILL_BOTH); + gd.heightHint = 120; + fRepositoriesTableViewer.getControl().setLayoutData(gd); + fRepositoriesTableViewer.setLabelProvider(new LabelProvider()); + fRepositoriesTableViewer.setContentProvider(ArrayContentProvider.getInstance()); + fRepositoriesTableViewer.addSelectionChangedListener(event -> updateRepositoryButtons()); + + // Load repositories + loadRepositories(); + + // Repository buttons + buttonComposite = SWTFactory.createComposite(repositoriesGroup, 1, 1, GridData.FILL_VERTICAL | GridData.VERTICAL_ALIGN_BEGINNING, 0, 0); + + fAddRepositoryButton = SWTFactory.createPushButton(buttonComposite, PDEUIMessages.SourceLookupPreferencePage_addRepository, null); + fAddRepositoryButton.addSelectionListener(widgetSelectedAdapter(e -> handleAddRepository())); + + fRemoveRepositoryButton = SWTFactory.createPushButton(buttonComposite, PDEUIMessages.SourceLookupPreferencePage_removeRepository, null); + fRemoveRepositoryButton.addSelectionListener(widgetSelectedAdapter(e -> handleRemoveRepository())); + + updateEnablement(); + updateRepositoryButtons(); + updateOrderButtons(); + + return composite; + } + + private String getStrategyLabel(String strategy) { + switch (strategy) { + case STRATEGY_REPOSITORIES: + return PDEUIMessages.SourceLookupPreferencePage_strategy_REPOSITORIES; + case STRATEGY_TARGETS: + return PDEUIMessages.SourceLookupPreferencePage_strategy_TARGETS; + case STRATEGY_INDEX: + return PDEUIMessages.SourceLookupPreferencePage_strategy_INDEX; + case STRATEGY_SITES: + return PDEUIMessages.SourceLookupPreferencePage_strategy_SITES; + default: + return strategy; + } + } + + private void loadLookupOrder() { + String order = getPreferenceStore().getString(IPreferenceConstants.SOURCE_LOOKUP_ORDER); + if (order == null || order.trim().isEmpty()) { + // Use default order + fLookupOrder = new ArrayList<>(Arrays.asList(STRATEGY_REPOSITORIES, STRATEGY_TARGETS, STRATEGY_INDEX, STRATEGY_SITES)); + } else { + fLookupOrder = new ArrayList<>(Arrays.asList(order.split(","))); //$NON-NLS-1$ + } + fOrderTableViewer.setInput(fLookupOrder); + } + + private void loadTargets() { + ITargetPlatformService service = TargetPlatformService.getDefault(); + if (service != null) { + ITargetHandle[] targets = service.getTargets(null); + for (ITargetHandle target : targets) { + try { + fTargets.add(target.getTargetDefinition()); + } catch (CoreException e) { + PDEPlugin.log(e); + } + } + fTargetsTableViewer.setInput(fTargets); + + // Restore selected targets + String selectedTargets = getPreferenceStore().getString(IPreferenceConstants.SOURCE_LOOKUP_SELECTED_TARGETS); + if (!selectedTargets.isEmpty()) { + List selectedNames = Arrays.asList(selectedTargets.split(",")); //$NON-NLS-1$ + List toCheck = fTargets.stream() + .filter(t -> t.getName() != null && selectedNames.contains(t.getName())) + .collect(Collectors.toList()); + fTargetsTableViewer.setCheckedElements(toCheck.toArray()); + } + } + } + + private void loadRepositories() { + String repositories = getPreferenceStore().getString(IPreferenceConstants.SOURCE_LOOKUP_REPOSITORIES); + if (!repositories.isEmpty()) { + fRepositories = new ArrayList<>(Arrays.asList(repositories.split(","))); //$NON-NLS-1$ + fRepositoriesTableViewer.setInput(fRepositories); + fRepositoriesTableViewer.setAllChecked(true); + } + } + + private void handleMoveUp() { + IStructuredSelection selection = (IStructuredSelection) fOrderTableViewer.getSelection(); + if (!selection.isEmpty()) { + String strategy = (String) selection.getFirstElement(); + int index = fLookupOrder.indexOf(strategy); + if (index > 0) { + fLookupOrder.remove(index); + fLookupOrder.add(index - 1, strategy); + fOrderTableViewer.refresh(); + fOrderTableViewer.setSelection(selection); + updateOrderButtons(); + } + } + } + + private void handleMoveDown() { + IStructuredSelection selection = (IStructuredSelection) fOrderTableViewer.getSelection(); + if (!selection.isEmpty()) { + String strategy = (String) selection.getFirstElement(); + int index = fLookupOrder.indexOf(strategy); + if (index < fLookupOrder.size() - 1) { + fLookupOrder.remove(index); + fLookupOrder.add(index + 1, strategy); + fOrderTableViewer.refresh(); + fOrderTableViewer.setSelection(selection); + updateOrderButtons(); + } + } + } + + private void handleAddRepository() { + InputDialog dialog = new InputDialog(getShell(), + PDEUIMessages.SourceLookupPreferencePage_addRepositoryTitle, + PDEUIMessages.SourceLookupPreferencePage_addRepositoryMessage, + "", //$NON-NLS-1$ + null); + if (dialog.open() == Window.OK) { + String url = dialog.getValue().trim(); + if (!url.isEmpty() && !fRepositories.contains(url)) { + fRepositories.add(url); + fRepositoriesTableViewer.setInput(fRepositories); + fRepositoriesTableViewer.setChecked(url, true); + } + } + } + + private void handleRemoveRepository() { + IStructuredSelection selection = (IStructuredSelection) fRepositoriesTableViewer.getSelection(); + if (!selection.isEmpty()) { + Object[] elements = selection.toArray(); + for (Object element : elements) { + fRepositories.remove(element); + } + fRepositoriesTableViewer.setInput(fRepositories); + } + } + + private void updateEnablement() { + boolean enabled = fEnableSourceLookup.getSelection(); + fOrderTableViewer.getControl().setEnabled(enabled); + fUpButton.setEnabled(enabled); + fDownButton.setEnabled(enabled); + fTargetsTableViewer.getControl().setEnabled(enabled); + fRepositoriesTableViewer.getControl().setEnabled(enabled); + fAddRepositoryButton.setEnabled(enabled); + fRemoveRepositoryButton.setEnabled(enabled && !fRepositoriesTableViewer.getSelection().isEmpty()); + updateOrderButtons(); + } + + private void updateOrderButtons() { + if (!fEnableSourceLookup.getSelection()) { + fUpButton.setEnabled(false); + fDownButton.setEnabled(false); + return; + } + + IStructuredSelection selection = (IStructuredSelection) fOrderTableViewer.getSelection(); + if (selection.isEmpty()) { + fUpButton.setEnabled(false); + fDownButton.setEnabled(false); + } else { + String strategy = (String) selection.getFirstElement(); + int index = fLookupOrder.indexOf(strategy); + fUpButton.setEnabled(index > 0); + fDownButton.setEnabled(index < fLookupOrder.size() - 1); + } + } + + private void updateRepositoryButtons() { + fRemoveRepositoryButton.setEnabled(fEnableSourceLookup.getSelection() && !fRepositoriesTableViewer.getSelection().isEmpty()); + } + + @Override + public void createControl(Composite parent) { + super.createControl(parent); + Dialog.applyDialogFont(getControl()); + PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(), IHelpContextIds.MAIN_PREFERENCE_PAGE); + } + + @Override + public boolean performOk() { + getPreferenceStore().setValue(IPreferenceConstants.SOURCE_LOOKUP_ENABLED, fEnableSourceLookup.getSelection()); + + // Save lookup order + String order = String.join(",", fLookupOrder); //$NON-NLS-1$ + getPreferenceStore().setValue(IPreferenceConstants.SOURCE_LOOKUP_ORDER, order); + + // Save selected targets + Object[] checkedTargets = fTargetsTableViewer.getCheckedElements(); + String selectedTargets = Arrays.stream(checkedTargets) + .filter(t -> t instanceof ITargetDefinition) + .map(t -> ((ITargetDefinition) t).getName()) + .filter(name -> name != null) + .collect(Collectors.joining(",")); //$NON-NLS-1$ + getPreferenceStore().setValue(IPreferenceConstants.SOURCE_LOOKUP_SELECTED_TARGETS, selectedTargets); + + // Save repositories + String repositories = String.join(",", fRepositories); //$NON-NLS-1$ + getPreferenceStore().setValue(IPreferenceConstants.SOURCE_LOOKUP_REPOSITORIES, repositories); + + return super.performOk(); + } + + @Override + protected void performDefaults() { + fEnableSourceLookup.setSelection(getPreferenceStore().getDefaultBoolean(IPreferenceConstants.SOURCE_LOOKUP_ENABLED)); + + // Reset to default order + fLookupOrder = new ArrayList<>(Arrays.asList(STRATEGY_REPOSITORIES, STRATEGY_TARGETS, STRATEGY_INDEX, STRATEGY_SITES)); + fOrderTableViewer.setInput(fLookupOrder); + fOrderTableViewer.refresh(); + + fTargetsTableViewer.setCheckedElements(new Object[0]); + fRepositories.clear(); + fRepositoriesTableViewer.setInput(fRepositories); + updateEnablement(); + updateRepositoryButtons(); + updateOrderButtons(); + super.performDefaults(); + } + + @Override + public void init(IWorkbench workbench) { + } +}