/* * Copyright 2007-2008 Sun Microsystems, Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of Sun Microsystems nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.sun.swingset3.codeview; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BoxLayout; import javax.swing.DefaultComboBoxModel; import javax.swing.ImageIcon; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.KeyStroke; import javax.swing.ListCellRenderer; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.UIManager; import javax.swing.border.EmptyBorder; import javax.swing.text.BadLocationException; import javax.swing.text.Highlighter; import org.jdesktop.swingx.JXEditorPane; import org.jdesktop.swingx.JXPanel; import org.jdesktop.swingx.util.PaintUtils; import org.jdesktop.swingxset.util.DemoUtils; import com.sun.swingset3.utilities.RoundedBorder; import com.sun.swingset3.utilities.Utilities; /** * GUI component for viewing a set of one or more Java source code files, * providing the user with the ability to easily highlight specific code fragments. * A tabbedpane is used to control which source code file is shown within the set * if more than one file is loaded. * <p> * Example usage: * <pre><code> * CodeViewer codeViewer = new CodeViewer(); * codeViewer.setSourceFiles(mySourceURLs); * frame.add(codeViewer); * </code></pre> * * <p> * When loading the source code, this viewer will automatically parse the files for * any source fragments which are marked with "snippet" start/end tags that * are embedded within Java comments. The viewer will allow the user to highlight * these code snippets for easier inspection of specific code. * <p> * The text following immediately after the start tag will be used * as the key for that snippet. Multiple snippets may share the same * key, defining a "snippet set". Snippet sets may even span across * multiple source files. * The key for each snippet set is displayed in a combobox to allow the user to * select which snippet set should be highlighted. For example:<p> * <pre><code> * //<snip>Create dog array * ArrayList dogs = new ArrayList(); * //</snip> * * [other code...] * * //<snip>Create dog array * dogs.add("Labrador"); * dogs.add("Golden Retriever"); * dogs.add("Australian Shepherd"); * //</snip> * </code></pre> * The above code would create a snippet set (containing 2 snippets) with the key * "Create dog array". * <p> * The viewer will allow the user to easily navigate across the currently highlighted * snippet set by pressing the navigation buttons or using accelerator keys. * * @author aim */ public class CodeViewer extends JPanel { public static final String SOURCES_JAVA = ".+\\.java"; public static final String SOURCES_TEXT = ".+\\.properties|.+\\.txt|.+\\.html|.+\\.xml"; public static final String SOURCES_IMAGES = ".+\\.jpg|.+\\.gif|.+\\.png"; private static final Color DEFAULT_HIGHLIGHT_COLOR = new Color(255,255,176); private static BufferedImage SNIPPET_GLYPH; private static String NO_SNIPPET_SELECTED; static final Logger logger = Logger.getLogger(CodeViewer.class.getName()); static { try { URL imageURL = CodeViewer.class.getResource("resources/images/snippetglyph.png"); SNIPPET_GLYPH = ImageIO.read(imageURL); } catch (Exception e) { System.err.println(e); } } // Cache all processed code files in case they are reloaded later private final Map<URL,CodeFileInfo> codeCache = new HashMap<URL,CodeFileInfo>(); private JComponent codeHighlightBar; private JComboBox snippetComboBox; private JComponent codePanel; private JLabel noCodeLabel; private JTabbedPane codeTabbedPane; private Color highlightColor; private Highlighter.HighlightPainter snippetPainter; private ResourceBundle bundle; // Current code file set private Map<URL, CodeFileInfo> currentCodeFilesInfo; private List<URL> additionalSourceFiles; // Map of all snippets in current code file set private final SnippetMap snippetMap = new SnippetMap(); private Action firstSnippetAction; private Action nextSnippetAction; private Action previousSnippetAction; private Action lastSnippetAction; /** * Creates a new instance of CodeViewer */ public CodeViewer() { setHighlightColor(DEFAULT_HIGHLIGHT_COLOR); initActions(); setLayout(new BorderLayout()); codeHighlightBar = createCodeHighlightBar(); codeHighlightBar.setVisible(false); add(codeHighlightBar, BorderLayout.NORTH); codePanel = createCodePanel(); add(codePanel, BorderLayout.CENTER); applyDefaults(); } protected JComponent createCodeHighlightBar() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); JPanel bar = new JPanel(gridbag); bar.setBorder(new EmptyBorder(0, 0, 10, 0)); NO_SNIPPET_SELECTED = getString("CodeViewer.snippets.selectOne", "Select One"); JLabel snippetSetsLabel = new JLabel(getString("CodeViewer.snippets.highlightCode", "Highlight code to: ")); c.gridx = 0; c.gridy = 0; c.anchor = GridBagConstraints.WEST; c.weightx = 0; gridbag.addLayoutComponent(snippetSetsLabel, c); bar.add(snippetSetsLabel); snippetComboBox = new JComboBox(); snippetComboBox.setMaximumRowCount(20); snippetComboBox.setRenderer(new SnippetCellRenderer(snippetComboBox.getRenderer())); snippetComboBox.addActionListener(new SnippetActivator()); snippetSetsLabel.setLabelFor(snippetComboBox); c.gridx++; c.weightx = 1; gridbag.addLayoutComponent(snippetComboBox, c); bar.add(snippetComboBox); SnippetNavigator snippetNavigator = new SnippetNavigator(snippetMap); snippetNavigator.setNavigateNextAction(nextSnippetAction); snippetNavigator.setNavigatePreviousAction(previousSnippetAction); c.gridx++; c.anchor = GridBagConstraints.EAST; c.weightx = 0; gridbag.addLayoutComponent(snippetNavigator, c); bar.add(snippetNavigator); return bar; } protected JComponent createCodePanel() { JPanel panel = new JXPanel(new BorderLayout()); panel.setBorder(new RoundedBorder(10)); noCodeLabel = new JLabel(getString("CodeViewer.noCodeLoaded", "no code loaded")); noCodeLabel.setHorizontalAlignment(JLabel.CENTER); panel.add(noCodeLabel, BorderLayout.CENTER); return panel; } @Override public void updateUI() { super.updateUI(); applyDefaults(); } protected void applyDefaults() { if (noCodeLabel != null) { noCodeLabel.setOpaque(false); noCodeLabel.setFont(UIManager.getFont("Label.font").deriveFont(24f)); noCodeLabel.setForeground( PaintUtils.setAlpha(UIManager.getColor("Label.foreground"), 110)); } if (codePanel != null) { Color base = UIManager.getColor("Panel.background"); codePanel.setBackground(Utilities.deriveColorHSB(base, 0, 0, -.06f)); } if (snippetComboBox != null) { // Now that the look and feel has changed, we need to wrap the new delegate snippetComboBox.setRenderer(new SnippetCellRenderer( new JComboBox().getRenderer())); } if (currentCodeFilesInfo != null) { Collection<CodeFileInfo> codeFiles = currentCodeFilesInfo.values(); for(CodeFileInfo cfi : codeFiles) { makeSelectionTransparent(cfi.textPane, 180); } } } private void makeSelectionTransparent(JEditorPane textPane, int alpha) { Color c = textPane.getSelectionColor(); textPane.setSelectionColor( new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha)); } protected String getString(String key, String fallback) { String value = fallback; if (bundle == null) { String bundleName = getClass().getPackage().getName()+".resources."+getClass().getSimpleName(); bundle = ResourceBundle.getBundle(bundleName); } try { value = bundle != null? bundle.getString(key) : fallback; } catch (MissingResourceException e) { logger.log(Level.WARNING, "missing String resource " + key + "; using fallback \"" +fallback + "\""); } return value; } protected void initActions() { firstSnippetAction = new FirstSnippetAction(); nextSnippetAction = new NextSnippetAction(); previousSnippetAction = new PreviousSnippetAction(); lastSnippetAction = new LastSnippetAction(); firstSnippetAction.setEnabled(false); nextSnippetAction.setEnabled(false); previousSnippetAction.setEnabled(false); lastSnippetAction.setEnabled(false); getActionMap().put("NextSnippet", nextSnippetAction); getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl N"),"NextSnippet"); getActionMap().put("PreviousSnippet", previousSnippetAction); getInputMap(WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl P"),"PreviousSnippet"); } public void setHighlightColor(Color highlight) { if (!highlight.equals(highlightColor)) { highlightColor = highlight; snippetPainter = new SnippetHighlighter.SnippetHighlightPainter(highlightColor); if (getCurrentSnippetKey() != null) { repaint(); } } } public Color getHighlightColor() { return highlightColor; } public void setSourceFiles(URL sourceFiles[]) { if (currentCodeFilesInfo != null && additionalSourceFiles != null && sourceFiles != null && currentCodeFilesInfo.size() + additionalSourceFiles.size() == sourceFiles.length) { List<URL> list = Arrays.asList(sourceFiles); if (list.containsAll(currentCodeFilesInfo.keySet()) && list.containsAll(additionalSourceFiles)) { // already loaded return; } } // clear everything clearAllSnippetHighlights(); snippetMap.clear(); if (sourceFiles == null) { // being reset to having no source files; need to clear everything currentCodeFilesInfo = null; additionalSourceFiles = null; configureCodePane(false); configureSnippetSetsComboBox(); } else { // Use LinkedHashMap to save source order currentCodeFilesInfo = new LinkedHashMap<URL, CodeFileInfo>(); additionalSourceFiles = new ArrayList<URL>(); boolean needProcessing = false; for (URL sourceFile : sourceFiles) { if (sourceFile.getFile().matches(SOURCES_JAVA)) { // look in cache first to avoid unnecessary processing CodeFileInfo cachedFilesInfo = codeCache.get(sourceFile); currentCodeFilesInfo.put(sourceFile, cachedFilesInfo); if (cachedFilesInfo == null) { needProcessing = true; } } else { additionalSourceFiles.add(sourceFile); } } configureCodePane(true); if (needProcessing) { // Do it on a separate thread new SourceProcessor(currentCodeFilesInfo).execute(); } else { for (CodeFileInfo codeFileInfo : currentCodeFilesInfo.values()) { registerSnippets(codeFileInfo); createCodeFileTab(codeFileInfo); } createAdditionalTabs(); configureSnippetSetsComboBox(); } } } private void createAdditionalTabs() { JPanel pnImages = null; for (URL sourceFile : additionalSourceFiles) { String sourcePath = sourceFile.getPath(); int i = sourcePath.indexOf('!'); if (i >= 0) { sourcePath = sourcePath.substring(i + 1); } if (sourceFile.getFile().matches(SOURCES_IMAGES)) { if (pnImages == null) { pnImages = new JPanel(); pnImages.setLayout(new BoxLayout(pnImages, BoxLayout.Y_AXIS)); } JLabel label = new JLabel(); label.setIcon(new ImageIcon(sourceFile)); label.setBorder(new EmptyBorder(10, 0, 40, 0)); pnImages.add(new JLabel(sourcePath)); pnImages.add(label); } if (sourceFile.getFile().matches(SOURCES_TEXT)) { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(sourceFile.openStream())); StringBuilder content = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { content.append(line).append('\n'); } JTextArea textArea = new JTextArea(content.toString()); Font font = textArea.getFont(); textArea.setEditable(false); textArea.setFont(new Font("Monospaced", font.getStyle(), font.getSize())); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setBorder(null); codeTabbedPane.addTab(Utilities.getURLFileName(sourceFile), scrollPane); } catch (IOException e) { System.err.println(e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { System.err.println(e); } } } } } if (pnImages != null) { JScrollPane scrollPane = new JScrollPane(pnImages); scrollPane.setBorder(null); codeTabbedPane.addTab(getString("CodeViewer.images", "Images"), scrollPane); } } private class SourceProcessor extends SwingWorker<Void, CodeFileInfo> { private final Map<URL, CodeFileInfo> codeFilesInfo; public SourceProcessor(Map<URL, CodeFileInfo> codeFilesInfo) { this.codeFilesInfo = codeFilesInfo; } @Override public Void doInBackground() { for (Map.Entry<URL, CodeFileInfo> entry : codeFilesInfo.entrySet()) { // if not already fetched from cache, then process source code if (entry.getValue() == null) { entry.setValue(initializeCodeFileInfo(entry.getKey())); } // We don't publish intermediate to avoid tab mixing codeCache.put(entry.getKey(), entry.getValue()); } return null; } private CodeFileInfo initializeCodeFileInfo(URL sourceFile) { CodeFileInfo codeFileInfo = new CodeFileInfo(); codeFileInfo.url = sourceFile; codeFileInfo.styled = loadSourceCode(sourceFile); // PENDING JW: implement better unitIncrement - should be one line // (that is depend on fontmetrics) codeFileInfo.textPane = new JXEditorPane(); codeFileInfo.textPane.setHighlighter(new SnippetHighlighter()); makeSelectionTransparent(codeFileInfo.textPane, 180); codeFileInfo.veneer = new CodeVeneer(codeFileInfo); Stacker layers = new Stacker(codeFileInfo.textPane); layers.add(codeFileInfo.veneer, JLayeredPane.POPUP_LAYER); codeFileInfo.textPane.setContentType("text/html"); codeFileInfo.textPane.setEditable(false); // HTML won't display correctly without this! codeFileInfo.textPane.setText(codeFileInfo.styled); codeFileInfo.textPane.setCaretPosition(0); DemoUtils.replaceCopyAction(codeFileInfo.textPane); // MUST parse AFTER textPane Document has been created to ensure // snippet offsets are relative to the editor pane's Document model codeFileInfo.snippets = SnippetParser.parse(codeFileInfo.textPane.getDocument()); return codeFileInfo; } @Override protected void done() { try { // It's possible that by now another set of source files has been loaded. // so check first before adding the source tab;' if (currentCodeFilesInfo == codeFilesInfo) { for (CodeFileInfo codeFileInfo : currentCodeFilesInfo.values()) { registerSnippets(codeFileInfo); createCodeFileTab(codeFileInfo); } } else { logger.log(Level.FINEST, "source files changed before sources was processed."); } createAdditionalTabs(); configureSnippetSetsComboBox(); } catch (Exception ex) { System.err.println(ex); } } } // SourceProcessor // Called from Source Processing Thread in SwingWorker private void configureCodePane(boolean hasCodeFiles) { if (hasCodeFiles) { if (codeTabbedPane == null) { codeTabbedPane = new JTabbedPane(); codePanel.remove(noCodeLabel); codePanel.add(codeTabbedPane); revalidate(); } else { codeTabbedPane.removeAll(); } } else { // No code files if (codeTabbedPane != null) { codePanel.remove(codeTabbedPane); codeTabbedPane = null; codePanel.add(noCodeLabel); revalidate(); } } } private void createCodeFileTab(CodeFileInfo codeFileInfo) { JLayeredPane layeredPane = JLayeredPane.getLayeredPaneAbove(codeFileInfo.textPane); JScrollPane scrollPane = new JScrollPane(layeredPane); scrollPane.setBorder(null); JPanel tabPanel = new JPanel(); tabPanel.setLayout(new BorderLayout()); tabPanel.add(scrollPane, BorderLayout.CENTER); codeTabbedPane.addTab(Utilities.getURLFileName(codeFileInfo.url), tabPanel); } private void registerSnippets(CodeFileInfo codeFileInfo) { for(String snippetKey: codeFileInfo.snippets.keySet()) { List<Snippet> snippetCodeList = codeFileInfo.snippets.get(snippetKey); for(Snippet snippet: snippetCodeList) { snippetMap.add(snippetKey, codeFileInfo.url, snippet); } } } private void configureSnippetSetsComboBox() { TreeSet sortedSnippets = new TreeSet(snippetMap.keySet()); String snippetSetKeys[] = (String[])sortedSnippets.toArray(new String[0]); DefaultComboBoxModel snippetModel = new DefaultComboBoxModel(); for(String snippetKey : snippetSetKeys) { snippetModel.addElement(snippetKey); } snippetModel.insertElementAt(NO_SNIPPET_SELECTED, 0); snippetModel.setSelectedItem(NO_SNIPPET_SELECTED); snippetComboBox.setModel(snippetModel); codeHighlightBar.setVisible(snippetModel.getSize() > 1); } /** * Reads the java source file at the specified URL and returns an * HTML version stylized for display */ protected String loadSourceCode(URL sourceUrl) { InputStreamReader isr = null; CodeStyler cv = new CodeStyler(); String styledCode = "<html><body bgcolor=\"#ffffff\"><pre>"; try { isr = new InputStreamReader(sourceUrl.openStream(), "UTF-8"); BufferedReader reader = new BufferedReader(isr); // Read one line at a time, htmlizing using super-spiffy // html java code formating utility from www.CoolServlets.com String line = reader.readLine(); while(line != null) { styledCode += cv.syntaxHighlight(line) + " \n "; line = reader.readLine(); } styledCode += "</pre></body></html>"; } catch (Exception ex) { ex.printStackTrace(); return "Could not load file from: " + sourceUrl; } finally { if (isr != null) { try { isr.close(); } catch (IOException e) { System.err.println(e); } } } return styledCode; } public void clearAllSnippetHighlights() { if (currentCodeFilesInfo != null) { snippetMap.setCurrentSet(null); for(CodeFileInfo code : currentCodeFilesInfo.values()) { if (code != null && code.textPane != null) { Highlighter highlighter = code.textPane.getHighlighter(); highlighter.removeAllHighlights(); code.textPane.repaint(); code.veneer.repaint(); } } } } public void highlightSnippetSet(String snippetKey) { clearAllSnippetHighlights(); snippetMap.setCurrentSet(snippetKey); URL files[] = snippetMap.getFilesForSet(snippetKey); CodeFileInfo firstCodeFileInfo = null; Snippet firstSnippet = null; for(URL file : files) { CodeFileInfo codeFileInfo = codeCache.get(file); Highlighter highlighter = codeFileInfo.textPane.getHighlighter(); // now add highlight for each snippet in this file associated // with the key Snippet snippets[] = snippetMap.getSnippetsForFile(snippetKey, file); if (firstCodeFileInfo == null) { firstCodeFileInfo = codeFileInfo; firstSnippet = snippets[0]; } for (Snippet snippet : snippets) { try { highlighter.addHighlight(snippet.startLine, snippet.endLine, snippetPainter ); codeFileInfo.veneer.repaint(); } catch (BadLocationException e) { e.printStackTrace(); } } } scrollToSnippet(firstCodeFileInfo, firstSnippet); snippetComboBox.setSelectedItem(snippetKey); } protected void scrollToSnippet(CodeFileInfo codeFileInfo, Snippet snippet) { if (!codeFileInfo.textPane.isShowing()) { // Need to switch tabs to source file with first snippet // remind: too brittle - need to find component some other way codeTabbedPane.setSelectedComponent( JLayeredPane.getLayeredPaneAbove(codeFileInfo.textPane).getParent().getParent().getParent()); } try { Rectangle r1 = codeFileInfo.textPane.modelToView(snippet.startLine); Rectangle r2 = codeFileInfo.textPane.modelToView(snippet.endLine); codeFileInfo.textPane.scrollRectToVisible( SwingUtilities.computeUnion(r1.x, r1.y, r1.width, r1.height, r2)); } catch (BadLocationException e) { System.err.println(e); } nextSnippetAction.setEnabled(snippetMap.nextSnippetExists()); previousSnippetAction.setEnabled(snippetMap.previousSnippetExists()); } protected String getCurrentSnippetKey() { String key = snippetMap.getCurrentSet(); return key != null? key : NO_SNIPPET_SELECTED; } protected Snippet getCurrentSnippet() { return snippetMap.getCurrentSnippet(); } protected void moveToFirstSnippet() { Snippet firstSnippet = snippetMap.firstSnippet(); if (firstSnippet != null) { CodeFileInfo codeFileInfo = codeCache.get(snippetMap.getFileForSnippet(firstSnippet)); scrollToSnippet(codeFileInfo, firstSnippet); } else { Toolkit.getDefaultToolkit().beep(); } } protected void moveToNextSnippet() { Snippet nextSnippet = snippetMap.nextSnippet(); if (nextSnippet != null) { CodeFileInfo codeFileInfo = codeCache.get(snippetMap.getFileForSnippet(nextSnippet)); scrollToSnippet(codeFileInfo, nextSnippet); } else { Toolkit.getDefaultToolkit().beep(); } } protected void moveToPreviousSnippet() { Snippet previousSnippet = snippetMap.previousSnippet(); if (previousSnippet != null) { CodeFileInfo codeFileInfo = codeCache.get(snippetMap.getFileForSnippet(previousSnippet)); scrollToSnippet(codeFileInfo, previousSnippet); } else { Toolkit.getDefaultToolkit().beep(); } } protected void moveToLastSnippet() { Snippet lastSnippet = snippetMap.lastSnippet(); if (lastSnippet != null) { CodeFileInfo codeFileInfo = codeCache.get(snippetMap.getFileForSnippet(lastSnippet)); scrollToSnippet(codeFileInfo, lastSnippet); } else { Toolkit.getDefaultToolkit().beep(); } } private class SnippetActivator implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String snippetKey = (String)snippetComboBox.getSelectedItem(); if (!snippetKey.equals(NO_SNIPPET_SELECTED)) { logger.log(Level.FINEST, "highlighting new snippet:"+snippetKey+"."); highlightSnippetSet(snippetKey); } else { clearAllSnippetHighlights(); } } } private abstract class SnippetAction extends AbstractAction { public SnippetAction(String name, String shortDescription) { super(name); putValue(AbstractAction.SHORT_DESCRIPTION, shortDescription); } } private class FirstSnippetAction extends SnippetAction { public FirstSnippetAction() { super("FirstSnippet", getString("CodeViewer.snippets.navigateFirst", "move to first code snippet within highlighted set")); } @Override public void actionPerformed(ActionEvent e) { moveToFirstSnippet(); } } private class NextSnippetAction extends SnippetAction { public NextSnippetAction() { super("NextSnippet", getString("CodeViewer.snippets.navigateNext", "move to next code snippet within highlighted set")); } @Override public void actionPerformed(ActionEvent e) { moveToNextSnippet(); } } private class PreviousSnippetAction extends SnippetAction { public PreviousSnippetAction() { super("PreviousSnippet", getString("CodeViewer.snippets.navigatePrevious", "move to previous code fragment within highlighted set")); } @Override public void actionPerformed(ActionEvent e) { moveToPreviousSnippet(); } } private class LastSnippetAction extends SnippetAction { public LastSnippetAction() { super("LastSnippet", getString("CodeViewer.snippets.navigateLast", "move to last code snippet within highlighted set")); } @Override public void actionPerformed(ActionEvent e) { moveToLastSnippet(); } } private class SnippetCellRenderer implements ListCellRenderer { private JLabel delegate; public SnippetCellRenderer(ListCellRenderer delegate) { this.delegate = (JLabel)delegate; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel renderer = (JLabel)((ListCellRenderer)delegate).getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); int count = snippetMap.getSnippetCountForSet((String)value); Color foreground = renderer.getForeground(); Color countForeground = Utilities.deriveColorHSB(foreground, 0, 0, isSelected? .5f : .4f); String text = "<html><font color=\"" + PaintUtils.toHexString(foreground) + "\">" + value + "</font>" + "<font color=\"" + PaintUtils.toHexString(countForeground) + "\">" + (count > 0? " (" + count + (count > 1? " snippets)" : " snippet)") : "") + "</font></html>"; renderer.setText(text); return renderer; } } private static class CodeFileInfo { public URL url; public String styled; public HashMap<String, List<Snippet>> snippets = new HashMap<String, List<Snippet>>(); public JEditorPane textPane; public JPanel veneer; } /** * Changed to implement Scrollable - which delegates to master if appropriate - * to fix Issue #36-demos, unit scroll increment far too small. */ private static class Stacker extends JLayeredPane implements Scrollable { private Component master; // dictates sizing, scrolling public Stacker(Component master) { this.master = master; setLayout(null); add(master, JLayeredPane.DEFAULT_LAYER); } @Override public Dimension getPreferredSize() { return master.getPreferredSize(); } @Override public void doLayout() { // ensure all layers are sized the same Dimension size = getSize(); Component layers[] = getComponents(); for(Component layer : layers) { layer.setBounds(0, 0, size.width, size.height); } } @Override public Dimension getPreferredScrollableViewportSize() { if (master instanceof Scrollable) { return ((Scrollable) master).getPreferredScrollableViewportSize(); } return getPreferredSize(); } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { if (master instanceof Scrollable) { return ((Scrollable) master).getScrollableBlockIncrement(visibleRect, orientation, direction); } return visibleRect.height; } @Override public boolean getScrollableTracksViewportHeight() { if (master instanceof Scrollable) { return ((Scrollable) master).getScrollableTracksViewportHeight(); } return false; } @Override public boolean getScrollableTracksViewportWidth() { if (master instanceof Scrollable) { return ((Scrollable) master).getScrollableTracksViewportWidth(); } return false; } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { if (master instanceof Scrollable) { return ((Scrollable) master).getScrollableUnitIncrement(visibleRect, orientation, direction); } return visibleRect.height / 10; } } private class CodeVeneer extends JPanel { private CodeFileInfo codeFileInfo; public CodeVeneer(CodeFileInfo codeFileInfo) { this.codeFileInfo = codeFileInfo; setOpaque(false); setLayout(null); } @Override protected void paintComponent(Graphics g) { String snippetKey = getCurrentSnippetKey(); if (snippetKey != NO_SNIPPET_SELECTED) { // Count total number of snippets for key int snippetTotal = 0; int snippetIndex = 0; List<Snippet> snippetList = null; URL files[] = snippetMap.getFilesForSet(snippetKey); for(URL file : files) { CodeFileInfo codeFileInfo = codeCache.get(file); if (this.codeFileInfo == codeFileInfo) { snippetList = codeFileInfo.snippets.get(snippetKey); snippetIndex = snippetTotal + 1; } snippetTotal += (codeFileInfo.snippets.get(snippetKey).size()); } if (snippetList != null) { Snippet currentSnippet = snippetMap.getCurrentSnippet(); CodeFileInfo currentSnippetCodeFileInfo = codeCache.get( snippetMap.getFileForSnippet(currentSnippet)); Font font = g.getFont(); g.setFont(font.deriveFont(10f)); FontMetrics metrics = g.getFontMetrics(); g.setColor(getHighlightColor()); Graphics2D g2Alpha = null; // cache composite for(Snippet snippet : snippetList) { Graphics2D g2 = (Graphics2D)g; try { if (currentSnippetCodeFileInfo != codeFileInfo || currentSnippet != snippet) { // if not painting the "current" snippet, then fade the glyph if (g2Alpha == null) { // first time, so create composite g2Alpha = (Graphics2D)g2.create(); g2Alpha.setComposite(AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.6f)); } g2 = g2Alpha; } Rectangle snipRect = codeFileInfo.textPane.modelToView(snippet.startLine); //String glyphLabel = snippetIndex++ + "/" + snippetTotal; String glyphLabel = "" + snippetIndex++; Rectangle labelRect = metrics.getStringBounds(glyphLabel, g2).getBounds(); g2.drawImage(SNIPPET_GLYPH, 0, snipRect.y, this); g2.setColor(Color.black); g2.drawString(glyphLabel, (SNIPPET_GLYPH.getWidth(this) - labelRect.width)/2, snipRect.y + (SNIPPET_GLYPH.getHeight(this) - labelRect.height)/2 + metrics.getAscent()); } catch (BadLocationException e) { System.err.println(e); } } } } } } }