openflexure-microscope-server/openflexure_microscope/api/static/src/App.vue
Richard Bowman afbb71607d Fix placement of tour messages for icons
The tour pop-ups were getting stuck in the tab icon container.
Explicitly specifying that they could overflow said container,
and setting them to show to the right, seems to have fixed this issue.
2021-02-06 15:32:27 +00:00

476 lines
12 KiB
Vue

<template>
<div
id="app"
class="uk-height-1-1 uk-margin-remove uk-padding-remove"
:class="handleTheme"
>
<loadingContent v-if="!$store.getters.ready" />
<div v-if="$store.getters.ready" id="tour-header"></div>
<appContent v-if="$store.getters.ready" />
<!-- Runtime modals -->
<div
id="modal-center"
ref="keyboardManualModal"
class="uk-flex-top"
uk-modal
>
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div
v-for="shortcut in keyboardManual"
:key="shortcut.shortcut"
class="uk-margin-small"
uk-grid
>
<div class="uk-width-small">{{ shortcut.shortcut }}</div>
<div class="uk-width-expand">{{ shortcut.description }}</div>
</div>
</div>
</div>
<v-tour
v-show="$store.getters.ready"
name="guidedTour"
:steps="tourSteps"
:callbacks="tourCallbacks"
:options="{ highlight: true }"
></v-tour>
</div>
</template>
<script>
// Import components
import appContent from "./components/appContent.vue";
import loadingContent from "./components/loadingContent.vue";
import axios from "axios";
var Mousetrap = require("mousetrap");
Mousetrap.prototype.stopCallback = function(e, element) {
// if the element has the class "mousetrap" then no need to stop
if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) {
return false;
}
// if we're in a lightbox, stop mousetrap
if (element.classList.contains("lightbox-link")) {
return true;
}
// stop for input, select, and textarea
return (
element.tagName == "INPUT" ||
element.tagName == "SELECT" ||
element.tagName == "TEXTAREA" ||
(element.contentEditable && element.contentEditable == "true")
);
};
// Export main app
export default {
name: "App",
components: {
appContent,
loadingContent
},
data: function() {
return {
appAvailable: false,
arrowKeysDown: {},
keyboardManual: [],
systemDark: undefined,
themeObserver: undefined,
tourCallbacks: {
onStop: () => {
this.setLocalStorageObj("completedTour", true);
}
}
};
},
computed: {
isSystemDark: function() {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return true;
} else {
return false;
}
},
handleTheme: function() {
var isDark = false;
if (this.$store.state.appTheme == "dark") {
isDark = true;
} else if (this.$store.state.appTheme == "system") {
if (this.systemDark) {
isDark = true;
}
}
return {
"uk-light": isDark,
"uk-background-secondary": isDark
};
},
tourSteps: function() {
let tabPopperParams = {
modifiers: {
preventOverflow: {
boundariesElement: "window"
}
},
placement: "right"
};
return [
{
target: "#tour-header", // We're using document.querySelector() under the hood
header: {
title: "Welcome to the OpenFlexure Microscope"
},
content: `Click Next to learn how to use your microscope`,
params: {
placement: "bottom"
}
},
{
target: "#gallery-tab-icon",
header: {
title: "Capture gallery"
},
content: `View and download your microscope images from the gallery tab`,
params: tabPopperParams
},
{
target: "#navigate-tab-icon",
header: {
title: "Navigate around your sample"
},
content: `Move your microscope stage and perform autofocus from the navigate tab`,
params: tabPopperParams
},
{
target: "#capture-tab-icon",
header: {
title: "Capture microscope images"
},
content: `Take images and simple tile scans from the capture tab`,
params: tabPopperParams
},
{
target: "#settings-tab-icon",
header: {
title: "Change settings"
},
content: `Change app and microscope settings, including microscope calibration, from the settings tab`,
params: tabPopperParams
},
{
target: "#extension-tab-divider",
header: {
title: "Microscope extensions"
},
content: `Extensions installed on your microscope will appear below this line`,
params: tabPopperParams
}
];
}
},
mounted() {
// Query CSS dark theme preference
var mql = window.matchMedia("(prefers-color-scheme: dark)");
// Check for system dark theme when mounted
if (mql.matches) {
this.systemDark = true;
}
// Create a theme observer to watch for changes
this.themeObserver = mql.addListener(e => {
if (e.matches) {
this.systemDark = true;
} else {
this.systemDark = false;
}
});
// Check connection to API
this.checkConnection();
// Handle guided tour
// If the user has already completed or skipped the guided tour
var completedTour = this.getLocalStorageObj("completedTour") || false;
if (!completedTour) {
this.$tours["guidedTour"].start();
}
},
created: function() {
window.addEventListener("beforeunload", this.handleExit);
// Scrollwheel listener
window.addEventListener("wheel", this.wheelMonitor);
// Watch for origin changes
this.unwatchOriginFunction = this.$store.watch(
(state, getters) => {
return getters.uriV2;
},
() => {
this.checkConnection();
}
);
// Keyboard shortcuts
Mousetrap.bind("?", () => {
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin
});
// Arrow keys
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
this.arrowKeysDown[event.keyCode] = true; //Add key to array
this.navigateKeyHandler();
},
"keydown"
);
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
delete this.arrowKeysDown[event.keyCode]; //Remove key from array
},
"keyup"
);
this.keyboardManual.push({
shortcut: "←↑→↓",
description: "Move the microscope stage"
});
// Focus keys
Mousetrap.bind("pageup", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, 1);
});
Mousetrap.bind("pagedown", () => {
this.$root.$emit("globalMoveStepEvent", 0, 0, -1);
});
this.keyboardManual.push({
shortcut: "pgup / pgdn",
description: "Move the microscope focus"
});
// Capture
Mousetrap.bind("c", () => {
this.$root.$emit("globalCaptureEvent");
});
this.keyboardManual.push({
shortcut: "c",
description: "Take a capture"
});
// Autofocus
Mousetrap.bind("a", () => {
this.$root.$emit("globalFastAutofocusEvent");
});
this.keyboardManual.push({
shortcut: "a",
description: "Fast autofocus"
});
// Increment/decrement tab
Mousetrap.bind("shift+down", () => {
this.$root.$emit("globalIncrementTab");
});
Mousetrap.bind("shift+up", () => {
this.$root.$emit("globalDecrementTab");
});
this.keyboardManual.push({
shortcut: "shift+↑ / shift+↓",
description: "Switch tab"
});
// Re-run tour
Mousetrap.bind("alt+t", () => {
this.$tours["guidedTour"].start();
});
},
beforeDestroy: function() {
// Disconnect the theme observer
if (this.themeObserver) {
this.themeObserver.disconnect();
}
// Remove scrollwheel listener
window.removeEventListener("wheel", this.wheelMonitor);
// Remove origin watcher
this.unwatchOriginFunction();
// Remove key listeners
Mousetrap.reset();
},
methods: {
checkConnection: function() {
var uriV2 = this.$store.getters.uriV2;
this.$store.commit("changeWaiting", true);
axios
.get(uriV2)
.then(() => {
this.$store.commit("setConnected");
this.$store.commit("setErrorMessage", null);
})
.catch(error => {
this.$store.commit("setErrorMessage", error);
})
.finally(() => {
this.$store.commit("changeWaiting", false);
});
},
handleExit: function() {
this.$root.$emit("globalTogglePreview", false);
},
// Handle global mouse wheel events to be associated with navigation
wheelMonitor: function(event) {
// Only capture scroll if the event target's parent contains the "scrollTarget" class
if (
event.target.parentNode.classList.contains("scrollTarget") ||
event.target.classList.contains("scrollTarget")
) {
var z_rel = event.deltaY / 100;
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit("globalMoveStepEvent", 0, 0, z_rel, false);
}
},
navigateKeyHandler: function() {
// Calculate movement array
var x_rel = 0;
var y_rel = 0;
var z_rel = 0;
if (37 in this.arrowKeysDown) {
x_rel = x_rel + 1;
}
if (39 in this.arrowKeysDown) {
x_rel = x_rel - 1;
}
if (38 in this.arrowKeysDown) {
y_rel = y_rel + 1;
}
if (40 in this.arrowKeysDown) {
y_rel = y_rel - 1;
}
// Make a position request
// Emit a signal to move, acted on by panelNavigate.vue
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel);
}
}
};
</script>
<style lang="less">
// Basic UIkit CSS
@import "../node_modules/uikit/src/less/uikit.less";
// Custom UIkit CSS modifications
@import "./assets/less/theme.less";
// We override the custom-electron-titlebar z-index
// UIKit lightbox must be able to draw over the titlebar
// as it currently always spawns at the root of the DOM
.titlebar,
.titlebar > * {
z-index: 1000 !important;
}
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
height: 100%;
}
body,
html {
height: 100%;
overflow: hidden;
}
.uk-disabled {
pointer-events: none;
opacity: 0.4;
}
.control-component {
overflow-y: auto;
overflow-x: hidden;
width: 300px;
height: 100%;
padding: 0;
background-color: rgba(180, 180, 180, 0.03);
border-width: 0 1px 0 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.view-component {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
padding: 0;
}
.section-content {
padding: 0;
height: 100%;
}
// Style tour
.v-tour__target--highlighted {
box-shadow: 0px 40px 200px 30px rgba(0, 0, 0, 0.5),
0px 0px 0px 4px rgba(128, 128, 128, 0.5) !important;
border-radius: 5px;
opacity: 100% !important;
pointer-events: none !important;
}
.v-step {
background: @global-primary-background !important;
}
.v-step__header {
background-color: darken(@global-primary-background, 7%) !important;
}
.v-step__button {
font-size: 0.9rem !important;
}
// Change step arrow colour
// This is awful and hacky and makes me sad, but needs must
.v-step .v-step__arrow {
border-color: darken(@global-primary-background, 7%) !important;
&--dark {
border-color: darken(@global-primary-background, 7%) !important;
}
}
.v-step[x-placement^="top"] .v-step__arrow {
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-step[x-placement^="bottom"] .v-step__arrow {
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
}
.v-step[x-placement^="right"] .v-step__arrow {
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
}
.v-step[x-placement^="left"] .v-step__arrow {
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
}
</style>