Consulo UI API от идеи до прототипа

    Всем привет, давно я не писал статьи о жизни проекта на хабре, решил исправиться и начну пожалуй с того над чем сейчас работаю, а именно Consulo UI API.


Consulo — это форк IntelliJ IDEA Community Edition, который имеет поддержку .NET (C#), Java


Начнем

Q: Consulo UI API — что это такое?

A: Это набор API для создания UI. На деле — простой набор интерфейсов, который повторяет разные компоненты — Button, RadionButton, Label, etc.

Q: Какова цель создания, еще одного набора UI API, если уже был Swing (так как IDEA UI использует Swing для отображения интерфейса)

A: Для этого давайте углубимся в идею которую я последовал работая над Consulo UI API. Так как я основной и практически единственный контрибьютор в проект Consulo, со временем мне стало трудно поддерживать то количество проектов которое сейчас (порядка 156 репозиториев). Встал вопрос о массовом анализе кода, но это нереально сделать в рамках одного инстанса IDE на Desktop платформе, а использовать опыт JetBrains где один проект idea-ultimate держит все плагины я не хотел практиковать по ряду причин.

    Зародилась мысль об анализе на веб-сервере. «Обычный анализ» меня не сильно устраивал на веб-сервере, захотелось сделать Web IDE (хотя бы readonly на старте) — при этом иметь полностью весь тот же функционал что и на Desktop.

    Вы можете сказать что это повторяет немного Upsource, сама идея подобная —, но подход совсем другой.

    И вот пришел тот момент — когда идея была, но не было известно как это сделать. За плечами был опыт использования GWT, Vaadin фреймворков — другие не Java фреймворки для генерации JS (ну или plain js) я не хотел использовать.

    Я уделил себе месяц на это исследования. Это был тест моих возможностей в этой части. Сначала я использовал только GWT — для получения информации я использовал встроенный RPC.

    Была простая цель — проект открыт уже, нужно было только отобразить Project Tree + Editor Tabs. При этом все должно было похожим на Desktop версию.

    Сразу появились проблемы с новоиспеченным бэкэндом. Например использования EventQueue для внутренних действий


    EventQueue если коротко это UI (AWT, Swing) поток в нем происходит почти все что связано с UI — отрисовка, обработка нажатия на кнопку и так далее.
    Исторически сложилось в IDEA то что write действия должны всегда выполняться в UI потоке.
     Write действие — это запись в файл, либо изменения какого то сервиса (например переименования модуля)

    Поначалу проблему с EventQueue можно было игнорировать —, но потом появились другие проблемы. Например банальные иконки. Представим у нас есть дерево проекта


  • [ ] project-name
    • [ ] src
      • [ ] Main.java
    • [ ] test
    • [ ] build.gradle

    И для каждого файла нам нужно загрузить и показать картинку. Так как мы работаем внутри Swing кода — мы используем класс javax.swing.Icon. Проблема в том что это просто интерфейс — который имеет очень много разных реализаций


  • image icon это иконка которая просто оборачивает Image (то есть обычную картинку с файловой системы)
  • layred icon слоеная иконка которая состоит из двух или более иконок наложенных друг на друга
  • disabled icon — иконка с наложенным серым фильтром
  • transparent icon — иконка с заданной прозрачностью
  • и много других

    В итоге чтобы показать иконку в браузере нужно поддерживать весь зоопарк (и почти все сразу).Одна из сопуствующих проблем состоит в том, что может прийти совсем неизвестная тебе иконка для файла (например внутри какого то плагина отрисован попиксельно какой то символ) — и такое нужно игнорировать.

    Методом костыля (ну куда же без них) — было сделано решение. Банально проверять через instanceof на нужный нам тип —, а все другие игнорировать.

    Спустя время была сделана поддержка навигации по файловой системе, открытия файла, подсветка синтаксиса, семантический анализ, quick doc info, навигация по code references (поддерживалась комбинация например Ctrl + B, или Ctrl + MouseClick1). По сути Editor был очень похож на Desktop платформу.

    Как это выглядило:

x4mr31gex_lj3k1jcjoqrzo6xeu.png

    Итак — сделать Web интерфейс было возможно моими силами. Но это была очень грубая работа — которую нужно было переделать. И тут пришёл на помощь Vaadin.

    Я решил переделать мою GWT реализацию на использования Vaadin фреймворка. Этот тест получился очень плохим (очень сильно деградировал перфоманс) — тут больше сказался мой опыт использования Vaadin, и я забраковал этот вариант (я даже сделал hard-reset на текущем бранче, чтобы забыть об этом : D).

    Но опыт использования Vaadin мне всеодно пригодился, зародилась мысль — унифицировать UI чтобы можно было писать один код, но на выходе получать различный результат в зависимости от платформы.

    Еще одна причина унифицировать UI — это полный зоопарк Swing компонентов внутри IntelliJ платформы. Пример такой проблемы — это две совсем разных реализации Tabs

gpfrgtuxuokw4ys9zy995caqgng.png

i1c60wdsldtk5pi1dakzszqis3e.png

Разделяем UI логику:


  • frontend — набор интерфейсов для каждого элемента, например consulo.ui.Button#create ()
  • backend — реализация в зависимости от платформы
    • Swing — desktop реализация
    • WGWT — web реализация

    Что такое WGWT? Сокращения от Wrapper GWT. Это самописный фреймворк — который хранил STATE компонента и отправлял его через WebSocket браузеру (который в свою очередь генерировал html). Он писался с оглядкой на Vaadin (да да — еще один костыль).

    Время шло — и уже я мог запустить тестовый UI, который работал одинаково на Desktop и в браузере

dipzkhwrwphaad8eddsohpoaubc.gif

    Так же паралельно я использовал Vaadin на работе, так как это один из самых дешёвых вариантов построить Web UI если использовать Java. Я все больше и больше изучал Vaadin — и я решил опять переписать WGWT на Vaadin, но с некоторыми правками.

Каковы были правки:


  • отказ от использования практически всех компонентов Vaadin. Причин было несколько — одна из них, это слишком ограниченные компоненты (кастомизация была минимальная).
  • использования уже существующие компоненты из моего самописного фреймворка WGWT — то есть их GWT реализацию
  • так же был сделать patch который позволял писать аннотации Connect без прямой ссылки на серверный компонент (это было сделано больше для project structure, во избежания доступности серверных классов внутри клиент кода)

В итоге получилось вот так:


  • frontend — набор интерфейсов для каждого элемента, например consulo.ui.Button#create ()
  • backend — текущая реализация в зависимости от платформы
    • Swing — desktop реализация
    • Vaadin — web реализация
    • Android? — ради того чтобы телефон сгорел на старте приложения : D Пока только на уровне идеи о том что можно будет использовать уже существующий код для переноса на Android (ибо не будет привязки к Swing)

И так родилась текущая использоваться Consulo UI API.

Где будет использоватся Consulo UI API?


  • Во всех плагинах. AWT/Swing будет «заблочен»(никаких больше java.awt.Color) во время компиляции (будет сделан javac processor — позднее, возможно вообще не нужно будет с приходом java 9). Свой набор компонентов — это не панацея, это я понимаю. На текущий момент — можно сделать свой кастомный UI компонент, пока только на Swing стороне (и в таких случаях нужно будет добавлять зависимость в плагин consulo.destop во избежания проблем на веб сервере). Создания Vaadin компонентов на стороне плагинов пока нет — будет, это минорная задача.
  • На стороне платформы — это Settings/Preferences, Run Configurations, Editor — по сути весь интерфейс который идет до JFrame.

Какие есть проблемы?


  • Полностью не совместимость с AWT/Swing кодом (есть костыльный класс TargetAWT/TargetVaadin который имеет методы для конвертацию компонентов, но эти классы не доступы для плагинов).
    Все Swing компоненты не могут быть отображены в браузере — в итоге нужно переписывать весь этот код.
    Практически везде уже поддерживается Consulo UI API внутри платформы — это позволяет уже использовать новый UI фреймворк внутри плагинов и не только.
  • Очень сильная привязаность IntelliJ платформы к Swing, он зарыт так глубоко — что без «очередного» костыля его не выкопать (cuinchsey3paqijfjqfn8ndniag.gif).


Спустя некоторое время

Вот этот код работает одинаково на обеих платформах.

Его работа на Desktop:

evev_5szgc6hxumf6tljcyzvp7u.png

Его работа в браузере:

ba3mpkqjv3bw90ugi8uy_c1iime.png

По поводу вышеописанных проблем:


  • Иконки. Был введен класс consulo.ui.image.Image который представляет собой картинку с файловой системы (и не только). Для загрузки картинки можно использовать метод consulo.ui.image.Image#create (java.net.URL).

        На Desktop платформе — иконки грузятся как и грузились ранее, только теперь возвращаемый тип это SwingImageRef (legacy названия класса — раньше consulo.ui.image.Image назывался consulo.ui.ImageRef) — интерфейс который наследует javax.swing.Icon и consulo.ui.image.Image. Позднее этот интерфейс будет удален (его существования обусловлено упрощенной миграцией на новый тип)

        На Web платформе — URL сохраняется внутри объекта, и является идентификатором для отображения в интерфейсе (через URL — /app/uiImage=URLhashCode)

        Был введен класс ImageEffects. Он имеет внутри себя методы которые нужны для создания производных иконок. Например #grayed (Image) вернет иконку с серым фильтром, #transparent (Image) полупрозрачную иконку.

        То есть — весь весь вышеописанный зоопарк был загнан в узкие рамки.
        Также будет введена поддержка ручной отрисовки элементов (ну куда без этого). Метод ImageEffects#canvas (int height, int width, Consumer painterConsumer) вернет иконку которая будет отрисована через Canvas2D

        На Desktop — будет использован wrapper поверх обычного Graphics2D из Swing

        На Web — каждый вызов Canvas2D методов будет сохранен, и потом будет передан в браузер где будет использован внутренний Canvas с браузера


  • Write Action in UI Thread. Ууууу. Пока решения этой проблемы нет. На текущий момент — есть прототип Write Action in Own Thread, но пока что только на Web платформе, слишком много нужно поменять внутри платформы чтобы это «выкатить» на Desktop.
  • UI был унифицирован — никакого зоопарка для простых элементов

    Также появилась новая проблема — Swing диалоги блокируют выполняющий поток во время показа. В итоге в IDEA любят писать код в таком виде:

DialogWrapper wrapper = ...;

int value = wrapper.showAndGet();

if(value == DialogWrapper.OK) {
  ...
}

При этом в Vaadin показ диалогов — не блокирует выполняющий поток.

Во избежания путаницы с синхронным и асинхронным показом диалогов — был выбран асинхронный вариант (вышеописанный код придется переосмыслить и переделать).


Итог

Спустя время я имеею рабочий прототип Web приложения.

puopwftlitlwgca5idhdgwyvw80.gif

Пока это прототип, который двигается к своему релизу —, но это будет не быстро (увы).

© Habrahabr.ru