Skill v1.0.0
currentAutomated scan100/100name: 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. metadata: version: "1.0.0"
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
/* ... 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;}@Overridepublic void connect(IPlugInPort plugInPort) {this.plugInPort = plugInPort;swingUI.injectMenuAction(new MyAction(plugInPort), "Tools");}@Overridepublic EnumSet<EventType> getSubscribedEventTypes() {return EnumSet.of(EventType.SELECTION_CHANGED);}@Overridepublic void processMessage(EventType eventType, Object... params) {if (eventType == EventType.SELECTION_CHANGED) {...}}}
Install it in MainFrame alongside the others:
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 anullaction 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,showConfirmDialogandshowInputDialog— 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:
// BAD — every file name produces a different key, so nothing is ever translatedswingUI.showMessage("Variants exported to \"" + file.getName() + "\".", "Success", ...);// GOODswingUI.showMessage(String.format(LangUtil.translate("Variants exported to \"%s\"."), file.getName()),LangUtil.translate("Success"), ISwingUI.INFORMATION_MESSAGE);
- Use
%sfor 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|Translationto every other file inlang/, 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
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.