diff options
author | Paul Pazderski | 2019-06-04 16:56:22 +0000 |
---|---|---|
committer | Eric Williams | 2019-06-10 15:51:56 +0000 |
commit | a4938743e4a8061dce698d5f6c3850bb2a4930c7 (patch) | |
tree | 06cc6ba776695aaeff3114a02f4b51a2716001f4 /examples | |
parent | 55635f432dd8824cb107c035a00fa4435bbf93df (diff) | |
download | eclipse.platform.swt-a4938743e4a8061dce698d5f6c3850bb2a4930c7.tar.gz eclipse.platform.swt-a4938743e4a8061dce698d5f6c3850bb2a4930c7.tar.xz eclipse.platform.swt-a4938743e4a8061dce698d5f6c3850bb2a4930c7.zip |
Bug 547934 - [Snippets] Add SnippetExplorer to filter and launch SWT
Snippets
Change-Id: I47d96bf18e94b8c318e3f00a32b5af24a452989a
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
Diffstat (limited to 'examples')
3 files changed, 1275 insertions, 6 deletions
diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetExplorer.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetExplorer.java new file mode 100644 index 0000000000..f3960b28cb --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetExplorer.java @@ -0,0 +1,1229 @@ +/******************************************************************************* + * Copyright (c) 2019 Paul Pazderski 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: + * Paul Pazderski - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.snippets; + +import java.io.*; +import java.lang.ProcessBuilder.*; +import java.lang.reflect.*; +import java.nio.charset.*; +import java.nio.file.*; +import java.nio.file.Path; +import java.util.*; +import java.util.List; +import java.util.concurrent.*; +import java.util.regex.*; +import java.util.regex.Pattern; + +import org.eclipse.swt.*; +import org.eclipse.swt.custom.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.layout.*; +import org.eclipse.swt.program.*; +import org.eclipse.swt.widgets.*; + +/** + * A useful application to list, filter and run the available Snippets. + */ +public class SnippetExplorer { + + private static final String USAGE_EXPLANATION = "Welcome to the SnippetExplorer!\n" + + "\n" + + "This tool will help you to explore and test the large collection of SWT example Snippets. " + + "You can use the text field on top to filter the Snippets by there description or Snippet number. " + + "To start a Snippet you can either double click its entry, press enter or use the button below. " + + "It is also possible to start multiple Snippets at once. (exact behavior depends on selected Snippet-Runner)\n" + + "\n" + + "It is recommended to start the Snippet Explorer connected to a console since some of the Snippets " + + "print useful informations to the console or do not open a window at all.\n" + + "\n" + + "The Explorer supports (dependent on your OS and environment) different modes to start Snippets. Those runners are:\n" + + "\n" + + " \u2022 Thread Runner: Snippets are executed as threads of the Explorer.\n" + + "\t- This runner is only available if the environment supports multiple Displays at the same time. (only Windows at the moment)\n" + + "\t- Multiple Snippets can be run parallel using this runner.\n" + + "\t- All running Snippets are closed when the explorer exits.\n" + + "\t- If to many Snippets are run in parallel SWT may run out of handles.\n" + + "\t- If a Snippet calls System.exit it will also force the explorer itself and all other running Snippets to exit as well.\n" + + "\n" + + " \u2022 Process Runner: Snippets are executed as separate processes.\n" + + "\t- This runner is only available if a JRE was found which can be used to start the Snippets.\n" + + "\t- Multiple Snippets can be run parallel using this runner.\n" + + "\t- This runner is more likely to fail Snippet launch due to incomplete classpath or other launch problems.\n" + + "\t- When the explorer exits it try to close all running Snippets but has less control over it as the Thread runner.\n" + + "\t- Unlike the Thread runner the Process runner is resisted to faulty Snippets. (e.g. Snippets calling System.exit)\n" + + "\n" + + " \u2022 Serial Runner: Snippets are executed one after another instead of the explorer.\n" + + "\t- This runner is always available.\n" + + "\t- Cannot run Snippets parallel.\n" + + "\t- To run Snippets the explorer gets closed, executes the selected Snippets one after another in the same JVM " + + "and after the last Snippet has finished restarts the Snippet Explorer.\n" + + "\t- A Snippet calling System.exit will stop the Snippet chain and the explorer itself can not restart."; + + /** Max length for Snippet description in the main table. */ + private static final int MAX_DESCRIPTION_LENGTH_IN_TABLE = 80; + /** + * If the user tries to start more than this number of Snippets at once a + * warning message is shown. + */ + private static final int START_MANY_SNIPPETS_WARNING_THREASHOLD = 10; + /** Message shown in the filter text field if empty. */ + private static final String FILTER_HINT = "type to filter list"; + /** + * Delay in milliseconds before a changed filter value is applied on the list. + */ + private static final int FILTER_DELAY_MS = 200; + /** + * Time snippets get to stop before tried to be killed forcefully. (currently + * applied per runner) + */ + private static final int SHUTDOWN_GRACE_TIME_MS = 5000; + /** Link to online snippet source. Used if no local source is available. */ + private static final String SNIPPET_SOURCE_LINK_TEMPLATE = "https://git.eclipse.org/c/platform/" + + "eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/%s.java"; + + /** + * Whether or not SWT support creating of multiple {@link Display} instances on + * the current system. Required to use the thread runner mode. + */ + private static boolean multiDisplaySupport; + /** + * The command used to invoke the java binary. May be <code>null</code> if not + * found. Required to use the process runner mode. + */ + private static String javaCommand; + /** The list of available Snippets. */ + private static List<Snippet> snippets; + + /** The runner used for thread mode. */ + private final SnippetRunner THREAD_RUNNER = new SnippetRunnerThread(); + /** The runner used for process mode. */ + private final SnippetRunner PROCESS_RUNNER = new SnippetRunnerProcess(); + + private Display display; + private Shell shell; + + /** Helper to perform the delayed list update if filter changed. */ + private ListUpdater listUpdater; + /** Text field to filter Snippet list. */ + private Text filterField; + /** The main table listing available Snippets. */ + private Table snippetTable; + /** Button to run selected Snippets. */ + private Button startSelectedButton; + /** Snippet runner selection. */ + private Combo runnerCombo; + /** The tabfolder to show information for selected Snippet. */ + private TabFolder infoTabs; + /** Element to show Snippet description or general help. */ + private StyledText descriptionView; + /** + * Element to show Snippets source code or link to source if not local + * available. + */ + private StyledText sourceView; + /** Element to show Snippet preview if possible. */ + private Label previewImageLabel; + + /** + * The snippet runner used for next snippet start. If <code>null</code> Snippets + * are run serial. + */ + private SnippetRunner snippetRunner; + /** Used to map {@link #runnerCombo} selection to actual Snippet runner. */ + private List<SnippetRunner> runnerMapping = new ArrayList<>(); + /** The Snippet currently shown in {@link #infoTabs}. May be <code>null</code>. */ + private Snippet currentInfoSnippet = null; + /** Snippets currently run in serial runner mode. May be <code>null</code>. */ + private List<Snippet> serialSnippets; + /** + * The SnippetExplorer location for the next {@link Shell#open()}. Used for + * restart after serial runner finished. + */ + private Point nextExplorerLocation = null; + + /** + * SnippetExplorer main method. + * + * @param args does not parse any arguments + */ + public static void main(String[] args) throws Exception { + final String os = System.getProperty("os.name"); + multiDisplaySupport = (os != null && os.toLowerCase().contains("windows")); + if (canRunCommand("java")) { + javaCommand = "java"; + } else { + final String javaHome = System.getProperty("java.home"); + if (javaHome != null) { + final Path java = Paths.get(javaHome, "bin", "java"); + java.normalize(); + if (canRunCommand(java.toString())) { + javaCommand = java.toString(); + } + } + } + + snippets = loadSnippets(); + snippets.sort((a, b) -> { + int cmp = Integer.compare(a.snippetNum, b.snippetNum); + if (cmp == 0) { + cmp = a.snippetName.compareTo(b.snippetName); + } + return cmp; + }); + + new SnippetExplorer().open(); + } + + /** + * Test if the given command can be executed. + * + * @param command command to test + * @return <code>false</code> if executing the command failed for any reason + */ + private static boolean canRunCommand(String command) { + try { + final Process p = Runtime.getRuntime().exec(command); + p.waitFor(150, TimeUnit.MILLISECONDS); + if (p.isAlive()) { + p.destroy(); + p.waitFor(100, TimeUnit.MILLISECONDS); + if (p.isAlive()) { + p.destroyForcibly(); + } + } + return true; + } catch (Exception ex) { + return false; + } + } + + public SnippetExplorer() { + } + + /** + * Initializes and shows the SnippetExplorer. The method doesn't return until + * the explorer is closed or otherwise disposed. + */ + public void open() { + initialize(); + runEventLoop(); + } + + /** + * Initialize the SnippetExplorer. Can be called again if the current explorer + * was properly disposed. + */ + private void initialize() { + display = Display.getDefault(); + snippetRunner = null; + shell = new Shell(display); + if (nextExplorerLocation != null) { + shell.setLocation(nextExplorerLocation); + } + shell.setText("SWT Snippet Explorer"); + + createControls(shell); + + final String[] columns = new String[] { "Name", "Description" }; + for (String col : columns) { + final TableColumn tableCol = new TableColumn(snippetTable, SWT.NONE); + tableCol.setText(col); + tableCol.setToolTipText(col); + tableCol.setResizable(true); + tableCol.setMoveable(true); + } + updateTable(null); + + for (TableColumn col : snippetTable.getColumns()) { + col.pack(); + } + final GridData rightSideLayout = (GridData) infoTabs.getLayoutData(); + final Point tableSize = snippetTable.getSize(); + rightSideLayout.widthHint = tableSize.x; + rightSideLayout.heightHint = tableSize.y; + shell.pack(); + shell.open(); + } + + /** Initialize the SnippetExplorer controls. + * + * @param shell parent shell + */ + private void createControls(Shell shell) { + shell.setLayout(new FormLayout()); + + if (listUpdater == null) { + listUpdater = new ListUpdater(); + listUpdater.start(); + } + + final Composite leftContainer = new Composite(shell, SWT.NONE); + leftContainer.setLayout(new GridLayout()); + + final Sash splitter = new Sash(shell, SWT.BORDER | SWT.VERTICAL); + final int splitterWidth = 3; + splitter.addListener(SWT.Selection, e -> splitter.setBounds(e.x, e.y, e.width, e.height)); + + final Composite rightContainer = new Composite(shell, SWT.NONE); + rightContainer.setLayout(new GridLayout()); + + FormData formData = new FormData(); + formData.left = new FormAttachment(0, 0); + formData.right = new FormAttachment(splitter, 0); + formData.top = new FormAttachment(0, 0); + formData.bottom = new FormAttachment(100, 0); + leftContainer.setLayoutData(formData); + + formData = new FormData(); + formData.left = new FormAttachment(50, 0); + formData.right = new FormAttachment(50, splitterWidth); + formData.top = new FormAttachment(0, 0); + formData.bottom = new FormAttachment(100, 0); + splitter.setLayoutData(formData); + splitter.addListener(SWT.Selection, event -> { + final FormData splitterFormData = (FormData) splitter.getLayoutData(); + splitterFormData.left = new FormAttachment(0, event.x); + splitterFormData.right = new FormAttachment(0, event.x + splitterWidth); + shell.layout(); + }); + + formData = new FormData(); + formData.left = new FormAttachment(splitter, 0); + formData.right = new FormAttachment(100, 0); + formData.top = new FormAttachment(0, 0); + formData.bottom = new FormAttachment(100, 0); + rightContainer.setLayoutData(formData); + + filterField = new Text(leftContainer, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL); + filterField.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + filterField.setMessage(FILTER_HINT); + filterField.addListener(SWT.Modify, event -> { + listUpdater.updateInMs(FILTER_DELAY_MS); + }); + snippetTable = new Table(leftContainer, SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION); + snippetTable.setLinesVisible(true); + snippetTable.setHeaderVisible(true); + final GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + data.heightHint = 500; + snippetTable.setLayoutData(data); + snippetTable.addListener(SWT.MouseDoubleClick, event -> { + final Point clickPoint = new Point(event.x, event.y); + launchSnippet(snippetTable.getItem(clickPoint)); + }); + snippetTable.addListener(SWT.KeyUp, event -> { + if (event.keyCode == '\r' || event.keyCode == '\n') { + launchSnippet(snippetTable.getSelection()); + } + }); + + final Composite buttonRow = new Composite(leftContainer, SWT.NONE); + buttonRow.setLayout(new GridLayout(3, false)); + buttonRow.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); + + startSelectedButton = new Button(buttonRow, SWT.LEAD); + startSelectedButton.setText(" Start &selected Snippets"); + snippetTable.addListener(SWT.Selection, event -> { + startSelectedButton.setEnabled(snippetTable.getSelectionCount() > 0); + updateInfoTab(snippetTable.getSelection()); + }); + startSelectedButton.setEnabled(snippetTable.getSelectionCount() > 0); + startSelectedButton.addListener(SWT.Selection, event -> { + launchSnippet(snippetTable.getSelection()); + }); + + final Label runnerLabel = new Label(buttonRow, SWT.NONE); + runnerLabel.setText("Snippet Runner:"); + runnerLabel.setLayoutData(new GridData(SWT.TRAIL, SWT.CENTER, true, false)); + + runnerCombo = new Combo(buttonRow, SWT.TRAIL | SWT.DROP_DOWN | SWT.READ_ONLY); + runnerMapping.clear(); + if (multiDisplaySupport) { + runnerCombo.add("Thread"); + runnerMapping.add(THREAD_RUNNER); + } + if (javaCommand != null) { + runnerCombo.add("Process"); + runnerMapping.add(PROCESS_RUNNER); + } + runnerCombo.add("Serial"); + runnerMapping.add(null); + runnerCombo.setData(runnerMapping); + runnerCombo.addListener(SWT.Modify, event -> { + if (runnerMapping.size() > runnerCombo.getSelectionIndex()) { + snippetRunner = runnerMapping.get(runnerCombo.getSelectionIndex()); + } else { + System.err.println("Unknown runner index " + runnerCombo.getSelectionIndex()); + } + }); + runnerCombo.select(0); + + infoTabs = new TabFolder(rightContainer, SWT.TOP); + infoTabs.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + descriptionView = new StyledText(infoTabs, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY | SWT.V_SCROLL); + + sourceView = new StyledText(infoTabs, SWT.MULTI | SWT.READ_ONLY | SWT.V_SCROLL | SWT.H_SCROLL); + setMonospaceFont(sourceView); + + final ScrolledComposite previewContainer = new ScrolledComposite(infoTabs, SWT.V_SCROLL | SWT.H_SCROLL); + previewImageLabel = new Label(previewContainer, SWT.NONE); + previewContainer.setContent(previewImageLabel); + + final TabItem descriptionTab = new TabItem(infoTabs, SWT.NONE); + descriptionTab.setText("Description"); + descriptionTab.setControl(descriptionView); + final TabItem sourceTab = new TabItem(infoTabs, SWT.NONE); + sourceTab.setText("Source"); + sourceTab.setControl(sourceView); + final TabItem previewTab = new TabItem(infoTabs, SWT.NONE); + previewTab.setText("Preview"); + previewTab.setControl(previewContainer); + + updateInfoTab(null, true); + updateInfoTab(snippetTable.getSelection()); + } + + /** + * Try to set a monospace font for this control. Other font properties like + * fontSize remain unchanged. + * + * @param control control to modify + */ + private void setMonospaceFont(Control control) { + final FontData[] fontData = control.getFont().getFontData(); + Font font = null; + if (font == null) { + font = tryCreateFont("Consolas", fontData); + } + if (font == null) { + font = tryCreateFont("DejaVu Sans Mono", fontData); + } + if (font == null) { + font = tryCreateFont("Noto Mono", fontData); + } + if (font == null) { + font = tryCreateFont("Liberation Mono", fontData); + } + if (font == null) { + font = tryCreateFont("Ubuntu Mono", fontData); + } + if (font == null) { + font = tryCreateFont("Courier New", fontData); + } + if (font == null) { + font = tryCreateFont("Courier", fontData); + } + if (font == null) { + font = tryCreateFont("Monospace", fontData); + } + + if (font != null) { + control.setFont(font); + } + } + + /** + * Try to create font with given name based on existing {@link FontData}. + * + * @param fontName name of font to create + * @param existingData existing font data to be used for other font attributes + * @return the created font or <code>null</code> if failed (e.g. no font + * available with given name) + */ + private Font tryCreateFont(String fontName, FontData[] existingData) { + for (int i = 0; i < existingData.length; i++) { + existingData[i].setName(fontName); + } + try { + return new Font(display, existingData); + } catch (SWTException e) { + return null; + } + } + + /** + * Update the info tabs for the given items. The behavior may change. At the + * moment informations are only shown for single items. + * + * @param items items to show info for + */ + private void updateInfoTab(TableItem[] items) { + // if multiple snippets are selected no info are shown + if (items.length != 1) { + updateInfoTab((TableItem) null, false); + } else { + updateInfoTab(items[0], false); + } + } + + /** + * Update the info tabs (right side of the explorer) for the given item. + * + * @param item the selected item containing Snippet metadata (may be + * <code>null</code>) + * @param force the tabs are only updated if they not already show info for the + * given item. If this is <code>true</code> the tabs are updated + * anyway. + */ + private void updateInfoTab(TableItem item, boolean force) { + final Snippet snippet = (item != null && item.getData() instanceof Snippet) ? (Snippet) item.getData() : null; + if (!force && currentInfoSnippet == snippet) { + return; + } + if (snippet == null) { + descriptionView.setText(USAGE_EXPLANATION); + sourceView.setText(""); + updatePreviewImage(null, ""); + } else { + descriptionView.setText(snippet.snippetName + "\n\n" + snippet.description); + if (snippet.source == null) { + sourceView.setWordWrap(true); + final String msg = "No source available for " + snippet.snippetName + " but you may find it at:\n\n"; + final String link = String.format(Locale.ROOT, SNIPPET_SOURCE_LINK_TEMPLATE, snippet.snippetName); + sourceView.setText(msg + link); + + final StyleRange linkStyle = new StyleRange(); + linkStyle.start = msg.length(); + linkStyle.length = link.length(); + linkStyle.underline = true; + linkStyle.underlineStyle = SWT.UNDERLINE_LINK; + sourceView.setStyleRange(linkStyle); + + sourceView.addListener(SWT.MouseDown, event -> { + int offset = sourceView.getOffsetAtPoint(new Point(event.x, event.y)); + if (offset != -1) { + try { + final StyleRange style = sourceView.getStyleRangeAtOffset(offset); + if (style != null && style.underline && style.underlineStyle == SWT.UNDERLINE_LINK) { + Program.launch(link); + } + } catch (IllegalArgumentException e) { + // no character under event.x, event.y + } + } + }); + } else { + sourceView.setWordWrap(false); + sourceView.setText(snippet.source); + } + try { + final Image previewImage = getPreviewImage(snippet); + updatePreviewImage(previewImage, previewImage == null ? "No preview image available." : ""); + } catch (IOException e) { + updatePreviewImage(null, "Failed to load preview image: " + e); + } + } + currentInfoSnippet = snippet; + } + + /** + * Update the control showing the image. If <em>image</em> is <code>null</code> + * show the <em>text</em> instead. + * + * @param image the image to show + * @param text the alternative text to show if image is <code>null</code> + */ + private void updatePreviewImage(Image image, String text) { + final Image previousImage = previewImageLabel.getImage(); + previewImageLabel.setImage(image); + if (image == null && text != null) { + previewImageLabel.setText(text); + } + if (previousImage != null) { + previousImage.dispose(); + } + previewImageLabel.pack(true); + } + + /** + * Get the preview image for the Snippet. + * + * @param snippet Snippet's metadata to load preview image for + * @return the preview image or <code>null</code> if none available + * @throws IOException if image loading failed + */ + private Image getPreviewImage(Snippet snippet) throws IOException { + final Path previewFile = Paths.get("previews", snippet.snippetName + ".png"); + if (Files.exists(previewFile)) { + try (InputStream imageStream = Files.newInputStream(previewFile)) { + return new Image(display, imageStream); + } + } + try (InputStream imageStream = SnippetExplorer.class + .getResourceAsStream("/previews/" + snippet.snippetName + ".png")) { + if (imageStream != null) { + return new Image(display, imageStream); + } + } + try (InputStream imageStream = ClassLoader + .getSystemResourceAsStream("previews/" + snippet.snippetName + ".png")) { + if (imageStream != null) { + return new Image(display, imageStream); + } + } + return null; + } + + /** + * Load all available Snippets from the preconfigured source path and from the + * current classppath. + * + * @return all found Snippets (never <code>null</code>) + */ + private static List<Snippet> loadSnippets() { + // Similar to SnippetLauncher this explorer tries to load Snippet0 to Snippet500 + // even if no sources are available. This array is used to track which snippets + // are already loaded from source. + final boolean[] loadedSnippets = new boolean[501]; + final List<Snippet> snippets = new ArrayList<>(); + + // load snippets from source directory + final Path sourceDir = SnippetsConfig.SNIPPETS_SOURCE_DIR.toPath(); + if (Files.exists(sourceDir)) { + try (DirectoryStream<Path> files = Files.newDirectoryStream(sourceDir, "*.java")) { + for (Path file : files) { + try { + final Snippet snippet = snippetFromSource(file); + if (snippet == null) { + continue; + } + snippets.add(snippet); + if (snippet.snippetNum >= 0) { + loadedSnippets[snippet.snippetNum] = true; + } + } catch (ClassNotFoundException | IOException ex) { + System.err.println("Failed to load snippet from " + file + ". Error: " + ex); + } + } + } catch (IOException ex) { + System.err.println("Failed to access source directory " + sourceDir + ". Error: " + ex); + } + } + + // load snippets from classpath + for (int i = 0; i < loadedSnippets.length; i++) { + if (!loadedSnippets[i]) { + final int snippetNum = i; + final String snippetName = "Snippet" + snippetNum; + final Class<?> snippetClass; + try { + snippetClass = Class.forName(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippetName, false, + SnippetExplorer.class.getClassLoader()); + } catch (ClassNotFoundException e) { + continue; + } + final String[] arguments = SnippetsConfig.getSnippetArguments(snippetNum); + snippets.add(new Snippet(snippetNum, snippetName, snippetClass, null, null, arguments)); + } + } + + return snippets; + } + + /** + * Load Snippet metadata from the Java source file found at the given path. + * + * @param sourceFile the source file to load + * @return the gathered Snippet metadata or <code>null</code> if failed + * @throws IOException on errors loading the source file + * @throws ClassNotFoundException if loading the Snippets corresponding class + * file failed + */ + private static Snippet snippetFromSource(Path sourceFile) throws IOException, ClassNotFoundException { + final Pattern snippetNamePattern = Pattern.compile("Snippet([0-9]+)", Pattern.CASE_INSENSITIVE); + sourceFile = sourceFile.normalize(); + final String filename = sourceFile.getFileName().toString(); + final String snippetName = filename.substring(0, filename.lastIndexOf('.')); + final Class<?> snippeClass = Class.forName(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippetName, false, + SnippetExplorer.class.getClassLoader()); + int snippetNum = Integer.MIN_VALUE; + final Matcher snippetNameMatcher = snippetNamePattern.matcher(snippetName); + if (snippetNameMatcher.matches()) { + try { + snippetNum = Integer.parseInt(snippetNameMatcher.group(1), 10); + } catch (NumberFormatException e) { + } + } + + // do not load snippets without number yet + if (snippetNum < 0) { + return null; + } + + final String src = getSnippetSource(sourceFile); + final String description = extractSnippetDescription(src); + final String[] arguments = SnippetsConfig.getSnippetArguments(snippetNum); + return new Snippet(snippetNum, snippetName, snippeClass, src, description, arguments); + } + + /** + * Read the content of the source file. (expect <code>UTF-8</code> encoding) + * + * @param sourceFile source file to load + * @return the files content or <code>null</code> if file does not exist + * @throws IOException if loading failed + */ + private static String getSnippetSource(Path sourceFile) throws IOException { + if (!Files.exists(sourceFile)) { + return null; + } + final String src = new String(Files.readAllBytes(sourceFile), StandardCharsets.UTF_8); + return src; + } + + /** + * Tries to extract a snippet description from the snippet source. + * <p> + * If description has multiple lines the delimiter is always in UNIX-style (\n). + * </p> + * + * @param snippetSrc the snippet source code + * @return the extracted snippet description. If none found returns + * <code>null</code> or in some cases an empty string. + */ + private static String extractSnippetDescription(String snippetSrc) { + if (snippetSrc == null) { + return null; + } + // Usually the second block comment contains a description of the snippet + // therefore this method returns the first block comment not containing the + // usual copyright keywords. + // Note: currently only real block comments are considered. A bunch of line + // comments forming a block (like that comment you're reading right now) are + // ignored. + + final Pattern blockCommentPattern = Pattern.compile("/\\*\\*?(.*?)\\*/", Pattern.DOTALL); + final Matcher blockCommentMatcher = blockCommentPattern.matcher(snippetSrc); + while (blockCommentMatcher.find()) { + String comment = blockCommentMatcher.group(1); + if (comment.contains("Copyright (c)") || comment.contains("https://www.eclipse.org/legal/epl-2.0/")) { + continue; + } + // normalize line breaks + comment = comment.replaceAll("\r\n?", "\n"); + // remove '*' at line start and trim lines + comment = comment.replaceAll("[ \t]*\n[ \\t]*\\*+[ \\t]*", "\n"); + // trim start and end + comment = comment.trim(); + return comment; + } + return null; + } + + private void updateTable(String filter) { + if (filter == null) { + filter = ""; + } + filter = filter.toLowerCase(); + int itemIndex = 0; + final int itemCount = snippetTable.getItemCount(); + snippetTable.setRedraw(false); + snippetTable.deselectAll(); + for (Snippet snippet : snippets) { + if (filter.isEmpty() || (snippet.description != null && snippet.description.toLowerCase().contains(filter)) + || String.valueOf(snippet.snippetNum).equals(filter)) { + final TableItem item = itemIndex < itemCount ? snippetTable.getItem(itemIndex) + : new TableItem(snippetTable, SWT.NONE); + fillTableItem(item, snippet); + itemIndex++; + } + } + if (itemIndex < itemCount) { + snippetTable.remove(itemIndex, itemCount - 1); + } + snippetTable.setRedraw(true); + } + + /** + * Initialize the table item with information from the Snippet. + * + * @param item table item to initialize (not <code>null</code>) + * @param snippet source Snippet (not <code>null</code>) + */ + private void fillTableItem(TableItem item, Snippet snippet) { + item.setData(snippet); + + final String shortDescription; + if (snippet.description == null) { + shortDescription = ""; + } else { + int index = snippet.description.indexOf('\n'); + if (index < 0) { + index = snippet.description.length(); + } + if (index > MAX_DESCRIPTION_LENGTH_IN_TABLE) { + shortDescription = snippet.description.substring(0, MAX_DESCRIPTION_LENGTH_IN_TABLE) + "..."; + } else { + shortDescription = snippet.description.substring(0, index); + } + } + + item.setText(new String[] { snippet.snippetName, shortDescription }); + } + + /** + * Process UI event queue until explorer is closed or otherwise ended. + */ + private void runEventLoop() { + // Apart from the usual "dispatch events until closed" pattern the + // SnippetExplorer supports the special workflow where it close itself, run one + // or more Snippets one after another and then restarts the explorer itself + // which is all handled in this method. + try { + while (true) { + serialSnippets = null; + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + if (serialSnippets == null || serialSnippets.isEmpty()) { + break; + } + + display.dispose(); + int i = 0; + for (Snippet snippet : serialSnippets) { + System.out.println(String.format("(%d/%d) %s", ++i, serialSnippets.size(), snippet.snippetName)); + runSnippetInCurrentThread(snippet); + } + final Display currentDisplay = Display.getCurrent(); + if (currentDisplay != null) { + // left over from the snippet run + currentDisplay.dispose(); + } + initialize(); + final int index = runnerMapping.indexOf(null); + if (index != -1) { + runnerCombo.select(index); + } + } + } finally { + stopSnippets(); + } + } + + /** Try to stop all running Snippets. */ + private synchronized void stopSnippets() { + for (SnippetRunner runner : runnerMapping) { + if (runner != null) { + runner.stopSnippets(); + } + } + } + + /** + * Launch the given snippet items with the currently selected snippet runner. + * <p> + * The items must contain the {@link Snippet} metadata as data object. + * </p> + * + * @param items the Snippets to launch + * @see #snippetRunner + */ + private void launchSnippet(TableItem... items) { + final List<Snippet> validSnippets = new ArrayList<>(); + for (TableItem item : items) { + if (item != null && item.getData() instanceof Snippet) { + validSnippets.add((Snippet) item.getData()); + } + } + + if (validSnippets.size() > START_MANY_SNIPPETS_WARNING_THREASHOLD) { + final MessageBox warnBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.YES | SWT.NO); + warnBox.setText("Starting many Snippets"); + warnBox.setMessage("You have selected " + validSnippets.size() + " Snippets to start.\n" + + "Do you really want to start so many Snippets at once?"); + if (warnBox.open() != SWT.YES) { + return; + } + } + + if (snippetRunner != null) { + snippetRunner.launchSnippet(validSnippets.toArray(new Snippet[0])); + } else { + nextExplorerLocation = shell.getLocation(); + serialSnippets = validSnippets; + shell.close(); + } + } + + /** + * Launches the given Snippet in the current thread by invoking the Snippets + * <code>main</code> method. + * + * @param snippet the Snippet to run (not <code>null</code>) + */ + private static void runSnippetInCurrentThread(Snippet snippet) { + final Method method; + final String[] arguments = snippet.arguments; + try { + method = snippet.snippetClass.getMethod("main", arguments.getClass()); + } catch (NoSuchMethodException ex) { + System.err.println("Did not find main(String []) for " + snippet.snippetName); + return; + } + try { + method.invoke(null, new Object[] { arguments }); + } catch (IllegalAccessException | IllegalArgumentException e) { + System.err.println("Failed to launch " + snippet.snippetName + ". Error: " + e); + } catch (InvocationTargetException e) { + System.err.println("Exception in Snippet " + snippet.snippetName + ": " + e.getTargetException()); + } + } + + /** + * Show a warning dialog that the Snippet may print with the default printer + * without further warnings. + * + * @param shell parent shell for the warning dialog + * @param snippetName the Snippet's name to warn for + * @return <code>true</code> if the user confirmed Snippet execution + */ + private static boolean printerWarning(Shell shell, String snippetName) { + final MessageBox warnBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK | SWT.CANCEL); + warnBox.setText("Printing Snippet"); + warnBox.setMessage( + snippetName + " may print something on your default printer without further warning or confirmation."); + return (warnBox.open() == SWT.OK); + } + + /** Class to store metadata for a Snippet. */ + private static class Snippet { + /** The Snippet's number. (not all Snippets may have numbers in the future) */ + private int snippetNum; + /** Snippet's name / main class name. */ + private String snippetName; + /** Snippet's main class. */ + private Class<?> snippetClass; + /** Snippet's source code or <code>null</code> if not available. */ + private String source; + /** + * Snippet description extracted from its source code. (may be + * <code>null</code>) + */ + private String description; + /** + * Arguments used when launching the Snippets. Can be configured in + * {@link SnippetsConfig#getSnippetArguments(int)}. + */ + private String[] arguments; + + public Snippet(int snippetNum, String snippetName, Class<?> snippetClass, String source, String description, + String[] arguments) { + super(); + this.snippetNum = snippetNum; + this.snippetName = snippetName; + this.snippetClass = snippetClass; + this.source = source; + this.description = description; + this.arguments = arguments; + } + + @Override + public String toString() { + return "Snippet [snippetNum=" + snippetNum + ", snippetName=" + snippetName + ", snippetClass=" + + snippetClass + ", source=" + source + ", description=" + description + ", arguments=" + + Arrays.toString(arguments) + "]"; + } + } + + /** Interface for a runner capable to launch Snippets. */ + private interface SnippetRunner { + /** + * Launch the given Snippets in the runner specific way. + * + * @param snippets Snippets to launch. Not <code>null</code>. + */ + void launchSnippet(Snippet... snippets); + + /** + * Stop all running Snippets launched with this runner. Some runners may not be + * able to stop Snippets. + */ + void stopSnippets(); + } + + /** Run Snippets in separate threads. */ + private class SnippetRunnerThread implements SnippetRunner { + + /** All currently <b>running</b> Snippets launched from this runner. */ + private final List<Thread> launchedSnippets = new ArrayList<>(); + + /** + * Launch Snippets parallel in separate threads. Call returns immediately after + * all Snippets are started. + */ + @Override + public void launchSnippet(Snippet... snippets) { + for (Snippet snippet : snippets) { + if (snippet == null) { + return; + } + final Thread thread = new Thread(() -> { + try { + synchronized (launchedSnippets) { + launchedSnippets.add(Thread.currentThread()); + } + + // warn user before printing + if (SnippetsConfig.isPrintingSnippet(snippet.snippetNum)) { + final Display d = new Display(); + try { + if (!printerWarning(new Shell(d), snippet.snippetName)) { + return; + } + } finally { + d.dispose(); + } + } + + runSnippetInCurrentThread(snippet); + + } finally { + synchronized (launchedSnippets) { + final Display d = Display.getCurrent(); + if (d != null) { + d.dispose(); + } + launchedSnippets.remove(Thread.currentThread()); + } + } + }); + thread.setDaemon(true); + thread.setName(snippet.snippetName); + thread.start(); + } + } + + /** + * Stops all running Snippets launched by this runner. If a Snippt refuses to + * react to this stop signal it will not be force stopped until the + * SnippetExplorer itself is closed. + */ + @Override + public void stopSnippets() { + final List<Thread> runningSnippets; + synchronized (launchedSnippets) { + runningSnippets = new ArrayList<>(launchedSnippets); + } + for (Thread t : runningSnippets) { + t.interrupt(); + final Display d = Display.findDisplay(t); + if (d != null) { + d.asyncExec( + () -> Arrays.stream(d.getShells()).filter(s -> !s.isDisposed()).forEach(s -> s.close())); + } + } + final long start = System.currentTimeMillis(); + for (Thread t : runningSnippets) { + if (System.currentTimeMillis() - SHUTDOWN_GRACE_TIME_MS > start) { + break; + } + try { + t.join(200); + } catch (InterruptedException e) { + } + } + synchronized (launchedSnippets) { + if (!launchedSnippets.isEmpty()) { + System.err.println("Some Snippets are still running:"); + for (Thread t : launchedSnippets) { + System.err.println(" " + t.getName() + " (ThreadId: " + t.getId() + ")"); + final Display d = Display.findDisplay(t); + if (d != null && !d.isDisposed()) { + d.syncExec(() -> d.dispose()); + } + } + } + } + } + } + + /** Run Snippets in separate processes. */ + private class SnippetRunnerProcess implements SnippetRunner { + /** + * All Snippets launched from this runner. Listed Snippets may already + * terminated. + */ + private List<Process> launchedSnippets = new ArrayList<>(); + + /** + * Launch Snippets parallel as separate processes using the auto discovered JRE. + * Call returns immediately after all Snippets are started. + */ + @Override + public synchronized void launchSnippet(Snippet... snippets) { + for (Snippet snippet : snippets) { + if (snippet == null) { + continue; + } + + // warn user before printing + if (SnippetsConfig.isPrintingSnippet(snippet.snippetNum)) { + if (!printerWarning(shell, snippet.snippetName)) { + continue; + } + } + + final List<String> command = new ArrayList<>(); + command.add(javaCommand); + final String os = System.getProperty("os.name"); + if (os != null && os.toLowerCase().contains("mac")) { + command.add("-XstartOnFirstThread"); + } + final String cp = System.getProperty("java.class.path"); + if (cp != null && !cp.isEmpty()) { + command.add("-cp"); + command.add(cp); + } + final String libPath = System.getProperty("java.library.path"); + if (libPath != null && !libPath.isEmpty()) { + command.add("-Djava.library.path=" + libPath); + } + command.add(SnippetsConfig.SNIPPETS_PACKAGE + "." + snippet.snippetName); + command.addAll(Arrays.asList(snippet.arguments)); + try { + System.out.println("Exec: " + String.join(" ", command)); + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.redirectOutput(Redirect.INHERIT); + processBuilder.redirectError(Redirect.INHERIT); + final Process p = processBuilder.start(); + launchedSnippets.add(p); + } catch (IOException e) { + System.err.println("Failed to launch " + snippet.snippetName + ". Error: " + e); + } + } + } + + /** + * Stops all running Snippets launched by this runner. If the stop signal was + * send but the Snippet is still running after a short grace time the runner + * tries to stop the Snippet forcefully. + * <p> + * If all attempts to stop the Snippet fail then the Snippet will run even after + * the SnippetExplorer was closed. + * </p> + */ + @Override + public synchronized void stopSnippets() { + for (Process p : launchedSnippets) { + p.destroy(); + } + + final long start = System.currentTimeMillis(); + while (!launchedSnippets.isEmpty() && System.currentTimeMillis() - SHUTDOWN_GRACE_TIME_MS < start) { + final Iterator<Process> it = launchedSnippets.iterator(); + while (it.hasNext()) { + final Process p = it.next(); + if (!p.isAlive()) { + it.remove(); + } + } + if (!launchedSnippets.isEmpty()) { + try { + launchedSnippets.get(0).waitFor(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + break; + } + } + } + + if (!launchedSnippets.isEmpty()) { + System.err.println(launchedSnippets.size() + " Snippets are still running."); + for (Process p : launchedSnippets) { + p.destroyForcibly(); + } + final Iterator<Process> it = launchedSnippets.iterator(); + while (it.hasNext()) { + final Process p = it.next(); + if (!p.isAlive()) { + it.remove(); + } + } + } + } + } + + /** + * Update thread used to delay the list filtering due to changed filter string. + */ + private class ListUpdater extends Thread { + /** + * The timestamp in milliseconds since epoch when the next update should be + * executed. + */ + private long nextListUpdate = 0; + + public ListUpdater() { + setName("List Updater"); + setDaemon(true); + } + + /** + * Reapply the table filter in X milliseconds. + * <p> + * If an update is already scheduled only the latest update time will be used. + * </p> + * + * @param ms sleep time before updating the main table + */ + public synchronized void updateInMs(long ms) { + if (ms < 0) { + return; + } + final long nextUpdate = System.currentTimeMillis() + ms; + if (nextListUpdate < nextUpdate) { + nextListUpdate = nextUpdate; + } + notify(); + } + + @Override + public void run() { + while (!isInterrupted()) { + final long nextUpdate; + synchronized (this) { + nextUpdate = nextListUpdate; + } + if (nextUpdate - System.currentTimeMillis() <= 0) { + if (filterField != null) { + display.syncExec(() -> updateTable(filterField.getText())); + } + synchronized (this) { + if (nextUpdate == nextListUpdate) { + // no new update was scheduled while updating + nextListUpdate = 0; + } + } + } + synchronized (this) { + long sleepTime = nextListUpdate; + if (sleepTime != 0) { + sleepTime -= System.currentTimeMillis(); + if (sleepTime <= 0) { + sleepTime = 1; + } + } + try { + wait(sleepTime); + } catch (InterruptedException e) { + break; + } + } + } + } + } +} diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetLauncher.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetLauncher.java index 6d56afe960..5c49658464 100644 --- a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetLauncher.java +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetLauncher.java @@ -13,7 +13,6 @@ *******************************************************************************/ package org.eclipse.swt.snippets; -import java.io.*; /* * Simple "hackable" code that runs all of the SWT Snippets, * typically for testing. One example of a useful "hack" is @@ -23,6 +22,7 @@ import java.io.*; * String source = String.valueOf(buffer); * in order to run all of the Table and Tree Snippets. */ +import java.io.*; import java.lang.reflect.*; import org.eclipse.swt.*; @@ -30,7 +30,7 @@ import org.eclipse.swt.*; public class SnippetLauncher { public static void main (String [] args) { - File sourceDir = new File("src/org/eclipse/swt/snippets"); + File sourceDir = SnippetsConfig.SNIPPETS_SOURCE_DIR; boolean hasSource = sourceDir.exists(); int count = 500; if (hasSource) { @@ -38,11 +38,11 @@ public class SnippetLauncher { if (files.length > 0) count = files.length; } for (int i = 1; i < count; i++) { - if (i == 132 || i == 133 || i == 318) continue; // avoid printing to printer + if (SnippetsConfig.isPrintingSnippet(i)) continue; // avoid printing to printer String className = "Snippet" + i; Class<?> clazz = null; try { - clazz = Class.forName("org.eclipse.swt.snippets." + className); + clazz = Class.forName(SnippetsConfig.SNIPPETS_PACKAGE + "." + className); } catch (ClassNotFoundException e) {} if (clazz != null) { System.out.println("\n" + clazz.getName()); @@ -85,8 +85,7 @@ public class SnippetLauncher { } } Method method = null; - String [] param = new String [0]; - if (i == 81) param = new String[] {"Shell.Explorer"}; + String [] param = SnippetsConfig.getSnippetArguments(i); try { method = clazz.getMethod("main", param.getClass()); } catch (NoSuchMethodException e) { diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetsConfig.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetsConfig.java new file mode 100644 index 0000000000..16a25cb43a --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/SnippetsConfig.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2019 Paul Pazderski 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: + * Paul Pazderski - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.snippets; + +import java.io.*; + +/** + * Used to store some metadata, argument or configuration stuff for snippets. + * Most of this was hardcoded in {@link SnippetLauncher} in the past. + */ +public class SnippetsConfig { + + public static final File SNIPPETS_SOURCE_DIR = new File("src/org/eclipse/swt/snippets"); + + public static final String SNIPPETS_PACKAGE = "org.eclipse.swt.snippets"; + + public static boolean isPrintingSnippet(int snippetNumber) { + return snippetNumber == 132 || snippetNumber == 133 || snippetNumber == 318; + } + + public static String[] getSnippetArguments(int snippetNumber) { + switch (snippetNumber) { + case 81: + return new String[] { "Shell.Explorer" }; + + default: + return new String[0]; + } + } +} |