| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.shell.log; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.TreeLogger.Type; |
| import com.google.gwt.dev.BootStrapPlatform; |
| import com.google.gwt.dev.shell.CloseButton; |
| import com.google.gwt.dev.shell.CloseButton.Callback; |
| import com.google.gwt.dev.shell.WrapLayout; |
| import com.google.gwt.dev.shell.log.SwingTreeLogger.LogEvent; |
| import com.google.gwt.dev.util.BrowserLauncher; |
| import com.google.gwt.dev.util.log.AbstractTreeLogger; |
| import com.google.gwt.dev.util.log.CompositeTreeLogger; |
| import com.google.gwt.dev.util.log.PrintWriterTreeLogger; |
| |
| import java.awt.BorderLayout; |
| import java.awt.Color; |
| import java.awt.Component; |
| import java.awt.Dimension; |
| import java.awt.FlowLayout; |
| import java.awt.Font; |
| import java.awt.HeadlessException; |
| import java.awt.Point; |
| import java.awt.datatransfer.Clipboard; |
| import java.awt.datatransfer.StringSelection; |
| import java.awt.event.ActionEvent; |
| import java.awt.event.ActionListener; |
| import java.awt.event.InputEvent; |
| import java.awt.event.KeyEvent; |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| |
| import javax.swing.AbstractAction; |
| import javax.swing.BorderFactory; |
| import javax.swing.JButton; |
| import javax.swing.JCheckBox; |
| import javax.swing.JComboBox; |
| import javax.swing.JComponent; |
| import javax.swing.JEditorPane; |
| import javax.swing.JLabel; |
| import javax.swing.JOptionPane; |
| import javax.swing.JPanel; |
| import javax.swing.JScrollPane; |
| import javax.swing.JSplitPane; |
| import javax.swing.JTextField; |
| import javax.swing.JTree; |
| import javax.swing.KeyStroke; |
| import javax.swing.Popup; |
| import javax.swing.PopupFactory; |
| import javax.swing.UIManager; |
| import javax.swing.event.HyperlinkEvent; |
| import javax.swing.event.HyperlinkEvent.EventType; |
| import javax.swing.event.HyperlinkListener; |
| import javax.swing.event.TreeModelEvent; |
| import javax.swing.event.TreeModelListener; |
| import javax.swing.event.TreeSelectionEvent; |
| import javax.swing.event.TreeSelectionListener; |
| import javax.swing.text.html.HTMLDocument; |
| import javax.swing.tree.DefaultMutableTreeNode; |
| import javax.swing.tree.DefaultTreeCellRenderer; |
| import javax.swing.tree.DefaultTreeModel; |
| import javax.swing.tree.TreeNode; |
| import javax.swing.tree.TreePath; |
| import javax.swing.tree.TreeSelectionModel; |
| |
| /** |
| * Swing widget containing a tree logger. |
| * |
| * <p> |
| * This class should not be serialized. |
| * </p> |
| */ |
| public class SwingLoggerPanel extends JPanel implements TreeSelectionListener, |
| HyperlinkListener { |
| |
| /** |
| * Callback interface for optional close button behavior. |
| */ |
| public interface CloseHandler { |
| |
| /** |
| * Called when the close button has been clicked on the tree logger |
| * and any confirmation needed has been handled. |
| * |
| * @param loggerPanel SwingTreeLogger instance being closed |
| */ |
| void onCloseRequest(SwingLoggerPanel loggerPanel); |
| } |
| |
| private class FindBox extends JPanel { |
| |
| private Popup findPopup; |
| private String lastSearch; |
| |
| private ArrayList<DefaultMutableTreeNode> matches; |
| |
| private int matchNumber; |
| private JTextField searchField; |
| private JLabel searchStatus; |
| |
| public FindBox() { |
| super(new BorderLayout()); |
| JPanel top = new JPanel(new FlowLayout()); |
| searchField = new JTextField(20); |
| top.add(searchField); |
| JButton nextButton = new JButton("+"); |
| top.add(nextButton); |
| nextButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| nextMatch(); |
| } |
| }); |
| JButton prevButton = new JButton("-"); |
| top.add(prevButton); |
| prevButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| prevMatch(); |
| } |
| }); |
| CloseButton closeButton = new CloseButton("Close this search box"); |
| closeButton.setCallback(new Callback() { |
| @Override |
| public void onCloseRequest() { |
| hideFindBox(); |
| } |
| }); |
| top.add(closeButton); |
| KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); |
| getInputMap(WHEN_IN_FOCUSED_WINDOW).put(key, "find-cancel"); |
| key = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); |
| getInputMap(WHEN_IN_FOCUSED_WINDOW).put(key, "find-search"); |
| getActionMap().put("find-search", new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| lastSearch = searchField.getText(); |
| matches = doFind(lastSearch); |
| matchNumber = 0; |
| updateSearchResult(); |
| } |
| }); |
| AbstractAction closeFindBox = new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| hideFindBox(); |
| } |
| }; |
| getActionMap().put("find-cancel", closeFindBox); |
| add(top, BorderLayout.NORTH); |
| searchStatus = new JLabel("Type search text and press Enter"); |
| searchStatus.setBorder(BorderFactory.createEmptyBorder(0, 2, 2, 0)); |
| add(searchStatus, BorderLayout.SOUTH); |
| } |
| |
| public void hideBox() { |
| if (findPopup != null) { |
| findPopup.hide(); |
| findPopup = null; |
| } |
| } |
| |
| public void nextMatch() { |
| if (matches != null && matches.size() > 0) { |
| matchNumber = (matchNumber + 1) % matches.size(); |
| updateSearchResult(); |
| } |
| } |
| |
| public void prevMatch() { |
| if (matches != null) { |
| int n = matches.size(); |
| if (n > 0) { |
| matchNumber = (matchNumber + n - 1) % n; |
| updateSearchResult(); |
| } |
| } |
| } |
| |
| public void showBox() { |
| Point loggerOrigin = details.getLocationOnScreen(); |
| Dimension dim = details.getSize(); |
| if (findPopup != null) { |
| findPopup.hide(); |
| } |
| // have to display once to get the correct size |
| int width = findBox.getWidth(); |
| boolean needsRelocate = (width <= 0); |
| int x = loggerOrigin.x + dim.width - width; |
| int y = loggerOrigin.y + dim.height - findBox.getHeight(); |
| PopupFactory popupFactory = PopupFactory.getSharedInstance(); |
| // TODO(jat): need to track window resize? |
| findPopup = popupFactory.getPopup(SwingLoggerPanel.this, findBox, x, y); |
| findPopup.show(); |
| if (needsRelocate) { |
| x = loggerOrigin.x + dim.width - findBox.getWidth(); |
| y = loggerOrigin.y + dim.height - findBox.getHeight(); |
| findPopup.hide(); |
| findPopup = popupFactory.getPopup(SwingLoggerPanel.this, findBox, x, y); |
| findPopup.show(); |
| } |
| searchField.requestFocusInWindow(); |
| } |
| |
| private void updateSearchResult() { |
| int n = matches.size(); |
| if (n == 0) { |
| searchStatus.setText("No matches"); |
| } else { |
| searchStatus.setText(String.valueOf(matchNumber + 1) + " of " |
| + n + " matches"); |
| showFindResult(matches.get(matchNumber), lastSearch); |
| } |
| } |
| } |
| |
| private static class TreeRenderer extends DefaultTreeCellRenderer { |
| @Override |
| public Component getTreeCellRendererComponent(JTree tree, Object value, |
| boolean sel, boolean expanded, boolean leaf, int row, |
| boolean componentHasFocus) { |
| super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, |
| componentHasFocus); |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; |
| Object userObject = node.getUserObject(); |
| if (userObject instanceof LogEvent) { |
| LogEvent event = (LogEvent) userObject; |
| event.setDisplayProperties(this); |
| } |
| return this; |
| } |
| } |
| |
| /** |
| * The mask to use for Ctrl -- mapped to Command on Mac. |
| */ |
| private static int ctrlKeyDown; |
| |
| private static final Color DISCONNECTED_COLOR = Color.decode("0xFFDDDD"); |
| |
| static { |
| ctrlKeyDown = BootStrapPlatform.isMac() ? InputEvent.ALT_DOWN_MASK |
| : InputEvent.CTRL_DOWN_MASK; |
| } |
| |
| // package protected for SwingTreeLogger to access |
| Type levelFilter; |
| |
| String regexFilter; |
| |
| private final JTree tree; |
| |
| DefaultTreeModel treeModel; |
| |
| private CloseHandler closeHandler; |
| |
| private CloseButton closeLogger; |
| |
| private final JEditorPane details; |
| |
| private boolean disconnected = false; |
| |
| private FindBox findBox; |
| |
| private JComboBox levelComboBox; |
| |
| private final TreeLogger logger; |
| |
| private JTextField regexField; |
| |
| private DefaultMutableTreeNode root; |
| |
| private JPanel topPanel; |
| |
| private JScrollPane treeView; |
| |
| private JCheckBox autoScroll; |
| |
| /** |
| * Create a Swing-based logger panel, with a tree section and a detail |
| * section. |
| * |
| * @param maxLevel |
| * @param logFile |
| */ |
| public SwingLoggerPanel(TreeLogger.Type maxLevel, File logFile) { |
| super(new BorderLayout()); |
| regexFilter = ""; |
| levelFilter = maxLevel; |
| // TODO(jat): how to make the topPanel properly layout items |
| // when the window is resized |
| topPanel = new JPanel(new BorderLayout()); |
| JPanel logButtons = new JPanel(new WrapLayout()); |
| JButton expandButton = new JButton("Expand All"); |
| expandButton.setMnemonic(KeyEvent.VK_E); |
| expandButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| expandAll(); |
| } |
| }); |
| logButtons.add(expandButton); |
| JButton collapseButton = new JButton("Collapse All"); |
| collapseButton.setMnemonic(KeyEvent.VK_O); |
| collapseButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| collapseAll(); |
| } |
| }); |
| logButtons.add(collapseButton); |
| autoScroll = new JCheckBox("Auto-scroll", true); |
| autoScroll.setMnemonic(KeyEvent.VK_U); |
| logButtons.add(autoScroll); |
| topPanel.add(logButtons, BorderLayout.CENTER); |
| // TODO(jat): temporarily avoid showing parts that aren't implemented. |
| if (false) { |
| logButtons.add(new JLabel("Filter Log Messages: ")); |
| levelComboBox = new JComboBox(); |
| for (TreeLogger.Type type : TreeLogger.Type.instances()) { |
| if (type.compareTo(maxLevel) > 0) { |
| break; |
| } |
| levelComboBox.addItem(type); |
| } |
| levelComboBox.setEditable(false); |
| levelComboBox.setSelectedIndex(levelComboBox.getItemCount() - 1); |
| topPanel.add(levelComboBox); |
| levelComboBox.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| setLevelFilter((TreeLogger.Type) levelComboBox.getSelectedItem()); |
| } |
| }); |
| regexField = new JTextField(20); |
| logButtons.add(regexField); |
| JButton applyRegexButton = new JButton("Apply Regex"); |
| applyRegexButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| setRegexFilter(regexField.getText()); |
| } |
| }); |
| logButtons.add(applyRegexButton); |
| JButton clearRegexButton = new JButton("Clear Regex"); |
| clearRegexButton.addActionListener(new ActionListener() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| regexField.setText(""); |
| setRegexFilter(""); |
| } |
| }); |
| logButtons.add(clearRegexButton); |
| } |
| closeLogger = new CloseButton("Close this log window"); |
| closeLogger.setCallback(new Callback() { |
| // TODO(jat): add support for closing active session when SWT is removed |
| @Override |
| public void onCloseRequest() { |
| if (disconnected && closeHandler != null) { |
| closeHandler.onCloseRequest(SwingLoggerPanel.this); |
| } |
| } |
| }); |
| closeLogger.setEnabled(false); |
| closeLogger.setVisible(false); |
| topPanel.add(closeLogger, BorderLayout.EAST); |
| add(topPanel, BorderLayout.NORTH); |
| root = new DefaultMutableTreeNode(); |
| treeModel = new DefaultTreeModel(root); |
| treeModel.addTreeModelListener(new TreeModelListener() { |
| @Override public void treeNodesInserted(TreeModelEvent e) { |
| for (Object treeNode : e.getChildren()) { |
| onTreeNodeAdded((DefaultMutableTreeNode) treeNode); |
| } |
| } |
| @Override public void treeStructureChanged(TreeModelEvent e) { } |
| @Override public void treeNodesRemoved(TreeModelEvent e) { } |
| @Override public void treeNodesChanged(TreeModelEvent e) { } |
| }); |
| tree = new JTree(treeModel); |
| tree.setRootVisible(false); |
| tree.setEditable(false); |
| tree.setExpandsSelectedPaths(true); |
| tree.setShowsRootHandles(true); |
| tree.setCellRenderer(new TreeRenderer()); |
| tree.getSelectionModel().setSelectionMode( |
| TreeSelectionModel.SINGLE_TREE_SELECTION); |
| tree.addTreeSelectionListener(this); |
| treeView = new JScrollPane(tree); |
| // TODO(jat): better way to do this |
| details = new JEditorPane() { |
| @Override |
| public boolean getScrollableTracksViewportWidth() { |
| return true; |
| } |
| }; |
| details.setEditable(false); |
| details.setContentType("text/html"); |
| details.setForeground(Color.BLACK); |
| details.addHyperlinkListener(this); |
| // font trick from http://explodingpixels.wordpress.com/2008/10/28/make-jeditorpane-use-the-system-font/ |
| Font font = UIManager.getFont("Label.font"); |
| String bodyRule = "body { font-family: " + font.getFamily() + "; " |
| + "font-size: " + font.getSize() + "pt; }"; |
| ((HTMLDocument) details.getDocument()).getStyleSheet().addRule(bodyRule); |
| JScrollPane msgView = new JScrollPane(details); |
| JSplitPane splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT); |
| splitter.setTopComponent(treeView); |
| splitter.setBottomComponent(msgView); |
| Dimension minSize = new Dimension(100, 50); |
| msgView.setMinimumSize(minSize); |
| treeView.setMinimumSize(minSize); |
| splitter.setDividerLocation(0.80); |
| add(splitter); |
| |
| AbstractTreeLogger uiLogger = new SwingTreeLogger(this); |
| uiLogger.setMaxDetail(maxLevel); |
| TreeLogger bestLogger = uiLogger; |
| if (logFile != null) { |
| try { |
| PrintWriterTreeLogger fileLogger = new PrintWriterTreeLogger(logFile); |
| fileLogger.setMaxDetail(maxLevel); |
| bestLogger = new CompositeTreeLogger(bestLogger, fileLogger); |
| } catch (IOException ex) { |
| bestLogger.log(TreeLogger.ERROR, "Can't log to file " |
| + logFile.getAbsolutePath(), ex); |
| } |
| } |
| logger = bestLogger; |
| KeyStroke key = getCommandKeyStroke(KeyEvent.VK_F, false); |
| getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "find"); |
| getActionMap().put("find", new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| showFindBox(); |
| } |
| }); |
| key = getCommandKeyStroke(KeyEvent.VK_C, false); |
| tree.getInputMap().put(key, "copy"); |
| tree.getActionMap().put("copy", new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| treeCopy(); |
| } |
| }); |
| findBox = new FindBox(); |
| key = getCommandKeyStroke(KeyEvent.VK_G, false); |
| tree.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "findnext"); |
| tree.getActionMap().put("findnext", new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| findBox.nextMatch(); |
| } |
| }); |
| key = getCommandKeyStroke(KeyEvent.VK_G, true); |
| tree.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "findprev"); |
| tree.getActionMap().put("findprev", new AbstractAction() { |
| @Override |
| public void actionPerformed(ActionEvent e) { |
| findBox.prevMatch(); |
| } |
| }); |
| } |
| |
| /** |
| * Collapse all tree nodes. |
| */ |
| @SuppressWarnings("unchecked") |
| public void collapseAll() { |
| Enumeration<TreeNode> children = root.postorderEnumeration(); |
| while (children.hasMoreElements()) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) children.nextElement(); |
| if (node != root) { |
| tree.collapsePath(new TreePath(node.getPath())); |
| } |
| } |
| tree.invalidate(); |
| } |
| |
| /** |
| * Show that the client connected to this logger has disconnected. |
| */ |
| public void disconnected() { |
| disconnected = true; |
| tree.setBackground(DISCONNECTED_COLOR); |
| tree.repaint(); |
| } |
| |
| /** |
| * Expand all tree nodes. |
| */ |
| @SuppressWarnings("unchecked") |
| public void expandAll() { |
| Enumeration<TreeNode> children = root.postorderEnumeration(); |
| while (children.hasMoreElements()) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) children.nextElement(); |
| if (node != root) { |
| tree.expandPath(new TreePath(node.getPath())); |
| } |
| } |
| tree.invalidate(); |
| } |
| |
| /** |
| * @return the TreeLogger for this panel |
| */ |
| public TreeLogger getLogger() { |
| return logger; |
| } |
| |
| @Override |
| public void hyperlinkUpdate(HyperlinkEvent event) { |
| EventType eventType = event.getEventType(); |
| if (eventType == HyperlinkEvent.EventType.ACTIVATED) { |
| URL url = event.getURL(); |
| try { |
| BrowserLauncher.browse(url.toExternalForm()); |
| return; |
| } catch (Exception e) { |
| // if anything fails, fall-through to failsafe implementation |
| } |
| // As a last resort, just use the details pane to display the HTML, but |
| // this is rather poor. |
| try { |
| details.setPage(url); |
| } catch (IOException e) { |
| logger.log(TreeLogger.ERROR, "Unable to follow link to " + url, e); |
| } |
| } |
| } |
| |
| @Override |
| public void removeAll() { |
| tree.removeAll(); |
| details.setText(""); |
| } |
| |
| /** |
| * Sets a callback for handling a close request, which also makes the close |
| * button visible. |
| * |
| * @param handler |
| */ |
| public void setCloseHandler(CloseHandler handler) { |
| closeHandler = handler; |
| closeLogger.setEnabled(true); |
| closeLogger.setVisible(true); |
| } |
| |
| @Override |
| public void valueChanged(TreeSelectionEvent e) { |
| if (e.isAddedPath()) { |
| TreePath path = e.getPath(); |
| Object treeNode = path.getLastPathComponent(); |
| if (treeNode == null) { |
| // handle the case of no selection |
| details.setText(""); |
| return; |
| } |
| Object userObject = ((DefaultMutableTreeNode) treeNode).getUserObject(); |
| String text = userObject.toString(); |
| if (userObject instanceof LogEvent) { |
| LogEvent event = (LogEvent) userObject; |
| text = event.getFullText(); |
| } |
| details.setText(text); |
| } |
| } |
| |
| protected void alert(String msg) { |
| JOptionPane.showMessageDialog(null, msg, "Alert: Not Implemented", |
| JOptionPane.INFORMATION_MESSAGE); |
| } |
| |
| /** |
| * Ask the user for confirmation to close the current logger. |
| * |
| * @return true if the user confirmed the request |
| */ |
| protected boolean confirmClose() { |
| int response = JOptionPane.showConfirmDialog(null, |
| "Close the logger for the currently displayed module", |
| "Close this Logger", JOptionPane.OK_CANCEL_OPTION, |
| JOptionPane.WARNING_MESSAGE); |
| return response != JOptionPane.YES_OPTION; |
| } |
| |
| protected ArrayList<DefaultMutableTreeNode> doFind(String search) { |
| @SuppressWarnings("unchecked") |
| Enumeration<TreeNode> children = root.preorderEnumeration(); |
| ArrayList<DefaultMutableTreeNode> matches = new ArrayList<DefaultMutableTreeNode>(); |
| while (children.hasMoreElements()) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) children.nextElement(); |
| if (node != root && nodeMatches(node, search)) { |
| matches.add(node); |
| // Make sure our this entry is visible by expanding up to parent |
| TreeNode[] nodePath = node.getPath(); |
| if (nodePath.length > 1) { |
| TreeNode[] parentPath = new TreeNode[nodePath.length - 1]; |
| System.arraycopy(nodePath, 0, parentPath, 0, parentPath.length); |
| tree.expandPath(new TreePath(parentPath)); |
| } |
| } |
| } |
| tree.invalidate(); |
| return matches; |
| } |
| |
| protected void hideFindBox() { |
| findBox.hideBox(); |
| } |
| |
| protected void setLevelFilter(Type selectedLevel) { |
| levelFilter = selectedLevel; |
| // TODO(jat): filter current tree |
| alert("Filtering not implemented yet"); |
| } |
| |
| protected void setRegexFilter(String regex) { |
| regexFilter = regex; |
| // TODO(jat): filter current tree |
| alert("Regex filtering not implemented yet"); |
| } |
| |
| protected void showFindBox() { |
| findBox.showBox(); |
| } |
| |
| protected void treeCopy() { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode) |
| tree.getLastSelectedPathComponent(); |
| if (node == null) { |
| return; |
| } |
| // is it better to use SwingUtilities2.canAccessSystemClipboard() here? |
| Clipboard clipboard; |
| try { |
| clipboard = tree.getToolkit().getSystemClipboard(); |
| } catch (SecurityException e) { |
| return; |
| } catch (HeadlessException e) { |
| return; |
| } |
| if (clipboard == null) { |
| return; |
| } |
| StringBuilder text = new StringBuilder(); |
| treeLogTraverse(text, node, 0); |
| StringSelection selection = new StringSelection(text.toString()); |
| clipboard.setContents(selection, selection); |
| } |
| |
| /** |
| * Returns a keystroke which adds the appropriate modifier for a command key: |
| * Command on mac, Ctrl everywhere else. |
| * |
| * @param key virtual key defined in {@code KeyEvent#VK_*} |
| * @param shift true if the Ctrl/Command key must be shifted |
| * @return KeyStroke of the Ctrl/Command-key |
| */ |
| private KeyStroke getCommandKeyStroke(int key, boolean shift) { |
| int mask = ctrlKeyDown; |
| if (shift) { |
| mask |= InputEvent.SHIFT_DOWN_MASK; |
| } |
| return KeyStroke.getKeyStroke(key, mask); |
| } |
| |
| private String htmlUnescape(String str) { |
| // TODO(jat): real implementation, needs to correspond to |
| // SwingTreeLogger.htmlEscape() |
| return str.replace("<", "<").replace(">", ">").replace("&", |
| "&").replace("<br>", "\n"); |
| } |
| |
| private boolean nodeMatches(DefaultMutableTreeNode node, String search) { |
| Object userObject = node.getUserObject(); |
| if (userObject instanceof LogEvent) { |
| LogEvent event = (LogEvent) userObject; |
| String text = htmlUnescape(event.getFullText()); |
| // TODO(jat): should this be more than a substring match, such as regex? |
| if (text.contains(search)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @param search the search string, currently ignored. |
| */ |
| private void showFindResult(DefaultMutableTreeNode node, String search) { |
| // TODO(jat): highlight search string |
| TreePath path = new TreePath(node.getPath()); |
| tree.scrollPathToVisible(path); |
| tree.setSelectionPath(path); |
| } |
| |
| private void treeLogTraverse(StringBuilder buf, TreeNode node, |
| int indent) { |
| for (int i = 0; i < indent; ++i) { |
| buf.append(' '); |
| } |
| if (node instanceof DefaultMutableTreeNode) { |
| DefaultMutableTreeNode mutableNode = (DefaultMutableTreeNode) node; |
| Object userObject = mutableNode.getUserObject(); |
| if (userObject instanceof LogEvent) { |
| LogEvent event = (LogEvent) userObject; |
| buf.append(htmlUnescape(event.getFullText())); |
| if (event.isBranchCommit) { |
| SwingTreeLogger childLogger = event.childLogger; |
| DefaultMutableTreeNode parent = childLogger.treeNode; |
| for (int i = 0; i < parent.getChildCount(); ++i) { |
| treeLogTraverse(buf, parent.getChildAt(i), indent + 2); |
| } |
| } |
| } else { |
| buf.append(userObject.toString()); |
| buf.append('\n'); |
| } |
| } else { |
| buf.append(node.toString()); |
| buf.append('\n'); |
| } |
| } |
| |
| private void onTreeNodeAdded(DefaultMutableTreeNode treeNode) { |
| TreePath path = new TreePath(treeNode.getPath()); |
| if (autoScroll.isSelected()) { |
| tree.scrollPathToVisible(path); // internally will also call makeVisible |
| } else { |
| Object userObject = treeNode.getUserObject(); |
| if (userObject instanceof LogEvent) { |
| if (((LogEvent) userObject).type.needsAttention()) { |
| tree.makeVisible(path); |
| } |
| } |
| } |
| } |
| } |