Предыстория
Встала задача создать для разработчиков и QA удобный способ стартовать порядка 20 серверных приложений, живущих в общем репозитрии (Spring с XML конфигурацией и общим для все частей приложения бутстрап классом).
Как сделать нечто удобное человеку, который последний раз GUI рисовал в Borland Delphi 6.0? Взять что-то уже готовое и приспособить для своих нужд, ну и раз уж будущие пользователи работают в IntelliJ Idea, появилась мысль соорудить плагин, который будет выглядеть и вести себя так, как это делает Maven Integration Plugin.
Под катом классы и некоторые утилитарные методы, которые помогут это сделать.
Пара слов о документации
Документацию по плагинам для Idea можно найти тут или в серии статей на хабре.
Единственная проблема этой документации в том, что про UI там нет почти ничего, к счастью, есть код, а это ведь лучше любой документации! (особенно если у тебя первый разряд по совковой лопате)
Нас, разумеется, интересует папка plugins/maven.
Tips and tricks
Tool Window
Чтобы добавить свое Tool Window документация предлагает использовать plugin.xml. Но если нужно, например, контролировать время инициализации окна, то придется все это делать вручную:
private void initToolWindow() {
final ToolWindowManagerEx manager = ToolWindowManagerEx.getInstanceEx(myProject);
ToolWindowEx myToolWindow = (ToolWindowEx) manager.registerToolWindow(HolyProjectPanel.ID, false, ToolWindowAnchor.RIGHT, myProject, true);
myToolWindow.setIcon(IconLoader.findIcon("/icons/jesterhat.png"));
final ContentFactory contentFactory = ServiceManager.getService(ContentFactory.class);
final Content content = contentFactory.createContent(panel, "", false);
ContentManager contentManager = myToolWindow.getContentManager();
contentManager.addContent(content);
}
Как дождаться, пока Idea полностью инициализирует проект?
Ничего сложного, если найти пример кода:
public static void runWhenInitialized(final Project project, final Runnable r) {
if (project.isDisposed()) return;
if (!project.isInitialized()) {
StartupManager.getInstance(project).registerPostStartupActivity(DisposeAwareRunnable.create(r, project));
return;
}
runDumbAware(project, r);
}
public static void runDumbAware(final Project project, final Runnable r) {
if (DumbService.isDumbAware(r)) {
r.run();
}
else {
DumbService.getInstance(project).runWhenSmart(DisposeAwareRunnable.create(r, project));
}
}
Как вырастить дерево
Перед тем, как выращивать дерево, нужно понять где это делать. Ответ очевиден: JPanel (упомянутый выше HolyProjectPanel является подклассом SimpleToolWindowPanel, который наследует от JPanel (яйцо в утке, утка в зайце, заяц в шоке).
Создаем объект JTree и сохраняем его как контент.
UI Maven plugin'a построено на классе SimpleTree, использование его ничем не отличается от JTree, однако оно добавляет полезные фичи, например, поиск на лету:
Как наполнить дерево
Наполнение предлагается делать с помощью SimpleTreeBuilder:
SimpleTreeBuilder myTreeBuilder = new SimpleTreeBuilder(tree, (DefaultTreeModel) tree.getModel(), this, null)
Disposer.register(project, myTreeBuilder);
myTreeBuilder.initRoot();
myTreeBuilder.expand(myRoot, null);
Кроме всего прочего, он позволяет сортировать узлы, достаточно просто передать Comparator как последний аргумент конструктора.
Также, часто полезно обновить все узлы, начиная с данного:
myTreeBuilder.addSubtreeToUpdate(node)
Чем наполнить дерево
По аналогии с SimpleTree и SimpleTreeBuilder будем использовать SimpleNode. Класс этот прост, как три копейки. Всего один метод getChildren, который нужно реализовать и который вызывается каждый раз, когда надо отрисовать узел (при открытии всего окна или при разворачивании поддерева), и несколько доступных полей с очевидными названиями, которые легко найти автодополнением (myName, myClosedIcon и так далее).
Эта кажущаяся простота кончается, когда начинаешь вглядываться в детали.
Как написать название узла разными цветами
Класс SimpleNode отрисовывается с помощью объектов класса PresentationData. его и надо использовать:
private void updatePresentation() {
PresentationData presentation = getPresentation();
presentation.clear();
presentation.addText(myName, SimpleTextAttributes.REGULAR_ATTRIBUTES);
presentation.addText(" Red", new SimpleTextAttributes(Font.PLAIN, Color.RED));
presentation.addText(" Level2Node", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
}
Кроме прочего, этот же объект надо использовать, чтобы перерисовать узел:
@Override
public void handleDoubleClickOrEnter(SimpleTree tree, InputEvent inputEvent) {
if(Color.RED.equals(myColor)){
myColor = Color.BLUE;
} else {
myColor = Color.RED;
}
updatePresentation();
}
private void updatePresentation() {
PresentationData presentation = getPresentation();
presentation.clear();
presentation.addText(myName, SimpleTextAttributes.REGULAR_ATTRIBUTES);
presentation.addText(" Red", new SimpleTextAttributes(Font.PLAIN, myColor));
presentation.addText(" Level2Node", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
}
Почему дерево сворачиватся каждый раз, когда я закрываю и открываю панель?
Это происходит потому, что все узлы, кроме корневого, на самом деле создаются заново. Чтобы избежать этого используйте класс CachingSimpleNode вместо SimpleNode (Он так же имеет единственный метод, который необходимо реализовать: buildChildren()).
Чтобы вручную перерисовать CachingSimpleNode необходимо вызвать на нем метод update() или update(PresentationData).
Как добавить кнопки над деревом
Способ добавления кнопок показался мне не вполне очевидным (если создаешь само ToolsWindow в коде — тем более). Они добавляются в файле plugin.xml в разделе Actions, там же настраиваются названия, иконки, тултипы и все остальное.
<actions>
<action id="HolyProject.ExpandAll" class="ui.ProcessesTreeAction$ExpandAll" text="Expand All"
icon="AllIcons.Actions.Expandall"/>
<action id="HolyProject.CollapseAll" class="ui.ProcessesTreeAction$CollapseAll" text="Collapse All"
icon="AllIcons.Actions.Collapseall"/>
<action class="actions.MyToggleAction" id="HolyProject.RefreshProcesses" icon="AllIcons.Nodes.Cvs_roots"
text="Refresh HolyProjectProcesses status" description="Refresh HolyProjectProcesses status"/>
<action class="actions.CommonAction" id="HolyProject.RecreateProcesses" icon="AllIcons.ToolbarDecorator.Import"
text="Recreate Processes List" description="Recreate Processes List"/>
<group id="HolyProject.ProcessesToolbar">
<reference id="HolyProject.RefreshProcesses"/>
<reference id="HolyProject.RecreateProcesses"/>
<separator/>
<reference id="HolyProject.ExpandAll"/>
<reference id="HolyProject.CollapseAll"/>
</group>
</actions>
Чтобы добавить Enabled/Disabled кнопку, как "Toggle 'Skip Tests' Mode" необходимо использовать ToggleAction вместо AnAction
Как вызвать какое-то действие в UI Thread из другого потока
Это не относится напрямую к разработке панели, но слишком часто возникает необходимость вызвать действие, которое должно выполняться в UI потоке, откуда-то еще. Для этого есть метод:
AppUIUtil.invokeOnEdt(() -> {/*do smth useful*/});
Вот и все, чем я хотел поделиться, пример плагина есть у меня на гитхабе.
Удачи!