
IntelliJ를 평소와 같이 사용하다가 문득 IntelliJ 배경화면에 최애 캐릭터의 움짤이나 무한도전 움짤과 같은 뭔가 움직이는 사진을 배경화면으로 사용하면 좋겠다는 생각이 들었습니다.
그래서 기본적으로 IntelliJ에서 제공해주지 않을까? 하는 생각에 gif 파일을 설정해보았지만, 전혀 되지 않았습니다. (첫 번째 이미지로 설정됨)
그럼 VSCODE 처럼 누군가가 플러그인으로 만들어놓지 않았을 까? 하는 기대로 플러그인들을 찾아보았고, 딱 하나 있었습니다.
하지만 이마저도 성능이 좋지 않은 환경에서는 정상 동작이 어려워 보였고, 여러 문제점들이 보였습니다.
그래서 이참에 만들어버리면 사용도 하고 오랜만에 swing도 만질 수 있을 것 같았기에 프로젝트를 시작했습니다.
IntelliJ 플러그인을 만드는 것은 처음이었기에 조금 어렵게 다가왔었습니다.
우선, IntelliJ가 어떤 원리로 에디터를 이루고 있고, 그 속에서 배경화면을 어떻게 수정해야하는 지 역시 감을 못 잡았었기 때문입니다.
그래서 BlueDriver는 어떻게 구현하였는 지 궁금하여 소스코드를 보고, IdeBackgroundUtil 클래스에 전적으로 의존하여 매 프레임마다 이미지를 바꾸고 있었습니다.
하지만 위 방법은 리소스와 버그를 발생할 수 있는 방법이기에 썩 좋은 방법이 아니라고 판단되었습니다. (그리곤 누군가가 이슈로 이 내용으로 조롱하기까지 했죠)
위 방법의 가장 치명적인 문제점은 dirtyRegion(repaint를 해야하는 영역)이 매우 넓고, IntelliJ에 전적으로 판단을 맡긴다는 점입니다. 이로 인해 굳이 변경되지 않아도 되는 컴포넌트들까지 dirtyRegion에 포함되어 repaint가 됩니다. 그리고 잦은 repaint는 event cancel과 리소스가 상당히 많이 발생하기도 합니다.
그렇다면 어떻게 해야 repaint가 가장 최소한으로 호출 혹은 특정 부분만 repaint되도록 할 수 있을 까? 라는 질문을 끊임없이 던진 결과, IntelliJ는 Swing으로 구현되고 있다는 점을 활용하여, 배경화면에 위치한 영역에 직접 pane를 하나 두고선 이미지를 그리면 되지 않을까? 하는 생각이 들었습니다.
이 방법이 가능하다는 확신은 없었기에 임의로 pane을 생성하고 보여지도록 했지만, 전혀 되지 않았습니다. 그리고 그 이유는 금방 알아낼 수 있었습니다. IntelliJ는 이미 구성된 범위 내에서만 새롭게 그 위에 추가는 되지만, 범위를 벗어나는 경우 ignore된다는 점이었습니다.
그래서 어떻게 하면 그 범위를 찾아내고, 범위 내에 컴포넌트를 추가할 수 있을까? 라는 생각을 하게 되었습니다.
IdeBackgroundUtil에는 inner class 로 MyGraphics 라는 클래스가 있는 데, 기본적으로 배경을 그려낼 때, 해당 클래스를 사용하여 배경을 그린다는 것을 알게되었습니다. 하지만 해당 클래스는 private 클래스였기에 접근하는 것이 불가능했습니다. 그리고 painter라는 것을 IntelliJ에서 등록시키고, 그 painter가 MyGraphics를 그려내는 방식으로 사이클이 돌아가고 있었습니다. (MyGraphics가 그림을 그리고, dirtyRegion 영역을 설정한다. 그리고 그 dirtyRegion를 painter가 repaint한다.)
그렇다면 IntelliJ에 어떻게 painter를 등록시키는 가? 가 제게 새로운 목표가 되었습니다.
IntelliJ의 painter는 IdeGlassPaneImpl 클래스를 통해 등록하는 것이 가능했습니다.
하지만 등록시키는 메소드는 private(internal)이었기에 외부에서 사용하는 것이 불가능했습니다.
(뭐 죄다 private거나 default야)
그러다 문득 한 가지 방법이 떠올랐습니다. Reflection API를 사용하여 강제로 flag를 바꿔 호출하는 방법이었습니다. 물론 좋은 방법이라고는 할 수 없었지만, 당시로 생각해낼 수 있는 방법 중 가장 이상적이어서 사용했습니다.
private Object getPaintersHelper(IdeGlassPaneImpl glassPane) {
return ReflectionUtil.invoke(Object.class, IdeGlassPaneImpl.class, glassPane,
"getNamedPainters$intellij_platform_ide_impl",
List.of(new Pair<>(String.class, "idea.background.editor")));
}
위 방법을 통해 배경화면에 위치한 PainterHelper 클래스(default 클래스이기에 Object로 지정)를 가져와서 다시 한 번 Reflection을 통해 강제로 painter를 주입(추가)하였습니다.
private void addPainter(JRootPane rootPane) {
IdeGlassPaneImpl glassPane = (IdeGlassPaneImpl) rootPane.getGlassPane();
Object helper = getPaintersHelper(glassPane);
ReflectionUtil.invokeVoid(helper.getClass(), helper, "addPainter",
List.of(new Pair<>(Painter.class, painter), new Pair<>(Component.class, glassPane)));
ReflectionUtil.get(Set.class, helper.getClass(), helper, "painters");
}
painter를 성공적으로 넣었으니, 이제 repaintManager를 IntelliJ에 넣어서 repaint를 언제 할지를 조절할 수 있도록 하였습니다. (이 역시, 정상적인 방법으로 replace는 불가하였기에 Reflection를 사용하였습니다. Reflection 최고!)
private void replaceRepaintManager() {
Object appContext = ReflectionUtil.invoke(Object.class, "sun.awt.AppContext",
null, "getAppContext", List.of());
this.repaintManager = new MyRepaintManager();
ReflectionUtil.invokeVoid(appContext.getClass(), appContext, "put",
List.of(new Pair<>(Object.class, RepaintManager.class), new Pair<>(Object.class, repaintManager)));
repaintRunnable = ReflectionUtil.get(Runnable.class, RepaintManager.class, repaintManager, "processingRunnable");
initialized = true;
}
AppContext를 가져와서, repainter를 추가해주고선 runnable 까지 가져왔습니다.
runnable은 강제로 이미지(배경화면)을 바꿔야할때 #run 해주는 역할로 가져왔습니다.
painter와 repaintManager를 추가해주었으니, 이제 본격적으로 gif 파일을 decode를 통해 프레임마다 한 장씩 분해하고선 분해한 이미지를 순서대로 배경화면에 그려주면 됩니다.
gif decode는 Dhyan Blum의 GifDecoder 클래스를 사용하여 손쉽게 분해하였고, 이를 디렉토리에 순서대로 저장하였습니다.
그리고 이미지를 순서대로 바뀌고 그리도록 하였습니다.
final long maxInterval = grabInterval * 2;
FrameInfo info;
if (isPlaying()) {
info = images.get(frame++);
_notify();
long occurredTime = measureTimeMillis(() -> draw(info.image()));
long interval = grabInterval - occurredTime;
if (interval > 0) {
_wait2(maxInterval);
}
} else {
info = null;
}
if (frame >= images.size()) {
frame = 0;
}
#draw 는 이미지 사이즈가 에디터 화면에 최대한 비율적으로 맞도록 설정하고 그리는 역할을 수행합니다. 이미지 graphic를 가지고 #drawImage를 통해 이미지를 그립니다.
이제 마음대로 gif 파일을 IntelliJ 배경화면으로 사용할 수 있어서 매우 행복하다.
만드는 데에는 보름정도 걸렸다. (push를 했어야했는 데 깜빡하고 하지 않고 여행을 가는 바람에 열흘 정도로 추가로 포함되어버렸다.)
만들고 싶은 것을 만들며 스트레스를 많이 푼 것 같다. 이제 다시 취준에 몰입해야할 것 같다.
소스코드
이런 유용한 정보를 나눠주셔서 감사합니다.