diff --git a/.gitignore b/.gitignore
index d9ddff83..e71649d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ binaries/
coverage/
dist/
jsdoc/
+.vscode
diff --git a/package-lock.json b/package-lock.json
index ade5e7f1..4c5020db 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "odin",
- "version": "2.7.0",
+ "version": "2.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "odin",
- "version": "2.7.0",
+ "version": "2.7.1",
"hasInstallScript": true,
"license": "AGPLv3",
"dependencies": {
@@ -41,6 +41,7 @@
"react-dom": "^18.2.0",
"react-easy-sort": "^1.5.1",
"react-fast-compare": "^3.2.0",
+ "react-tooltip": "^5.21.3",
"reproject": "^1.2.7",
"sanitize-filename": "^1.6.3",
"subleveldown": "^6.0.1",
@@ -2156,6 +2157,28 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz",
+ "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.1.1"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.2.tgz",
+ "integrity": "sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog==",
+ "dependencies": {
+ "@floating-ui/core": "^1.4.1",
+ "@floating-ui/utils": "^0.1.1"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.2.tgz",
+ "integrity": "sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ=="
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -4722,6 +4745,11 @@
"node": ">=8"
}
},
+ "node_modules/classnames": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+ "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+ },
"node_modules/clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
@@ -11977,6 +12005,19 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-tooltip": {
+ "version": "5.21.3",
+ "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.21.3.tgz",
+ "integrity": "sha512-z3Q+Uka4D6uYxfsssPqfx1W8vw7NIHyC2ZMq+NJkWg4EpUD3w7Fwz/o+dezyUQMCHL7nO/2sFbtWIrkyxktq2Q==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0",
+ "classnames": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.14.0",
+ "react-dom": ">=16.14.0"
+ }
+ },
"node_modules/read-config-file": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
diff --git a/package.json b/package.json
index 0af8c515..8580c1c4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "odin",
"productName": "ODINv2",
- "version": "2.7.0",
+ "version": "2.7.1",
"description": "Open Source Command and Control Information System (C2IS)",
"main": "dist/main.js",
"scripts": {
@@ -88,6 +88,7 @@
"react-dom": "^18.2.0",
"react-easy-sort": "^1.5.1",
"react-fast-compare": "^3.2.0",
+ "react-tooltip": "^5.21.3",
"reproject": "^1.2.7",
"sanitize-filename": "^1.6.3",
"subleveldown": "^6.0.1",
diff --git a/src/renderer/components/DropdownMenu.js b/src/renderer/components/DropdownMenu.js
index 13ed515f..e1f161b1 100644
--- a/src/renderer/components/DropdownMenu.js
+++ b/src/renderer/components/DropdownMenu.js
@@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
+import { Tooltip } from 'react-tooltip'
import * as mdi from '@mdi/js'
import Icon from '@mdi/react'
import uuid from 'uuid-random'
@@ -8,14 +9,19 @@ import './DropdownMenu.css'
export const DropdownMenu = props => {
const [id] = React.useState(uuid())
+ const [collapsed, setCollapsed] = React.useState(true)
const handleClick = () => {
document.getElementById(id).classList.toggle('show')
+ setCollapsed(current => !current)
}
const handleBlur = () => {
// Let brief background flashing show on option selection (if any).
- const hide = () => document.getElementById(id).classList.remove('show')
+ const hide = () => {
+ document.getElementById(id).classList.remove('show')
+ setCollapsed(true)
+ }
setTimeout(hide, 200)
}
@@ -27,7 +33,8 @@ export const DropdownMenu = props => {
}
return (
-
+ <>
+
+ { collapsed && }
+ >
)
}
DropdownMenu.propTypes = {
path: PropTypes.string.isRequired,
- options: PropTypes.array.isRequired
+ options: PropTypes.array.isRequired,
+ toolTip: PropTypes.string
}
diff --git a/src/renderer/components/Toolbar.js b/src/renderer/components/Toolbar.js
index 59e118ca..a1295ce1 100644
--- a/src/renderer/components/Toolbar.js
+++ b/src/renderer/components/Toolbar.js
@@ -54,7 +54,7 @@ export const Toolbar = () => {
return (
-
+
{
commands.map(([key, command]) => {
return command === 'separator'
@@ -62,18 +62,20 @@ export const Toolbar = () => {
:
})
}
-
+
diff --git a/src/renderer/components/ToolbarButtons.js b/src/renderer/components/ToolbarButtons.js
index f61bee59..5df577cb 100644
--- a/src/renderer/components/ToolbarButtons.js
+++ b/src/renderer/components/ToolbarButtons.js
@@ -1,28 +1,35 @@
import React from 'react'
import PropTypes from 'prop-types'
+import { Tooltip } from 'react-tooltip'
import Icon from '@mdi/react'
import * as mdi from '@mdi/js'
import './Toolbar.css'
+
export const SimpleButton = props => {
const className = props.checked
? 'toolbar__button toolbar__button--checked'
: 'toolbar__button'
return (
+ <>
+
+ >
)
}
SimpleButton.propTypes = {
onClick: PropTypes.func.isRequired,
path: PropTypes.string.isRequired,
- checked: PropTypes.bool.isRequired
+ checked: PropTypes.bool.isRequired,
+ toolTip: PropTypes.string
}
@@ -38,13 +45,17 @@ export const CommandButton = props => {
}, [command])
return (
+ <>
+
+ >
)
}
diff --git a/src/renderer/components/properties/Coordinates.js b/src/renderer/components/properties/Coordinates.js
index 9c844ee8..3c9e21fc 100644
--- a/src/renderer/components/properties/Coordinates.js
+++ b/src/renderer/components/properties/Coordinates.js
@@ -5,6 +5,7 @@ import * as mdi from '@mdi/js'
import ColSpan2 from './ColSpan2'
import FlexRow from './FlexRow'
import textProperty from './textProperty'
+import { Tooltip } from 'react-tooltip'
import { useServices, useMemento } from '../hooks'
const Button = props => {
@@ -36,6 +37,7 @@ const TextProperty = props => {
feature.geometry.coordinates = coordinates
} catch (err) {
/* TODO: handle parse error */
+ console.error(err)
}
return feature
@@ -50,9 +52,10 @@ const Coordinates = props => {
-
-
+
+
+
)
diff --git a/src/renderer/components/properties/TilePresetProperties.js b/src/renderer/components/properties/TilePresetProperties.js
index c695259a..faecf58c 100644
--- a/src/renderer/components/properties/TilePresetProperties.js
+++ b/src/renderer/components/properties/TilePresetProperties.js
@@ -2,9 +2,10 @@
import React from 'react'
import SortableList, { SortableItem } from 'react-easy-sort'
import Icon from '@mdi/react'
-import { mdiDrag, mdiEye, mdiEyeOff } from '@mdi/js'
+import { mdiDrag, mdiEye, mdiEyeOff, mdiOpacity } from '@mdi/js'
import { useServices, useList } from '../hooks'
import Range from './Range'
+import { Tooltip } from 'react-tooltip'
import './TilePresetProperties.scss'
@@ -46,19 +47,28 @@ const Layer = props => (
{/* react-easy-sort kills list style => add necessary margins. */}
-
+
{props.name}
+
+
+
+
{
: mdi.mdiPinOutline
const renameButton = (capabilities || '').includes('RENAME')
- ? emitter.emit('edit/begin', { id })}>
-
-
+ ? <>
+ emitter.emit('edit/begin', { id })}>
+
+
+
+ >
: null
return (
@@ -213,8 +217,9 @@ export const Card = React.forwardRef((props, ref) => {
/>
{ renameButton }
emitter.emit(pinned ? 'unpin' : 'pin', { id })}>
-
+
+
{ children.body }
diff --git a/src/renderer/components/sidebar/FilterInput.js b/src/renderer/components/sidebar/FilterInput.js
index 62ddd5e4..13f6f0b7 100644
--- a/src/renderer/components/sidebar/FilterInput.js
+++ b/src/renderer/components/sidebar/FilterInput.js
@@ -5,6 +5,7 @@ import { defaultSearch } from './state'
import { matcher, stopPropagation } from '../events'
import { cmdOrCtrl } from '../../platform'
import { preventDefault } from 'ol/events/Event'
+import { Tooltip } from 'react-tooltip'
import './FilterInput.scss'
@@ -69,7 +70,14 @@ export const FilterInput = props => {
onChange={handleChange}
onKeyDown={handleKeyDown}
onClick={stopPropagation}
+ id='filter-input'
/>
+
+
+ You can search for multiple phrases or #tags
+ and also combine them.
+
+
)
}
diff --git a/src/renderer/components/sidebar/ScopeSwitcher.js b/src/renderer/components/sidebar/ScopeSwitcher.js
index 52c76a18..10c56c2c 100644
--- a/src/renderer/components/sidebar/ScopeSwitcher.js
+++ b/src/renderer/components/sidebar/ScopeSwitcher.js
@@ -6,6 +6,7 @@ import { useMemento } from '../hooks'
import { defaultSearch } from './state'
import * as ID from '../../ids'
import { IconTag } from './IconTag'
+import { Tooltip } from 'react-tooltip'
/**
@@ -37,13 +38,17 @@ const ScopeSwitch = props => {
{props.name}
{props.label}
- : {props.label}
+ : <>
+ {props.label}
+
+ >
}
ScopeSwitch.propTypes = {
name: PropTypes.string,
label: PropTypes.string.isRequired,
- scope: PropTypes.string.isRequired
+ scope: PropTypes.string.isRequired,
+ toolTip: PropTypes.string
}
@@ -76,11 +81,25 @@ export const ScopeSwitcher = props => {
[`@${ID.MEASURE}`]: 'measure'
}
+ const TOOLTIPS = {
+ '#pin': 'Manage pinned items',
+ [`@${ID.LAYER}`]: 'Manage existing layers',
+ [`@${ID.FEATURE}`]: 'Manage existing features',
+ [`@${ID.LINK}`]: 'Manage existing links',
+ [`@${ID.SYMBOL}`]: 'Create new features based on the symbol palette',
+ [`@${ID.MARKER}`]: 'Manage existing markers',
+ [`@${ID.BOOKMARK}`]: 'Manage existing bookmarks',
+ [`@${ID.PLACE}`]: 'Search for addresses based on OSM (online only)',
+ [`@${ID.TILE_SERVICE}`]: 'Manage existing tile services for maps',
+ [`@${ID.MEASURE}`]: 'Manage existing measurements'
+ }
+
const defaultSwitches = Object.entries(SCOPES).map(([scope, label]) =>
)
@@ -94,10 +113,13 @@ export const ScopeSwitcher = props => {
)
const back = history.length > 1
- ?
+
+ >
: null
return (
diff --git a/src/renderer/components/sidebar/Sidebar.js b/src/renderer/components/sidebar/Sidebar.js
index 5e8bd8ed..1c0888f5 100644
--- a/src/renderer/components/sidebar/Sidebar.js
+++ b/src/renderer/components/sidebar/Sidebar.js
@@ -11,6 +11,7 @@ import { ScopeSwitcher } from './ScopeSwitcher'
import { MemoizedCard } from './Card'
import { LazyList } from './LazyList'
import { FilterInput } from './FilterInput'
+import { Tooltip } from 'react-tooltip'
import './Sidebar.scss'
@@ -284,6 +285,27 @@ export const Sidebar = () => {
focusIndex={state.focusIndex}
renderEntry={renderEntry}
/>
+ {
+ /*
+ Unfortunately we have elements that are addressed by both id and class selectors. In order
+ to avoid showing wrong tooltips we do not show a tt if the element has an id.
+ */
+ if (activeAnchor?.id) return null
+
+ const toggler = ['VISIBLE', 'HIDDEN', 'LOCKED', 'UNLOCKED']
+ const highlighter = ['LAYER', 'FEATURE', 'MARKER', 'PLACE']
+ const linker = ['LINK']
+
+ if (toggler.includes(activeAnchor?.innerText)) return 'Click to toggle value'
+ if (highlighter.includes(activeAnchor?.innerText)) return 'Click and hold to highlight'
+ if (linker.includes(activeAnchor?.innerText)) return 'Show assigned links'
+ return ''
+ }} />
+ {
+ if (activeAnchor?.id) return null
+ return 'Show content'
+ }} />
+
)
}
diff --git a/src/renderer/model/commands/ClipboardCommands.js b/src/renderer/model/commands/ClipboardCommands.js
index 0f63aafc..481d8843 100644
--- a/src/renderer/model/commands/ClipboardCommands.js
+++ b/src/renderer/model/commands/ClipboardCommands.js
@@ -1,9 +1,9 @@
export default services => {
const { clipboard } = services
return {
- CLIPBOARD_CUT: { path: 'mdiContentCut', execute: () => clipboard.cut() },
- CLIPBOARD_COPY: { path: 'mdiContentCopy', execute: () => clipboard.copy() },
- CLIPBOARD_PASTE: { path: 'mdiContentPaste', execute: () => clipboard.paste() },
- CLIPBOARD_DELETE: { path: 'mdiTrashCanOutline', execute: () => clipboard.delete() }
+ CLIPBOARD_CUT: { path: 'mdiContentCut', execute: () => clipboard.cut(), toolTip: 'Cut' },
+ CLIPBOARD_COPY: { path: 'mdiContentCopy', execute: () => clipboard.copy(), toolTip: 'Copy' },
+ CLIPBOARD_PASTE: { path: 'mdiContentPaste', execute: () => clipboard.paste(), toolTip: 'Paste' },
+ CLIPBOARD_DELETE: { path: 'mdiTrashCanOutline', execute: () => clipboard.delete(), toolTip: 'Delete' }
}
}
diff --git a/src/renderer/model/commands/GlobalCommands.js b/src/renderer/model/commands/GlobalCommands.js
index 0daf8a3d..99a25205 100644
--- a/src/renderer/model/commands/GlobalCommands.js
+++ b/src/renderer/model/commands/GlobalCommands.js
@@ -5,6 +5,7 @@ const Pin = function (services) {
this.store = services.store
this.selection = services.selection
this.path = 'mdiPinOutline'
+ this.toolTip = 'Add this to the pinned items'
this.selection.on('selection', () => this.emit('changed'))
}
diff --git a/src/renderer/model/commands/LayerCommands.js b/src/renderer/model/commands/LayerCommands.js
index df13ee2e..cca2f0a0 100644
--- a/src/renderer/model/commands/LayerCommands.js
+++ b/src/renderer/model/commands/LayerCommands.js
@@ -11,6 +11,7 @@ const SetDefaultLayer = function (services) {
this.selection = services.selection
this.store = services.store
this.path = 'mdiCreation'
+ this.toolTip = 'Make the selected layer the default layer'
this.selection.on('selection', () => this.emit('changed'))
}
@@ -34,6 +35,7 @@ const SelectTilePreset = function (services) {
this.preferencesStore = services.preferencesStore
this.store = services.store
this.tileLayerStore = services.tileLayerStore
+ this.toolTip = 'Change the visibility of the background maps'
this.path = 'mdiMap'
}
@@ -47,6 +49,7 @@ const ExportLayer = function (services) {
this.store = services.store
this.clipboard = services.clipboard
this.path = 'mdiExport'
+ this.toolTip = 'Export selected layer to the filesystem'
this.selection.on('selection', () => this.emit('changed'))
}
diff --git a/src/renderer/model/commands/PrintCommands.js b/src/renderer/model/commands/PrintCommands.js
index 5aa07dc6..c465a8f5 100644
--- a/src/renderer/model/commands/PrintCommands.js
+++ b/src/renderer/model/commands/PrintCommands.js
@@ -4,11 +4,13 @@ import EventEmitter from '../../../shared/emitter'
const state = [
{
event: 'TOOLBAR_SCOPE/PRINT',
- path: 'mdiPrinterOutline'
+ path: 'mdiPrinterOutline',
+ toolTip: 'Print ...'
},
{
event: 'TOOLBAR_SCOPE/STANDARD',
- path: 'mdiExitToApp'
+ path: 'mdiExitToApp',
+ toolTip: 'Return to the main application view'
}
]
@@ -30,13 +32,21 @@ PrintSwitchScopeCommand.prototype.enabled = function () { return true }
const PrintCommand = function (services) {
this.emitter = services.emitter
this.path = 'mdiPrinter'
+ this.toolTip = 'Print this view now!'
}
PrintCommand.prototype.execute = function () {
this.emitter.emit('PRINT')
}
+const buildScopeCommand = (services) => {
+ const command = new PrintSwitchScopeCommand(services)
+ Object.defineProperty(command, 'path', { get () { return state[this.currentState].path } })
+ Object.defineProperty(command, 'toolTip', { get () { return state[this.currentState].toolTip } })
+ return command
+}
+
export default services => ({
- PRINT_SWITCH_SCOPE: Object.defineProperty(new PrintSwitchScopeCommand(services), 'path', { get () { return state[this.currentState].path } }),
+ PRINT_SWITCH_SCOPE: buildScopeCommand(services),
PRINT_MAP: new PrintCommand(services)
})
diff --git a/src/renderer/model/commands/UndoCommands.js b/src/renderer/model/commands/UndoCommands.js
index 2367702b..24a5ad4a 100644
--- a/src/renderer/model/commands/UndoCommands.js
+++ b/src/renderer/model/commands/UndoCommands.js
@@ -4,6 +4,7 @@ import EventEmitter from '../../../shared/emitter'
const Undo = function (services) {
this.undo = services.undo
this.path = 'mdiUndo'
+ this.toolTip = 'Undo'
this.undo.on('changed', () => this.emit('changed'))
}
@@ -15,6 +16,7 @@ Undo.prototype.enabled = function () { return this.undo.canUndo() }
const Redo = function (services) {
this.undo = services.undo
this.path = 'mdiRedo'
+ this.toolTip = 'Redo'
this.undo.on('changed', () => this.emit('changed'))
}
diff --git a/src/renderer/store/options/marker.js b/src/renderer/store/options/marker.js
index ca54e914..65e7f149 100644
--- a/src/renderer/store/options/marker.js
+++ b/src/renderer/store/options/marker.js
@@ -15,7 +15,7 @@ export default async function (id) {
title: marker.name,
description,
tags: [
- 'SCOPE:MARKER:NONE',
+ 'SCOPE:MARKER',
hidden ? 'SYSTEM:HIDDEN' : 'SYSTEM:VISIBLE',
locked ? 'SYSTEM:LOCKED' : 'SYSTEM:UNLOCKED',
...((tags || [])).map(label => `USER:${label}:NONE`),
diff --git a/src/renderer/store/options/place.js b/src/renderer/store/options/place.js
index 8692100c..b84bf077 100644
--- a/src/renderer/store/options/place.js
+++ b/src/renderer/store/options/place.js
@@ -27,7 +27,7 @@ export default async function (id) {
sort: `${distance}`, // should be string because of Intl.Collator.compare()
description: `${place.description} - ${formattedDistance} km`,
tags: [
- 'SCOPE:PLACE:NONE',
+ 'SCOPE:PLACE',
...systemTags,
...(userTags || []).map(label => `USER:${label}:NONE`),
'PLUS'