| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * 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.devrel.cluestick.studioclient; |
| |
| import com.google.devrel.cluestick.searchservice.CluestickSearch; |
| import com.google.devrel.cluestick.searchservice.EventLog; |
| |
| import com.appspot.cluestick_server.search.model.Result; |
| import com.intellij.CommonBundle; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.ide.IdeBundle; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.ActionPlaces; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.actionSystem.DefaultActionGroup; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.DumbAware; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.Splitter; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.ui.DoubleClickListener; |
| import com.intellij.ui.OnePixelSplitter; |
| import com.intellij.ui.PopupHandler; |
| import com.intellij.ui.ScrollPaneFactory; |
| import com.intellij.ui.SmartExpander; |
| import com.intellij.util.ui.tree.TreeUtil; |
| |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.awt.BorderLayout; |
| import java.awt.Cursor; |
| import java.awt.Desktop; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.MouseEvent; |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.List; |
| |
| import javax.swing.JComponent; |
| import javax.swing.JPanel; |
| import javax.swing.event.TreeSelectionEvent; |
| import javax.swing.event.TreeSelectionListener; |
| import javax.swing.tree.TreeNode; |
| import javax.swing.tree.TreePath; |
| import javax.swing.tree.TreeSelectionModel; |
| |
| /** |
| * SearchResultsView extends JPanel to show the entire Cluestick sample widget. |
| */ |
| public class SearchResultsView extends JPanel implements Disposable { |
| |
| private static final Logger LOG = Logger |
| .getInstance("#com.google.devrel.cluestick.studioclient.SearchResultsView"); |
| |
| private final EventLog eventLog; |
| private final Splitter splitter; |
| private final SearchResultsTree tree; |
| private final Browser browser; |
| private final JComponent secondComponent; |
| private Runnable closeAction; |
| |
| public SearchResultsView(@NotNull final Project project, @NotNull final Symbol symbol, |
| @NotNull final List<Result> results) { |
| eventLog = new EventLog(ApplicationManager.getApplication().getService(CluestickSearch.class)); |
| |
| setLayout(new BorderLayout()); |
| |
| tree = new SearchResultsTree(symbol, results); |
| tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { |
| @Override |
| public void valueChanged(TreeSelectionEvent e) { |
| syncBrowser(); |
| } |
| }); |
| SmartExpander.installOn(tree); |
| browser = new CodeBrowser(project, eventLog); |
| |
| splitter = new OnePixelSplitter(false, 0.5f); |
| splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(tree)); |
| |
| secondComponent = ScrollPaneFactory.createScrollPane(browser.getPanel()); |
| splitter.setSecondComponent(secondComponent); |
| secondComponent.setVisible(false); // initially hide browser |
| |
| add(splitter, BorderLayout.CENTER); |
| |
| createActionsPanel(); |
| |
| // Add action listeners. These listeners don't do anything if the target node isn't a leaf, |
| // because trunk nodes respond by opening/closing on these same events. Users should use the |
| // popup menu to open any relevant URLs instead. |
| |
| new DoubleClickListener() { |
| @Override |
| protected boolean onDoubleClick(MouseEvent event) { |
| TreeNode node = getSingleSelectedNode(); |
| TreePath path = tree.getClosestPathForLocation(event.getX(), event.getY()); |
| if (node != null && node.isLeaf() && path != null && tree.isPathSelected(path)) { |
| invokeSelectAction(node); |
| return true; |
| } |
| return false; |
| } |
| }.installOn(tree); |
| |
| tree.addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| TreeNode node = getSingleSelectedNode(); |
| if (node != null && node.isLeaf() && e.getKeyCode() == KeyEvent.VK_ENTER) { |
| invokeSelectAction(node); |
| } |
| } |
| }); |
| |
| DefaultActionGroup actionGroup = new DefaultActionGroup(); |
| actionGroup.add(new OpenURLAction()); |
| PopupHandler.installPopupMenu(tree, actionGroup, ActionPlaces.USAGE_VIEW_POPUP); |
| |
| TreeUtil.expandAll(tree); |
| } |
| |
| /** |
| * @param close The code to run when the Close button is pressed. |
| */ |
| public void setClose(Runnable close) { |
| this.closeAction = close; |
| } |
| |
| private void invokeSelectAction(@Nullable TreeNode node) { |
| String url = getURLForNode(node); |
| if (url != null) { |
| openExternalBrowser(url); |
| } |
| } |
| |
| @Nullable private String getURLForNode(@Nullable TreeNode node) { |
| if (node instanceof SearchResultsNode) { |
| String url = ((SearchResultsNode) node).getURL(); |
| if (!StringUtil.isEmpty(url)) { |
| return url; |
| } |
| } |
| return null; |
| } |
| |
| private void openExternalBrowser(@NotNull String url) { |
| LOG.info(String.format("Opening URL: `%s`", url)); |
| |
| // In nearly all cases, this will load the user's browser with the target URL. However, |
| // swallow its exceptions for sanity. |
| try { |
| Desktop.getDesktop().browse(new URI(url)); |
| } catch (URISyntaxException ignored) { |
| // Thrown if the URL is bad. |
| } catch (IOException ignored) { |
| // Thrown if a browser can't be found or run. |
| } catch (RuntimeException ignored) { |
| // Thrown on a variety of other reasons, see- |
| // http://docs.oracle.com/javase/7/docs/api/java/awt/Desktop.html#browse(java.net.URI) |
| } |
| } |
| |
| /** |
| * Create and add a JPanel containing default actions. This just includes Close for now. |
| */ |
| private void createActionsPanel() { |
| DefaultActionGroup group = new DefaultActionGroup(); |
| group.add(new CloseAction()); |
| |
| ActionManager actionManager = ActionManager.getInstance(); |
| JComponent actionsToolbar = actionManager |
| .createActionToolbar(ActionPlaces.CODE_INSPECTION, group, false).getComponent(); |
| |
| JPanel actionsPanel = new JPanel(new BorderLayout()); |
| actionsPanel.add(actionsToolbar, BorderLayout.WEST); |
| add(actionsPanel, BorderLayout.WEST); |
| } |
| |
| @Override |
| public void dispose() { |
| splitter.dispose(); |
| eventLog.cancel(); |
| |
| if (browser instanceof Disposable) { |
| ((Disposable) browser).dispose(); |
| } |
| } |
| |
| private TreeNode getSingleSelectedNode() { |
| TreeSelectionModel selectionModel = tree.getSelectionModel(); |
| if (selectionModel.getSelectionCount() == 1) { |
| TreePath pathSelected = tree.getSelectionModel().getLeadSelectionPath(); |
| if (pathSelected != null) { |
| return (TreeNode) pathSelected.getLastPathComponent(); |
| } |
| } |
| return null; |
| } |
| |
| private void syncBrowser() { |
| boolean visible = false; |
| TreeNode node = getSingleSelectedNode(); |
| if (node instanceof SearchResultsNode) { |
| Result result = ((SearchResultsNode) node).getSearchResult(); |
| if (result != null && showInBrowser(result)) { |
| visible = true; |
| } |
| } |
| |
| if (!visible) { |
| browser.showEmpty(); |
| } |
| if (secondComponent.isVisible() != visible) { |
| secondComponent.setVisible(visible); |
| splitter.doLayout(); |
| splitter.doLayout(); // run twice, in case skipNextLayouting() was called on Splitter |
| } |
| } |
| |
| /** |
| * Shows the given {@link Result} inside the current {@link Browser}. |
| * @param result The result to show. |
| * @return Whether the result could be shown. |
| */ |
| private boolean showInBrowser(@NotNull Result result) { |
| // TODO(thorogood): Make this part of Browser interface. |
| if (result.getCode() == null) { |
| return false; |
| } |
| |
| Cursor currentCursor = getCursor(); |
| setCursor(new Cursor(Cursor.WAIT_CURSOR)); |
| browser.showResult(result); |
| setCursor(currentCursor); |
| return true; |
| } |
| |
| private class OpenURLAction extends AnAction { |
| private OpenURLAction() { |
| super(IdeBundle.message("open.url.in.browser.tooltip"), null, null); |
| } |
| |
| @Override |
| public void update(AnActionEvent e) { |
| String url = getURLForNode(getSingleSelectedNode()); |
| e.getPresentation().setVisible(url != null); |
| } |
| |
| @Override |
| public void actionPerformed(AnActionEvent e) { |
| invokeSelectAction(getSingleSelectedNode()); |
| } |
| } |
| |
| private class CloseAction extends AnAction implements DumbAware { |
| private CloseAction() { |
| super(CommonBundle.message("action.close"), null, AllIcons.Actions.Cancel); |
| } |
| |
| @Override |
| public void actionPerformed(AnActionEvent e) { |
| if (closeAction != null) { |
| closeAction.run(); |
| } |
| } |
| } |
| } |