Files
diy-layout-creator/diylc/.claude/skills/add-plugin/SKILL.md
T

189 lines
8.5 KiB
Markdown

---
name: add-plugin
description: Add or modify a DIYLC Swing plugin, menu action, toolbar item, dialog or side panel. Use when asked to add a UI feature, a menu entry, a new tool window, a background task, to react to domain events like selection or project changes, or to localize UI text and update the language files.
---
# Adding a plugin or UI feature to DIYLC
The Swing module is a shell that hosts plugins. Everything the user sees — the toolbox, the canvas,
every menu, the status bar, autosave — is an `IPlugIn`. Adding UI means adding or extending a plugin,
not reaching into frames.
## Where things go
| Path | Contents |
| --- | --- |
| `diylc-swing/src/main/java/org/diylc/swing/plugins/<name>/` | One package per plugin |
| `diylc-swing/src/main/java/org/diylc/swing/actions/` | `AbstractAction` implementations, grouped into `file`, `edit`, `analyze` sub-packages |
| `diylc-swing/src/main/java/org/diylc/swing/gui/` | `MainFrame`, dialogs, editors, shared components |
| `diylc-swing/src/main/java/org/diylc/swing/gui/MainFrame.java` | Where plugins are installed |
## Before writing anything
Read the plugin closest to what is being asked. `plugins/autosave/AutoSavePlugin.java` is the
smallest complete example (event subscription plus background work);
`plugins/statusbar/`, `plugins/layers/` and `plugins/analyze/` show menu and panel injection.
Match the one you read.
## The plugin
```java
/* ... GPL header from HEADER.txt ... */
package org.diylc.swing.plugins.myfeature;
public class MyFeaturePlugin implements IPlugIn {
private static final Logger LOG = Logger.getLogger(MyFeaturePlugin.class);
private ISwingUI swingUI;
private IPlugInPort plugInPort;
public MyFeaturePlugin(ISwingUI swingUI) {
this.swingUI = swingUI;
}
@Override
public void connect(IPlugInPort plugInPort) {
this.plugInPort = plugInPort;
swingUI.injectMenuAction(new MyAction(plugInPort), "Tools");
}
@Override
public EnumSet<EventType> getSubscribedEventTypes() {
return EnumSet.of(EventType.SELECTION_CHANGED);
}
@Override
public void processMessage(EventType eventType, Object... params) {
if (eventType == EventType.SELECTION_CHANGED) {
...
}
}
}
```
Install it in `MainFrame` alongside the others:
```java
presenter.installPlugin(() -> new MyFeaturePlugin(this));
```
Order matters for menus and for docked panels — insert the line where the resulting menu or panel
position is correct, not blindly at the end.
## Talking to the domain
Plugins reach the domain **only** through `IPlugInPort`. Never import a concrete component type into
a plugin unless the feature is genuinely component-specific, and never call into `Presenter`
directly. If `IPlugInPort` lacks what you need, adding a method there is the right move — but it also
means touching `diylc-core`, so keep the addition minimal and in the style of its neighbours.
## Reacting to events
`getSubscribedEventTypes()` returns an `EnumSet<EventType>`. Subscribe to the narrowest set that
works. `PROJECT_MODIFIED`, `REPAINT` and `MOUSE_MOVED` fire constantly — anything non-trivial in
response to those will be felt as lag. The available types are in
`diylc-core/src/main/java/org/diylc/common/EventType.java` (`PROJECT_LOADED`, `PROJECT_SAVED`,
`SELECTION_CHANGED`, `ZOOM_CHANGED`, `SLOT_CHANGED`, `LAYER_STATE_CHANGED`, `CLOUD_LOGGED_IN`, …).
## Injecting UI
- `swingUI.injectMenuAction(action, "Menu Name")` — a menu item; passing a `null` action inserts a
separator, and a missing menu is created automatically.
- `swingUI.injectSubmenu(name, icon, parentMenuName)` and
`injectDynamicSubmenu(name, icon, parentMenuName, handler)` for submenus.
- `swingUI.injectGUIComponent(component, SwingConstants.LEFT, collapsible, visibilityConfigKey)` for
a docked panel. `TOP`, `BOTTOM`, `LEFT`, `RIGHT` are valid; `CENTER` is reserved for the canvas.
Pass a config key for collapsible panels so the open/closed state persists.
- `swingUI.getOwnerFrame()` is the parent for dialogs.
## Actions
Actions are small classes extending `javax.swing.AbstractAction`, holding an `IPlugInPort`, setting
their label with `putValue(AbstractAction.NAME, ...)`, and delegating to `plugInPort` in
`actionPerformed`. See `actions/RenumberAction.java` for the minimal shape. They carry
`private static final long serialVersionUID = 1L;` like any Swing class.
## Background work
Never block the EDT. Use `swingUI.executeBackgroundTask(task, blockUI)` with an `ITask<T>`, which
handles the busy cursor and optional glass pane, or a dedicated executor for periodic work as
`AutoSavePlugin` does.
## Settings and strings
- Persisted preferences: `ConfigurationManager.getInstance().readString(KEY, default)` and
`writeValue(KEY, value)`, with the key as a `String` constant. Implement `IConfigListener` to react
to changes.
- Every user-visible string goes through `LangUtil.translate("...")` — see *Localization* below.
- Icons come from `IconLoader`.
## Localization
DIYLC ships translations in `diylc-swing/src/main/resources/lang/`. `LangUtil.translate(text)` looks
the whole English string up, case-insensitively, and falls back to the English text when there is no
entry. A string that never reaches `translate` is never localized, so every new piece of UI needs
both the call and the dictionary entries. Adding the entries and translating them into **every**
language file is part of the change itself — do not leave it as a follow-up or ship English-only
keys.
**What `MainFrame` translates for you.** Menu names and the `Action.NAME` of every action passed to
`injectMenuAction`, `injectSubmenu` and `injectDynamicSubmenu` are translated on injection. Pass the
English literal there and do not translate it yourself: `ToggleAction` and `ComponentBrowserAction`
use their English title as the persisted config value, and translating a translated string can hit
an unrelated entry.
**What you must translate yourself.** Everything else the user reads:
- the message *and* the title of `showMessage`, `showConfirmDialog` and `showInputDialog` — titles
like `"Error"`, `"Success"` and `"Export"` included;
- dialog titles, button and label text, tooltips, placeholder text and borders;
- text rendered into panes, including HTML (translate the sentence, keep the markup outside it).
**Text with variables uses placeholders, never concatenation.** The key passed to `translate` is the
whole sentence with `%s` where the values go, and `String.format` fills them in afterwards:
```java
// BAD — every file name produces a different key, so nothing is ever translated
swingUI.showMessage("Variants exported to \"" + file.getName() + "\".", "Success", ...);
// GOOD
swingUI.showMessage(
String.format(LangUtil.translate("Variants exported to \"%s\"."), file.getName()),
LangUtil.translate("Success"), ISwingUI.INFORMATION_MESSAGE);
```
- Use `%s` for every value, numbers included; a translator whose grammar needs another order writes
`%2$s ... %1$s`. A literal percent sign must be `%%`.
- One key per complete sentence. Do not build a sentence from translated fragments — word order
differs between languages. A long paragraph may be several keys, one per sentence.
- URLs, file names, HTML links and other values that must not change go in through `%s`, not into
the key.
- Log messages and exception text stay in English and are not translated.
**Dictionary entries.** For each new key:
- append the English text on its own line to `English.txt`;
- append `English text|Translation` to every other file in `lang/`, keeping the terminology that
file already uses (grep it for `Error`, `Export`, `Variants`, ...). The translation must contain
the same number of placeholders as the key, and neither side may contain `|`;
- recent entries are appended at the end of each file; do the same, and preserve each file's line
endings (most use CRLF, `हिंदी.txt` and `简体中文.txt` use LF) and UTF-8 encoding.
Running the app with `-Dorg.diylc.storeDict=true` writes every string that reached `translate`
without a dictionary entry to `pending_translations.txt` in the working directory, which is a quick
way to find gaps after a UI change.
## Verify
```bash
mvn -q -pl diylc-swing -am compile
```
Confirm that every new user-facing string reaches `LangUtil.translate` and has an entry in each file
under `diylc-swing/src/main/resources/lang/`.
Compilation is not evidence that the UI works. Say so, and offer to walk through launching the app
via `.run/DIYLCStarter.run.xml` (which carries the required `--add-opens` / `--add-exports` flags) for
a visual check.