Skip to content

Commit

Permalink
Add Context menu to layer tree items (#48)
Browse files Browse the repository at this point in the history
* Add context menu to rename and remove groups and layers

* Tweak getlayerTreeInfo function

* Tweak mainview changes

* Move where commands are added

* Add commands to palette

* wip

* Implement multi select in layer tree panel

* Rename function

* Move layers wip

* Fix right click behavior

* Fix duplicate menu item

* Clean up

* Adjust css

* Add layers to new group wip

* Implement review suggestions

* Add layers to new group
  • Loading branch information
gjmooney authored Jul 17, 2024
1 parent 8f7c477 commit d98aa7d
Show file tree
Hide file tree
Showing 9 changed files with 708 additions and 69 deletions.
260 changes: 252 additions & 8 deletions packages/base/src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
import { JupyterFrontEnd } from '@jupyterlab/application';
import { Dialog, WidgetTracker, showErrorMessage } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import { ITranslator } from '@jupyterlab/translation';
import { redoIcon, undoIcon } from '@jupyterlab/ui-components';
import {
IDict,
IGeoJSONSource,
IJGISFormSchemaRegistry,
IJGISLayer,
IJGISLayerBrowserRegistry,
IJGISLayerGroup,
IJGISLayerItem,
IJGISSource,
IJupyterGISModel
IJupyterGISModel,
SelectionType
} from '@jupytergis/schema';
import { JupyterFrontEnd } from '@jupyterlab/application';
import { Dialog, WidgetTracker, showErrorMessage } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import { ITranslator } from '@jupyterlab/translation';
import { redoIcon, undoIcon } from '@jupyterlab/ui-components';
import { UUID } from '@lumino/coreutils';
import { Ajv } from 'ajv';
import * as geojson from 'geojson-schema/GeoJSON.json';

import { GeoJSONLayerDialog } from './dialogs/geoJsonLayerDialog';
import { LayerBrowserWidget } from './dialogs/layerBrowserDialog';
import {
DataErrorDialog,
DialogAddDataSourceBody,
FormDialog
} from './formdialog';
import { geoJSONIcon } from './icons';
import { LayerBrowserWidget } from './dialogs/layerBrowserDialog';
import { GeoJSONLayerDialog } from './dialogs/geoJsonLayerDialog';
import { JupyterGISWidget } from './widget';

/**
* The command IDs.
*/
export namespace CommandIDs {
export const createNew = 'jupytergis:create-new-jGIS-file';
export const redo = 'jupytergis:redo';
export const undo = 'jupytergis:undo';

Expand All @@ -41,6 +45,14 @@ export namespace CommandIDs {
export const newVectorTileLayer = 'jupytergis:newVectorTileLayer';

export const newVectorLayer = 'jupytergis:newVectorLayer';

export const renameLayer = 'jupytergis:renameLayer';
export const removeLayer = 'jupytergis:removeLayer';
export const renameGroup = 'jupytergis:renameGroup';
export const removeGroup = 'jupytergis:removeGroup';

export const moveLayersToGroup = 'jupytergis:moveLayersToGroup';
export const moveLayerToNewGroup = 'jupytergis:moveLayerToNewGroup';
}

/**
Expand Down Expand Up @@ -106,6 +118,130 @@ export function addCommands(
)
});

commands.addCommand(CommandIDs.removeLayer, {
label: trans.__('Remove Layer'),
execute: () => {
const model = tracker.currentWidget?.context.model;
Private.removeSelectedItems(model, 'layer', selection => {
model?.sharedModel.removeLayer(selection);
});
}
});

commands.addCommand(CommandIDs.renameLayer, {
label: trans.__('Rename Layer'),
execute: async () => {
const model = tracker.currentWidget?.context.model;
await Private.renameSelectedItem(model, 'layer', (layerId, newName) => {
const layer = model?.getLayer(layerId);
if (layer) {
layer.name = newName;
model?.sharedModel.updateLayer(layerId, layer);
}
});
}
});

commands.addCommand(CommandIDs.removeGroup, {
label: trans.__('Remove Group'),
execute: async () => {
const model = tracker.currentWidget?.context.model;
Private.removeSelectedItems(model, 'group', selection => {
model?.removeLayerGroup(selection);
});
}
});

commands.addCommand(CommandIDs.renameGroup, {
label: trans.__('Rename Group'),
execute: async () => {
const model = tracker.currentWidget?.context.model;
await Private.renameSelectedItem(model, 'group', (groupName, newName) => {
model?.renameLayerGroup(groupName, newName);
});
}
});

commands.addCommand(CommandIDs.moveLayersToGroup, {
label: args =>
args['label'] ? (args['label'] as string) : trans.__('Move to Root'),
execute: args => {
const model = tracker.currentWidget?.context.model;
const groupName = args['label'] as string;

const selectedLayers = model?.localState?.selected?.value;

if (!selectedLayers) {
return;
}

model.moveSelectedLayersToGroup(selectedLayers, groupName);
}
});

commands.addCommand(CommandIDs.moveLayerToNewGroup, {
label: trans.__('Move Layers to New Group'),
execute: async () => {
const model = tracker.currentWidget?.context.model;
const selectedLayers = model?.localState?.selected?.value;

if (!selectedLayers) {
return;
}

function newGroupName() {
const input = document.createElement('input');
input.classList.add('jp-gis-left-panel-input');
input.style.marginLeft = '26px';
const panel = document.getElementById('jp-gis-layer-tree');
if (!panel) {
return;
}

panel.appendChild(input);
input.focus();

return new Promise<string>(resolve => {
input.addEventListener('blur', () => {
panel.removeChild(input);
resolve(input.value);
});

input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.stopPropagation();
event.preventDefault();
input.blur();
} else if (event.key === 'Escape') {
event.stopPropagation();
event.preventDefault();
input.blur();
}
});
});
}

const newName = await newGroupName();
if (!newName) {
console.warn('New name cannot be empty');
return;
}

const layers: IJGISLayerItem[] = [];

Object.keys(selectedLayers).forEach(key => {
layers.push(key);
});

const newLayerGroup: IJGISLayerGroup = {
name: newName,
layers: layers
};

model.addNewLayerGroup(selectedLayers, newLayerGroup);
}
});

commands.addCommand(CommandIDs.newGeoJSONLayer, {
label: trans.__('New vector layer'),
isEnabled: () => {
Expand Down Expand Up @@ -416,4 +552,112 @@ namespace Private {
await dialog.launch();
};
}

export async function getUserInputForRename(
text: HTMLElement,
input: HTMLInputElement,
original: string
): Promise<string> {
const parent = text.parentElement as HTMLElement;
parent.replaceChild(input, text);
input.value = original;
input.select();
input.focus();

return new Promise<string>(resolve => {
input.addEventListener('blur', () => {
parent.replaceChild(text, input);
resolve(input.value);
});

input.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.stopPropagation();
event.preventDefault();
input.blur();
} else if (event.key === 'Escape') {
event.stopPropagation();
event.preventDefault();
input.value = original;
input.blur();
text.focus();
}
});
});
}

export function removeSelectedItems(
model: IJupyterGISModel | undefined,
itemTypeToRemove: SelectionType,
removeFunction: (id: string) => void
) {
const selected = model?.localState?.selected.value;

if (!selected) {
console.info('Nothing selected');
return;
}

for (const selection in selected) {
if (selected[selection].type === itemTypeToRemove) {
removeFunction(selection);
}
}
}

export async function renameSelectedItem(
model: IJupyterGISModel | undefined,
itemType: SelectionType,
callback: (itemId: string, newName: string) => void
) {
const selectedItems = model?.localState?.selected.value;

if (!selectedItems) {
console.error(`No ${itemType} selected`);
return;
}

let itemId = '';

// If more then one item is selected, only rename the first
for (const id in selectedItems) {
if (selectedItems[id].type === itemType) {
itemId = id;
break;
}
}

if (!itemId) {
return;
}

const nodeId = selectedItems[itemId].selectedNodeId;
if (!nodeId) {
return;
}

const node = document.getElementById(nodeId);
if (!node) {
console.warn(`Node with ID ${nodeId} not found`);
return;
}

const edit = document.createElement('input');
edit.classList.add('jp-gis-left-panel-input');
const originalName = node.innerText;
const newName = await Private.getUserInputForRename(
node,
edit,
originalName
);

if (!newName) {
console.warn('New name cannot be empty');
return;
}

if (newName !== originalName) {
callback(itemId, newName);
}
}
}
2 changes: 1 addition & 1 deletion packages/base/src/mainview/mainview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ export class MainView extends React.Component<IProps, IStates> {
}
change.layerChange?.forEach(change => {
const layer = change.newValue;
if (!layer) {
if (!layer || Object.keys(layer).length === 0) {
this.removeLayer(change.id);
} else {
if (
Expand Down
Loading

0 comments on commit d98aa7d

Please sign in to comment.