Created
June 18, 2025 11:02
-
-
Save risc12/72d15b8503e907c4bd6c4d6d2bd52b05 to your computer and use it in GitHub Desktop.
Find or Create Archi Elements
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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