Skip to content

Instantly share code, notes, and snippets.

@risc12
Created June 18, 2025 11:02
Show Gist options
  • Save risc12/72d15b8503e907c4bd6c4d6d2bd52b05 to your computer and use it in GitHub Desktop.
Save risc12/72d15b8503e907c4bd6c4d6d2bd52b05 to your computer and use it in GitHub Desktop.
Find or Create Archi Elements
/**
* Archi Script: Find or Create Element
*
* This script provides a user interface for finding existing elements
* or creating new elements in Archi diagrams. It uses a custom SelectionPalette
* to display and filter elements, and allows for quick element creation and placement.
*
* Element will be placed at the location of the cursor.
*
* Personally I have mapped this to cmd+p.
*/
console.log("findOrCreate.ajs:");
// Import necessary Java classes
/** @type JavaClass */ const SWT = Java.type('org.eclipse.swt.SWT');
/** @type JavaClass */ const GridLayoutFactory = Java.type('org.eclipse.jface.layout.GridLayoutFactory');
/** @type JavaClass */ const Shell = Java.type('org.eclipse.swt.widgets.Shell');
/** @type JavaClass */ const Text = Java.type('org.eclipse.swt.widgets.Text');
/** @type JavaClass */ const Composite = Java.type('org.eclipse.swt.widgets.Composite');
/** @type JavaClass */ const Table = Java.type('org.eclipse.swt.widgets.Table');
/** @type JavaClass */ const TableItem = Java.type('org.eclipse.swt.widgets.TableItem');
/** @type JavaClass */ const TableColumn = Java.type('org.eclipse.swt.widgets.TableColumn');
/** @type JavaClass */ const Listener = Java.type('org.eclipse.swt.widgets.Listener');
/** @type JavaClass */ const GridData = Java.type('org.eclipse.swt.layout.GridData');
/** @type JavaClass */ const GridDataFactory = Java.type('org.eclipse.jface.layout.GridDataFactory');
/** @type JavaClass */ const GridLayout = Java.type('org.eclipse.swt.layout.GridLayout');
/** @type JavaClass */ const Display = Java.type('org.eclipse.swt.widgets.Display');
/** @type JavaClass */ const SelectionAdapter = Java.type('org.eclipse.swt.events.SelectionAdapter');
/** @type JavaClass */ const KeyAdapter = Java.type('org.eclipse.swt.events.KeyAdapter');
/** @type JavaClass */ const ModifyListener = Java.type('org.eclipse.swt.events.ModifyListener');
/** @type JavaClass */ const Image = Java.type('org.eclipse.swt.graphics.Image');
/** @type JavaClass */ const ArchiPlugin = Java.type('com.archimatetool.editor.ArchiPlugin');
/** @type JavaClass */ const IArchiImages = Java.type('com.archimatetool.editor.ui.IArchiImages');
/** @type JavaClass */ const Point = Java.type('org.eclipse.draw2d.geometry.Point');
/** @type JavaClass */ const GC = Java.type('org.eclipse.swt.graphics.GC');
/** @type JavaClass */ const FontData = Java.type('org.eclipse.swt.graphics.FontData');
// Define the SelectionPalette class
class SelectionPalette {
constructor(items, callbacks) {
this.shell = shell;
this.items = items;
this.filteredItems = this.items.slice();
this.callbacks = callbacks;
}
open() {
let display = shell.display;
// Create a shell with no trim (no window decorations) and no background
let dialog = new Shell(this.shell, SWT.ON_TOP);
const layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
dialog.setText("Select Item");
dialog.setSize(600, 600);
dialog.setLayout(layout);
dialog.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
// Little border around the input
const comp = new Composite(dialog, SWT.ON_TOP);
const compLayout = new GridLayout();
compLayout.marginHeight = 5;
compLayout.marginWidth = 5;
comp.setLayout(compLayout);
comp.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
comp.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); // Set background to white
// Text input for filtering items
let filterText = new Text(comp, SWT.NONE);
filterText.setMessage("Type to search or create...");
filterText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
filterText.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); // Set background to white
// Table to display items
let table = new Table(dialog, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION | SWT.VIRTUAL);
table.setHeaderVisible(false);
table.setLinesVisible(false);
table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
table.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); // Set background to white
table.setItemCount(this.filteredItems.length);
// Add SetData listener to populate items on-demand
table.addListener(SWT.SetData, new Listener({
handleEvent: (event) => {
const item = event.item;
const index = event.index;
const data = this.filteredItems[index];
if (data) {
item.setText([data.name, data.type]);
item.setImage(data.icon);
item.setData("id", data.id);
}
}
}));
// Define columns
let nameColumn = new TableColumn(table, SWT.NONE);
nameColumn.setText("Name");
nameColumn.setWidth(300);
let typeColumn = new TableColumn(table, SWT.NONE);
typeColumn.setText("Type");
typeColumn.setWidth(200);
// Populate table with items
this.populateTable(table, this.filteredItems);
const filterItems = () => {
let filter = filterText.getText();
this.filteredItems = this.items
.map(item => ({
...item,
score: fuzzyMatchScore(item.name, filter)
}))
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score);
this.populateTable(table, this.filteredItems);
}
// Handle Enter key press in the table to select an item
table.addKeyListener(new KeyAdapter({
keyPressed: (e) => {
if (e.keyCode === 13) { // Enter key
let selectedItem = table.getSelection()[0];
this.callbacks.onExisting(selectedItem.getData("id")); // Use ID for lookup
dialog.close();
}
}
}));
// Filter items as the user types
filterText.addModifyListener(new ModifyListener({
modifyText: () => filterItems()
}));
// Handle Enter key press in filterText to create new item
filterText.addListener(SWT.DefaultSelection, (e) => {
if (!this.callbacks.onNew) return;
let newItem = filterText.getText();
this.callbacks.onNew(newItem);
dialog.close();
});
// Handle key events to move focus to the table
filterText.addKeyListener(new KeyAdapter({
keyPressed: (e) => {
if (e.keyCode === SWT.ARROW_DOWN) {
table.setFocus();
if (table.getItemCount() > 0) {
table.setSelection(0);
}
}
}
}));
// Set the dialog location to the cursor location
const cursorLocation = shell.display.getCursorLocation();
dialog.setLocation(cursorLocation.x, cursorLocation.y);
// Add a focus listener to close the dialog when it loses focus
dialog.addListener(SWT.Deactivate, () => dialog.close());
// Keep the dialog open until it is closed
dialog.open();
while (!dialog.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
populateTable(table, items) {
table.setItemCount(items.length);
table.clearAll();
}
}
// Main function to run the SelectionPalette
function main() {
const canvasCoordinates = getCanvasCoordinates();
if (!canvasCoordinates) return alert("Something went wrong while trying to determine the location of the cursor inside the canvas...");
let findOrCreatePalette = new SelectionPalette(getAllElements(), {
onExisting: (id) => {
addElement($(`#${id}`).first(), canvasCoordinates)
},
onNew: name => {
new SelectionPalette(getAllElementTypes(), {
onExisting: id => {
let element = model.createElement(id, name);
addElement(element, canvasCoordinates);
}
}).open();
}
});
findOrCreatePalette.open();
}
main();
/* --------------- Helpers ------------------------------------------------------*/
function addElement(element, location) {
let view;
if (selection[0].getType() === "archimate-diagram-model") {
view = selection[0];
} else {
view = selection[0].getView();
}
if (!view) return alert("No current view");
view.add(element, location.x, location.y, -1, -1);
}
function getCanvasCoordinates() {
const cursorLocation = shell.display.getCursorLocation();
// This is disgusting... Only to get the location of the cursor, but in canvas coordinates. Fails when using splits.
const graphicalViewer = globalThis.workbenchwindow.getActivePage().getActiveEditor().getGraphicalViewer();
const locationInsideCanvas = graphicalViewer.getControl().getViewport().getViewLocation();
const controlLocation = graphicalViewer.getControl().getParent().getParent().getParent().getParent().getLocation();
const locationOfCanvasOnScreen = graphicalViewer.getControl().toDisplay(controlLocation);
const locationOfCursorOnControl = new Point(cursorLocation.x - locationOfCanvasOnScreen.x, cursorLocation.y - locationOfCanvasOnScreen.y);
const zoomLevel = graphicalViewer.getRootEditPart().getZoomManager().getZoom();
const locationOfCursorInsideCanvas = new Point(locationOfCursorOnControl.x + locationInsideCanvas.x, locationOfCursorOnControl.y + locationInsideCanvas.y).scale(1 / zoomLevel);
return locationOfCursorInsideCanvas;
}
/**
* Convert a camel case or Pascal case string to UPPERCASE_CASING
* @param {string} str - The input string in camel case or Pascal case
* @return {string} - The converted string in UPPERCASE_CASING
*/
function toUpperCaseCasing(str) {
return str.replaceAll("-", "_").replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
}
function fuzzyMatchScore(string, query) {
const hay = string.toLowerCase();
const s = query.toLowerCase();
let score = 0;
let lastIndex = -1;
let consecutiveMatches = 0;
for (let i = 0; i < s.length; i++) {
const index = hay.indexOf(s[i], lastIndex + 1);
if (index === -1) return 0; // No match
score += hay.length - index; // Prioritize earlier matches
if (index === lastIndex + 1) {
consecutiveMatches++;
score += consecutiveMatches * 2; // Bonus for consecutive matches
} else {
consecutiveMatches = 0;
}
lastIndex = index;
}
// Bonus for exact match
if (hay === s) score += 1000;
return score;
}
function getImageForElementType(elementType) {
return IArchiImages.ImageFactory.getImage(IArchiImages[`ICON_${toUpperCaseCasing(elementType)}`] || IArchiImages.ICON_CANCEL_SEARCH);
}
/**
* Get all elements from the model
* @return {Array} - Array of elements with id, name, type, and icon
*/
function getAllElements() {
return $('element').map(element => ({
id: element.id, // Store the element ID
name: element.name,
type: element.type,
icon: getImageForElementType(element.type),
}));
}
/**
* Get all element types
* @return {Array} - Array of element types with id and icon
*/
function getAllElementTypes() {
return [
"capability", "course-of-action", "resource", "value-stream",
"business-actor", "business-role", "business-collaboration", "business-interface", "business-process", "business-function", "business-interaction", "business-event", "business-service", "business-object", "contract", "product", "representation",
"application-component", "application-collaboration", "application-interface", "application-function", "application-interaction", "application-process", "application-event", "application-service", "data-object",
"node", "device", "system-software", "technology-collaboration", "technology-interface", "path", "communication-network", "technology-function", "technology-process", "technology-interaction", "technology-event", "technology-service", "artifact",
"equipment", "facility", "distribution-network", "material",
"workpackage", "deliverable", "implementation-event", "plateau", "gap",
"stakeholder", "driver", "assessment", "goal", "outcome", "principle", "requirement", "meaning", "value", "constraint",
"grouping", "location"
].map(id => ({
id,
name: id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
icon: getImageForElementType(id)
}));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment