/**
* Copyright 2014 Microsoft Open Technologies 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.microsoftopentechnologies.intellij.components;
import com.intellij.icons.AllIcons;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.openapi.wm.ex.ToolWindowEx;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree;
import com.microsoftopentechnologies.intellij.forms.ManageSubscriptionForm;
import com.microsoftopentechnologies.intellij.helpers.UIHelper;
import com.microsoftopentechnologies.intellij.helpers.collections.ListChangeListener;
import com.microsoftopentechnologies.intellij.helpers.collections.ListChangedEvent;
import com.microsoftopentechnologies.intellij.helpers.collections.ObservableList;
import com.microsoftopentechnologies.intellij.serviceexplorer.azure.AzureServiceModule;
import com.microsoftopentechnologies.intellij.serviceexplorer.Node;
import com.microsoftopentechnologies.intellij.serviceexplorer.NodeAction;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Collection;
import java.util.concurrent.ExecutionException;
public class ServerExplorerToolWindowFactory implements ToolWindowFactory, PropertyChangeListener {
private JTree tree;
private AzureServiceModule azureServiceModule;
private DefaultTreeModel treeModel;
@Override
public void createToolWindowContent(@NotNull final Project project, @NotNull final ToolWindow toolWindow) {
// initialize azure service module
azureServiceModule = new AzureServiceModule(project);
// initialize with all the service modules
treeModel = new DefaultTreeModel(initRoot());
// initialize tree
tree = new Tree(treeModel);
tree.setRootVisible(false);
tree.setCellRenderer(new NodeTreeCellRenderer());
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
// add a click handler for the tree
tree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
treeMousePressed(e);
}
});
// add the tree to the window
toolWindow.getComponent().add(new JBScrollPane(tree));
// setup toolbar icons
addToolbarItems(toolWindow);
}
private DefaultMutableTreeNode initRoot() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode();
// add the azure service root service module
root.add(createTreeNode(azureServiceModule));
// kick-off asynchronous load of child nodes on all the modules
azureServiceModule.load();
return root;
}
private void treeMousePressed(MouseEvent e) {
// get the tree node associated with this mouse click
TreePath treePath = tree.getPathForLocation(e.getX(), e.getY());
if(treePath == null)
return;
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)treePath.getLastPathComponent();
Node node = (Node)treeNode.getUserObject();
// delegate click to the node's click action if this is a left button click
if(SwingUtilities.isLeftMouseButton(e)) {
// if the node in question is in a "loading" state then we
// do not propagate the click event to it
if(!node.isLoading()) {
node.getClickAction().fireNodeActionEvent();
}
}
// for right click show the context menu populated with all the
// actions from the node
else if(SwingUtilities.isRightMouseButton(e) || e.isPopupTrigger()) {
if(node.hasNodeActions()) {
// select the node which was right-clicked
tree.getSelectionModel().setSelectionPath(treePath);
JPopupMenu menu = createPopupMenuForNode(node);
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
private JPopupMenu createPopupMenuForNode(Node node) {
JPopupMenu menu = new JPopupMenu();
for(final NodeAction nodeAction : node.getNodeActions()) {
JMenuItem menuItem = new JMenuItem(nodeAction.getName());
menuItem.setIconTextGap(16);
menuItem.setEnabled(nodeAction.isEnabled());
// delegate the menu item click to the node action's listeners
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
nodeAction.fireNodeActionEvent();
}
});
menu.add(menuItem);
}
return menu;
}
private DefaultMutableTreeNode createTreeNode(Node node) {
DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node, true);
// associate the DefaultMutableTreeNode with the Node via it's "viewData"
// property; this allows us to quickly retrieve the DefaultMutableTreeNode
// object associated with a Node
node.setViewData(treeNode);
// listen for property change events on the node
node.addPropertyChangeListener(this);
// listen for structure changes on the node, i.e. when child nodes are
// added or removed
node.getChildNodes().addChangeListener(new NodeListChangeListener(treeNode));
// create child tree nodes for each child node
if(node.hasChildNodes()) {
for (Node childNode : node.getChildNodes()) {
treeNode.add(createTreeNode(childNode));
}
}
return treeNode;
}
private void removeEventHandlers(Node node) {
node.removePropertyChangeListener(this);
ObservableList<Node> childNodes = node.getChildNodes();
childNodes.removeAllChangeListeners();
if(node.hasChildNodes()) {
// this remove call should cause the NodeListChangeListener object
// registered on it's child nodes to fire which should recursively
// clean up event handlers on it's children
node.removeAllChildNodes();
}
}
@Override
public void propertyChange(final PropertyChangeEvent evt) {
// if we are not running on the dispatch thread then switch
// to dispatch thread
if(!ApplicationManager.getApplication().isDispatchThread()) {
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
propertyChange(evt);
}
}, ModalityState.any());
return;
}
// this event is fired whenever a property on a node in the
// model changes; we respond by triggering a node change
// event in the tree's model
Node node = (Node)evt.getSource();
// the treeModel object can be null before it is initialized
// from createToolWindowContent; we ignore property change
// notifications till we have a valid model object
if(treeModel != null) {
treeModel.nodeChanged((TreeNode) node.getViewData());
}
}
private class NodeListChangeListener implements ListChangeListener {
private DefaultMutableTreeNode treeNode;
public NodeListChangeListener(DefaultMutableTreeNode treeNode) {
this.treeNode = treeNode;
}
@Override
public void listChanged(final ListChangedEvent e) {
// if we are not running on the dispatch thread then switch
// to dispatch thread
if(!ApplicationManager.getApplication().isDispatchThread()) {
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
listChanged(e);
}
}, ModalityState.any());
return;
}
switch (e.getAction()) {
case add:
// create child tree nodes for the new nodes
for(Node childNode : (Collection<Node>)e.getNewItems()) {
treeNode.add(createTreeNode(childNode));
}
break;
case remove:
// unregister all event handlers recursively and remove
// child nodes from the tree
for(Node childNode : (Collection<Node>)e.getOldItems()) {
removeEventHandlers(childNode);
// remove this node from the tree
treeNode.remove((MutableTreeNode) childNode.getViewData());
}
break;
}
treeModel.reload(treeNode);
}
}
private class NodeTreeCellRenderer extends NodeRenderer {
@Override
protected void doPaint(Graphics2D g) {
super.doPaint(g);
setOpaque(false);
}
@Override
public void customizeCellRenderer(JTree jTree,
Object value,
boolean selected,
boolean expanded,
boolean isLeaf,
int row,
boolean focused) {
super.customizeCellRenderer(tree, value, selected, expanded, isLeaf, row, focused);
// if the node has an icon set then we use that
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)value;
Node node = (Node)treeNode.getUserObject();
// "node" can be null if it's the root node which we keep hidden to simulate
// a multi-root tree control
if(node == null) {
return;
}
String iconPath = node.getIconPath();
if(iconPath != null && !iconPath.isEmpty()) {
setIcon(loadIcon(iconPath));
}
// setup a tooltip
setToolTipText(node.getName());
}
private ImageIcon loadIcon(String iconPath) {
URL url = NodeTreeCellRenderer.class.getResource("/com/microsoftopentechnologies/intellij/icons/" + iconPath);
return new ImageIcon(url);
}
}
private void addToolbarItems(ToolWindow toolWindow) {
if (toolWindow instanceof ToolWindowEx) {
ToolWindowEx toolWindowEx = (ToolWindowEx) toolWindow;
toolWindowEx.setTitleActions(
new AnAction("Refresh", "Refresh Service List", UIHelper.loadIcon("refresh.png")) {
@Override
public void actionPerformed(AnActionEvent event) {
azureServiceModule.load();
}
},
new AnAction("Manage Subscriptions", "Manage Subscriptions", AllIcons.Ide.Link) {
@Override
public void actionPerformed(AnActionEvent anActionEvent) {
ManageSubscriptionForm form = new ManageSubscriptionForm(anActionEvent.getProject());
UIHelper.packAndCenterJDialog(form);
form.setVisible(true);
azureServiceModule.load();
}
});
}
}
}