Skip to content

Commit a535d3b

Browse files
Allow to paste position or link anywhere (#8652)
* allow to paste position or link anywhere * update changelog --------- Co-authored-by: Tom Herold <tom.herold@scalableminds.com>
1 parent f850a8e commit a535d3b

File tree

3 files changed

+46
-5
lines changed

3 files changed

+46
-5
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1717
- In the NML upload route, the additional form field `description` can be specified. If so, it will overwrite the description contained in the NML files. [#8631](https://github.com/scalableminds/webknossos/pull/8631)
1818
- Added the possibility for super users to retry manually cancelled jobs from the jobs list. [#8629](https://github.com/scalableminds/webknossos/pull/8629)
1919
- Added checkboxes to the segments tab that allow to show/hide individual segments. The visibility of segments that are not listed in the segments list can be controlled with a new "Hide unlisted segments" toggle in the layer settings. Additionally, you can right-click a segment in a data viewport and select "Only show this segment" (and similar functionality). [#8546](https://github.com/scalableminds/webknossos/pull/8546)
20+
- Instead of pasting a dataset position from the clipboard to the position input box, you can simply paste it without focussing the position input first. Furthermore, you can also paste a "hash string", such as `#1406,1794,1560,0,0.234,186`, as it can be found in WK URLs. Pasting such a string will also set the encoded zoom, rotation, viewport etc. Note that the `#` has to be included in the pasted text. You can also copy and paste an entire link, but note that the dataset or annotation id in the link will be ignored. [#8652](https://github.com/scalableminds/webknossos/pull/8652)
2021

2122
### Changed
2223
- Remove `data.maybe` dependency and replaced with regular Typescript types. [#8563](https://github.com/scalableminds/webknossos/pull/8563)

frontend/javascripts/viewer/controller/url_manager.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,17 @@ class UrlManager {
161161
}
162162

163163
onHashChange = () => {
164-
const urlState = this.parseUrlHash();
165-
applyState(urlState);
164+
this.updateToHash();
166165
};
167166

168-
parseUrlHash(): PartialUrlManagerState {
169-
const urlHash = decodeURIComponent(location.hash.slice(1));
167+
updateToHash(optionalHash?: string) {
168+
const urlState = this.parseUrlHash(optionalHash);
169+
applyState(urlState);
170+
}
171+
172+
parseUrlHash(optionalHash?: string): PartialUrlManagerState {
173+
// Cut off the #
174+
const urlHash = decodeURIComponent(optionalHash ?? location.hash.slice(1));
170175

171176
if (urlHash.includes("{")) {
172177
// The hash is in json format

frontend/javascripts/viewer/view/layouting/tracing_layout_view.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { Dispatch } from "redux";
1313
import { NavAndStatusBarTheme } from "theme";
1414
import type { APICompoundType } from "types/api_types";
1515
import CrossOriginApi from "viewer/api/cross_origin_api";
16-
import Constants from "viewer/constants";
16+
import Constants, { type Vector3 } from "viewer/constants";
1717
import type { ControllerStatus } from "viewer/controller";
1818
import WebKnossosController from "viewer/controller";
1919
import MergerModeController from "viewer/controller/merger_mode_controller";
@@ -24,6 +24,7 @@ import { cancelSagaAction } from "viewer/model/actions/actions";
2424
import { resetStoreAction } from "viewer/model/actions/actions";
2525
import { updateUserSettingAction } from "viewer/model/actions/settings_actions";
2626
import rootSaga from "viewer/model/sagas/root_saga";
27+
import { applyState } from "viewer/model_initialization";
2728
import { Store } from "viewer/singletons";
2829
import { Model } from "viewer/singletons";
2930
import { type Theme, type TraceOrViewCommand, type WebknossosState, startSaga } from "viewer/store";
@@ -114,9 +115,11 @@ class TracingLayoutView extends React.PureComponent<PropsWithRouter, State> {
114115

115116
componentDidMount() {
116117
startSaga(rootSaga);
118+
document.addEventListener("paste", this.onPaste);
117119
}
118120

119121
componentWillUnmount() {
122+
document.removeEventListener("paste", this.onPaste);
120123
UrlManager.stopUrlUpdater();
121124
Model.reset();
122125
destroySceneController();
@@ -261,6 +264,38 @@ class TracingLayoutView extends React.PureComponent<PropsWithRouter, State> {
261264
getLayoutNamesFromCurrentView = (layoutKey): Array<string> =>
262265
this.props.storedLayouts[layoutKey] ? Object.keys(this.props.storedLayouts[layoutKey]) : [];
263266

267+
onPaste = (event: ClipboardEvent) => {
268+
const target = event.target as HTMLElement;
269+
270+
// Check if the target is an editable input or textarea
271+
const isFormField =
272+
target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.isContentEditable;
273+
274+
if (isFormField) {
275+
// Let the browser handle the paste normally
276+
return;
277+
}
278+
279+
// Otherwise, prevent default and handle paste
280+
event.preventDefault();
281+
282+
const pastedText = event.clipboardData?.getData("text");
283+
284+
if (!pastedText) {
285+
return;
286+
}
287+
288+
const hashPos = pastedText.indexOf("#");
289+
if (hashPos > -1) {
290+
UrlManager.updateToHash(pastedText.slice(hashPos + 1));
291+
} else {
292+
const numbers = pastedText.split(",").map(Number);
293+
if (numbers.length === 3 && !numbers.some(isNaN)) {
294+
applyState({ position: numbers as Vector3 });
295+
}
296+
}
297+
};
298+
264299
render() {
265300
if (this.state.hasError) {
266301
return (

0 commit comments

Comments
 (0)