package net.bitpot.railways.gui;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.HyperlinkLabel;
import com.intellij.ui.JBSplitter;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.table.JBTable;
import net.bitpot.railways.actions.UpdateRoutesListAction;
import net.bitpot.railways.models.*;
import net.bitpot.railways.models.routes.SimpleRoute;
import net.bitpot.railways.parser.RailsRoutesParser;
import net.bitpot.railways.routesView.RoutesManager;
import net.bitpot.railways.routesView.RoutesView;
import net.bitpot.railways.routesView.RoutesViewPane;
import net.bitpot.railways.utils.RailwaysUtils;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
*
*/
public class MainPanel {
@SuppressWarnings("unused")
private static Logger log = Logger.getInstance(MainPanel.class.getName());
// Names of cards for main panel that contains several pages.
// Names should be the same as specified in GUI designer for appropriate panels.
private final static String ROUTES_CARD_NAME = "routesCard"; // Main page with routes table.
private final static String INFO_CARD_NAME = "infoCard"; // Panel with message/error information.
private final static String NO_INFO = "-";
private static final int LINK_OPEN_SETTINGS = 1;
private static final int LINK_SHOW_STACKTRACE = 2;
private RouteTableModel myTableModel;
private JPanel rootPanel;
private JBTable routesTable;
private JTextField pathFilterField;
private JPanel cardsPanel;
private HyperlinkLabel infoLink;
private JLabel infoLbl;
private JLabel routesCounterLbl;
private JPanel routeInfoPanel;
private HyperlinkLabel actionLbl;
private JLabel nameLbl;
private JLabel methodLbl;
private JLabel routeLbl;
private JPanel topPanel;
private JPanel actionsPanel;
private JBScrollPane routesScrollPane;
private JPanel mainRoutePanel;
private JBLabel environmentLbl;
private CardLayout cardLayout;
private boolean routesHidden = false;
private Project project;
// A pane that is used as a data source for the Routes panel.
private RoutesViewPane myDataSource = null;
// Contains route which information is shown in the info panel.
// Contains null if no route is selected.
private Route currentRoute;
private int infoLinkAction;
private JBSplitter mySplitter;
public MainPanel(Project project) {
this.project = project;
initToolbar();
// Init handlers after everything is initialized
initHandlers();
myTableModel = new RouteTableModel();
routesTable.setModel(myTableModel);
myTableModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
updateCounterLabel();
}
});
routesTable.setDefaultRenderer(Route.class,
new RouteCellRenderer(myTableModel.getFilter()));
routesTable.setDefaultRenderer(Object.class,
new FilterHighlightRenderer(myTableModel.getFilter()));
routesTable.setRowHeight(20);
cardLayout = (CardLayout) (cardsPanel.getLayout());
updateCounterLabel();
// Update route info panel
showRouteInfo(null);
initSplitter();
}
private void createUIComponents() {
routesTable = new RoutesTable();
nameLbl = new LabelWithCopy();
routeLbl = new LabelWithCopy();
((LabelWithCopy)routeLbl).setCopyFormatter(RailwaysUtils.STRIP_REQUEST_FORMAT);
}
/**
* Initializes splitter that divides routes table and info panel.
* We do this manually as there were difficulties with UI designer and
* the splitter.
*/
private void initSplitter() {
// Remove required components from main panel
mainRoutePanel.remove(routeInfoPanel);
mainRoutePanel.remove(routesScrollPane);
mySplitter = new JBSplitter(true, 0.8f);
mySplitter.setHonorComponentsMinimumSize(true);
mySplitter.setAndLoadSplitterProportionKey("Railways.SplitterProportion");
mySplitter.setOpaque(false);
mySplitter.setShowDividerControls(false);
mySplitter.setShowDividerIcon(false);
mySplitter.setFirstComponent(routesScrollPane);
mySplitter.setSecondComponent(routeInfoPanel);
mainRoutePanel.add(mySplitter, BorderLayout.CENTER);
}
public void setOrientation(boolean isVertical) {
mySplitter.setOrientation(isVertical);
}
/**
* Hides panel with route list and shows panel with information message.
*
* @param message Message to show.
*/
private void showMessagePanel(String message, @Nullable String envName) {
infoLbl.setText(message);
environmentLbl.setText("Environment: " + (envName == null ? "Default" : envName));
environmentLbl.setVisible(true);
infoLink.setVisible(false);
routesHidden = true;
updateCounterLabel();
setControlsEnabled(false);
cardLayout.show(cardsPanel, INFO_CARD_NAME);
}
/**
* Hides routes panel and shows panel with error message and with link that shows dialog with error details
*/
private void showErrorPanel(int parserError) {
switch (parserError) {
case RailsRoutesParser.ERROR_RAKE_TASK_NOT_FOUND:
RoutesManager.State settings = myDataSource.getRoutesManager().getState();
infoLbl.setText("Rake task '" + settings.routesTaskName + "' is not found.");
infoLink.setHyperlinkText("Configure");
infoLinkAction = LINK_OPEN_SETTINGS;
AnAction act = ActionManager.getInstance().getAction("Railways.settingsAction");
infoLink.setIcon(act.getTemplatePresentation().getIcon());
break;
default:
infoLbl.setText("Failed to load routes");
infoLink.setHyperlinkText("Show details");
infoLinkAction = LINK_SHOW_STACKTRACE;
infoLink.setIcon(null);
}
infoLink.setVisible(true);
environmentLbl.setVisible(false);
routesCounterLbl.setVisible(false);
routesHidden = true;
updateCounterLabel();
setControlsEnabled(false);
cardLayout.show(cardsPanel, INFO_CARD_NAME);
}
/**
* Show panel that contains routes table, hiding any other panel (information or error).
*/
private void showRoutesPanel() {
routesHidden = false;
updateCounterLabel();
setControlsEnabled(true);
routesCounterLbl.setVisible(true);
cardLayout.show(cardsPanel, ROUTES_CARD_NAME);
}
private void initHandlers() {
// When filter field text is changed, routes table will be refiltered.
pathFilterField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
myTableModel.getFilter().setPathFilter(pathFilterField.getText());
}
});
// Register mouse handler to handle double-clicks.
// Double clicking a row will navigate to the action of selected route.
routesTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
JTable target = (JTable) e.getSource();
navigateToViewRow(target.rowAtPoint(e.getPoint()));
}
}
});
routesTable.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
JTable target = (JTable) e.getSource();
navigateToViewRow(target.getSelectedRow());
}
}
});
// Bind handler that
routesTable.getSelectionModel().addListSelectionListener(
new RouteSelectionListener(routesTable));
infoLink.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
RoutesManager rm = RoutesView.getInstance(project).getCurrentRoutesManager();
if (rm == null)
return;
switch (infoLinkAction) {
case LINK_OPEN_SETTINGS:
RailwaysUtils.invokeAction("Railways.settingsAction", project);
break;
default:
RailwaysUtils.showErrorInfo(rm);
}
}
});
actionLbl.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (currentRoute != null)
currentRoute.navigate(false);
}
});
}
/**
* Navigates to a route in specified viewRow, if row exists.
* @param viewRow Row index which contains route to navigate to.
*/
private void navigateToViewRow(int viewRow) {
if (viewRow < 0)
return;
int row = routesTable.convertRowIndexToModel(viewRow);
myTableModel.getRoute(row).navigate(false);
}
private void setControlsEnabled(boolean value) {
pathFilterField.setEnabled(value);
routesCounterLbl.setEnabled(value);
}
/**
* Updates text of routes counter label. Sets text to undefined when routes
* list is not visible (info or error panels are shown)
*/
private void updateCounterLabel() {
if (routesHidden)
routesCounterLbl.setText("--/--");
else
routesCounterLbl.setText(String.format("%d/%d",
myTableModel.getRowCount(), myTableModel.getTotalRoutesCount()));
}
private void initToolbar() {
ActionManager am = ActionManager.getInstance();
// The toolbar is registered in plugin.xml
ActionGroup actionGroup = (ActionGroup) am.getAction("railways.MainToolbar");
// Create railways toolbar.
ActionToolbar toolbar = am.createActionToolbar(ActionPlaces.UNKNOWN, actionGroup, true);
toolbar.setTargetComponent(actionsPanel);
actionsPanel.add(toolbar.getComponent(), BorderLayout.CENTER);
}
public JPanel getRootPanel() {
return rootPanel;
}
/**
* Shows additional info in information panel.
*
* @param route Route which info should be showed or nil if info should be
* hidden.
*/
private void showRouteInfo(@Nullable Route route) {
currentRoute = route;
if (route == null) {
routeLbl.setText(NO_INFO);
methodLbl.setText(NO_INFO);
methodLbl.setIcon(null);
actionLbl.setText(NO_INFO);
actionLbl.setIcon(null);
nameLbl.setText(NO_INFO);
} else {
routeLbl.setText(route.getPath());
nameLbl.setText(route.getRouteName());
methodLbl.setText(route.getRequestMethod().getName());
methodLbl.setIcon(route.getRequestMethod().getIcon());
actionLbl.setIcon(route.getActionIcon());
actionLbl.setToolTipText(null);
if (route.canNavigate())
actionLbl.setHyperlinkText(route.getQualifiedActionTitle());
else
actionLbl.setText(route.getQualifiedActionTitle());
if (route instanceof SimpleRoute) {
RailsActionInfo action = ((SimpleRoute)route).getActionInfo();
if (action.getPsiMethod() != null)
actionLbl.setToolTipText("Go to action declaration");
else if (action.getPsiClass() != null)
actionLbl.setToolTipText("Go to controller declaration");
else
actionLbl.setToolTipText("Cannot find controller declaration");
}
}
routeInfoPanel.revalidate();
routeInfoPanel.repaint();
}
// ----------------------------------------
// Railways event handlers
public void setUpdatedRoutes(RouteList routeList) {
myTableModel.setRoutes(routeList);
showRoutesPanel();
UpdateRoutesListAction.updateIcon(project);
}
public void showLoadingMessage() {
RoutesManager.State settings = myDataSource.getRoutesManager().getState();
showMessagePanel("Running `rake " + settings.routesTaskName + "`...",
settings.environment);
}
public void showRoutesUpdateError(int parserError) {
showErrorPanel(parserError);
UpdateRoutesListAction.updateIcon(project);
}
/**
* Sets RoutesViewPane as a source of all data that should be displayed in
* the tool window.
*
* @param dataSource A pane which data should be displayed in the tool window.
*/
public void setDataSource(RoutesViewPane dataSource) {
if (myDataSource == dataSource) return;
myDataSource = dataSource;
RouteList routes =
(dataSource != null) ? dataSource.getRoutesManager().getRouteList() : null;
myTableModel.setRoutes(routes);
}
public void refresh() {
// Use fireTableRowsUpdated to avoid full tree refresh and to keep selection.
myTableModel.fireTableRowsUpdated(0, myTableModel.getRowCount() - 1);
}
public RoutesFilter getRouteFilter() {
return myTableModel.getFilter();
}
private class RouteSelectionListener implements ListSelectionListener {
private JTable table;
public RouteSelectionListener(JTable table) {
this.table = table;
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting())
return;
RouteTableModel model = (RouteTableModel) table.getModel();
int id = table.convertRowIndexToModel(table.getSelectedRow());
Route route = null;
if (id >= 0)
route = model.getRoute(id);
showRouteInfo(route);
}
}
}