Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Context menu to layer tree items #48

Merged
merged 16 commits into from
Jul 17, 2024
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a keyboard shortcut here for delete

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(
gjmooney marked this conversation as resolved.
Show resolved Hide resolved
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
Loading