Merge branch 'tweak-property-controls' into 'v3'
Improve property control UI Closes #456 See merge request openflexure/openflexure-microscope-server!383
This commit is contained in:
commit
bb29c50032
5 changed files with 159 additions and 39 deletions
|
|
@ -6,14 +6,14 @@
|
||||||
<input
|
<input
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
@focusin="focusIn"
|
@focusin="focusIn"
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
|
@animationend="animationEnd"
|
||||||
/>
|
/>
|
||||||
<a class="button-next-to-input" @click="requestUpdate">
|
<sync-property-button @click="requestUpdate" />
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div v-if="dataType == 'boolean'" class="input-and-buttons-container">
|
<div v-if="dataType == 'boolean'" class="input-and-buttons-container">
|
||||||
|
|
@ -27,9 +27,7 @@
|
||||||
/>
|
/>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<a class="button-next-to-input" @click="requestUpdate">
|
<sync-property-button @click="requestUpdate" />
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<label v-if="dataType == 'number_array'" class="uk-form-label"
|
<label v-if="dataType == 'number_array'" class="uk-form-label"
|
||||||
>{{ label }}
|
>{{ label }}
|
||||||
|
|
@ -39,14 +37,15 @@
|
||||||
:key="i"
|
:key="i"
|
||||||
v-model="internalValue[i - 1]"
|
v-model="internalValue[i - 1]"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
|
@input="updateIsEdited"
|
||||||
@focusin="focusIn"
|
@focusin="focusIn"
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
|
@animationend="animationEnd"
|
||||||
/>
|
/>
|
||||||
<a class="button-next-to-input" @click="requestUpdate">
|
<sync-property-button @click="requestUpdate" />
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||||
|
|
@ -57,14 +56,15 @@
|
||||||
<input
|
<input
|
||||||
v-model="internalValue[key]"
|
v-model="internalValue[key]"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
type="number"
|
||||||
|
@input="updateIsEdited"
|
||||||
@focusin="focusIn"
|
@focusin="focusIn"
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
|
@animationend="animationEnd"
|
||||||
/>
|
/>
|
||||||
<a class="button-next-to-input" @click="requestUpdate">
|
<sync-property-button @click="requestUpdate" />
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -83,9 +83,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import syncPropertyButton from "./syncPropertyButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "InputFromSchema",
|
name: "InputFromSchema",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
syncPropertyButton
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
dataSchema: {
|
dataSchema: {
|
||||||
type: Object
|
type: Object
|
||||||
|
|
@ -97,19 +103,49 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: ""
|
default: ""
|
||||||
},
|
},
|
||||||
|
animate: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
internalValue: this.value,
|
// Initialise with a copy to try to prevent the this.value prop being mutated if
|
||||||
valueOnEnter: undefined,
|
// the value is an array or object. For future updates we stringify and parse
|
||||||
focused: false
|
// (see resetInternalValue). If we do this here there is a chance we get errors
|
||||||
|
// as internalValue is still null when rendering starts.
|
||||||
|
internalValue: Array.isArray(this.value)
|
||||||
|
? [...this.value]
|
||||||
|
: typeof this.value === 'object'
|
||||||
|
? { ...this.value }
|
||||||
|
: this.value,
|
||||||
|
// Is edited can't be computed as we mutate internalValue
|
||||||
|
isEdited: false,
|
||||||
|
animateUpdate: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (this.value !== undefined) {
|
||||||
|
this.resetInternalValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value(newValue) {
|
value() {
|
||||||
this.internalValue = newValue;
|
// Fire updateIsEdited on both value and internal value change,
|
||||||
|
// as change in value may not causse internalValue to change.
|
||||||
|
this.updateIsEdited();
|
||||||
|
this.resetInternalValue();
|
||||||
|
},
|
||||||
|
internalValue() {
|
||||||
|
this.updateIsEdited();
|
||||||
|
},
|
||||||
|
animate(updated) {
|
||||||
|
if (updated) {
|
||||||
|
this.animateUpdate = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -172,6 +208,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
resetInternalValue: function() {
|
||||||
|
// Whenever updatirng th internal value stringify and parse as a form of deepcopy.
|
||||||
|
// This ensure that the this.value prop is not mutated for when elements of arrays
|
||||||
|
// or objects are updated.
|
||||||
|
this.internalValue = JSON.parse(JSON.stringify(this.value));
|
||||||
|
},
|
||||||
requestUpdate: async function() {
|
requestUpdate: async function() {
|
||||||
this.$emit("requestUpdate")
|
this.$emit("requestUpdate")
|
||||||
},
|
},
|
||||||
|
|
@ -197,7 +239,32 @@ export default {
|
||||||
if (event.keyCode == 13) {
|
if (event.keyCode == 13) {
|
||||||
this.sendValue();
|
this.sendValue();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateIsEdited: function() {
|
||||||
|
this.isEdited = this.deepStringify(this.internalValue) !== this.deepStringify(this.value);
|
||||||
|
},
|
||||||
|
animationEnd: function() {
|
||||||
|
this.animateUpdate = false;
|
||||||
|
this.$emit('animationShown');
|
||||||
|
},
|
||||||
|
deepStringify: function(val) {
|
||||||
|
// Create a json string where all internal numbers are also JSON strings. This is
|
||||||
|
// needed to robustly check if the value is updated because the raw value may be a
|
||||||
|
// number but anything typed in the input is a string. In the case of arrays or
|
||||||
|
// objects even with JSON.stringify we end up comparing ["1", 3] with [1, 3] and
|
||||||
|
// find them as not equal.
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
return JSON.stringify(val.map(String));
|
||||||
|
}
|
||||||
|
if (val && typeof val === 'object') {
|
||||||
|
const normalized = Object.fromEntries(
|
||||||
|
Object.entries(val).map(([k, v]) => [k, String(v)])
|
||||||
|
);
|
||||||
|
return JSON.stringify(normalized);
|
||||||
|
}
|
||||||
|
return JSON.stringify(String(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -217,11 +284,16 @@ export default {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
}
|
}
|
||||||
.button-next-to-input {
|
.edited {
|
||||||
flex-grow: 0;
|
background-color: #fff3cd;
|
||||||
padding-left: 5px;
|
}
|
||||||
padding-right: 5px;
|
@keyframes green-flash {
|
||||||
vertical-align: middle;
|
0% { background-color: #3fda63; }
|
||||||
cursor: pointer;
|
100% { background-color: white; }
|
||||||
|
}
|
||||||
|
.flash {
|
||||||
|
animation: green-flash 0.7s ease;
|
||||||
|
/*Without this background-colour chrome will ignore the animation colour.*/
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
v-model="value"
|
v-model="value"
|
||||||
:data-schema="propertyDescription"
|
:data-schema="propertyDescription"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:animate="animate"
|
||||||
@requestUpdate="readProperty"
|
@requestUpdate="readProperty"
|
||||||
@sendValue="writeProperty"
|
@sendValue="writeProperty"
|
||||||
|
@animationShown="resetAnimate"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -45,7 +47,8 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: undefined
|
value: undefined,
|
||||||
|
animate: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -98,18 +101,25 @@ export default {
|
||||||
await new Promise(r => setTimeout(r, this.readBackDelay));
|
await new Promise(r => setTimeout(r, this.readBackDelay));
|
||||||
let newVal = await this.readProperty();
|
let newVal = await this.readProperty();
|
||||||
if (newVal == requestedValue) {
|
if (newVal == requestedValue) {
|
||||||
await this.modalNotify(`Set ${this.label} to ${newVal}.`);
|
this.animate = true;
|
||||||
} else {
|
} else {
|
||||||
|
this.animate = true;
|
||||||
await this.modalNotify(
|
await this.modalNotify(
|
||||||
`Set ${this.label} to ${newVal} (requested ${requestedValue}).`
|
`Set ${this.label} to ${newVal} (closest valid value to requested ${requestedValue}).`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
|
this.animate = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.modalError(error); // Let mixin handle error
|
// Use mixin to display error
|
||||||
|
this.modalError(error);
|
||||||
|
// Re-read property to try to update to server value
|
||||||
|
this.readProperty();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
resetAnimate: function() {
|
||||||
|
this.animate = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<a class="sync-button">
|
||||||
|
<span
|
||||||
|
class="material-symbols-outlined sync-icon"
|
||||||
|
title="Reread value from microscope. Required if microscope is updated externally"
|
||||||
|
>
|
||||||
|
sync
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "syncPropertyButton"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sync-button {
|
||||||
|
flex-grow: 0;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined.sync-icon{
|
||||||
|
color: #888;
|
||||||
|
transition: transform 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined.sync-icon:hover{
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
color: #c5247f;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -10,8 +10,10 @@
|
||||||
v-model="backgroundDetectorStatus.settings"
|
v-model="backgroundDetectorStatus.settings"
|
||||||
:data-schema="backgroundDetectorStatus.settings_schema"
|
:data-schema="backgroundDetectorStatus.settings_schema"
|
||||||
label=""
|
label=""
|
||||||
|
:animate="animate"
|
||||||
@requestUpdate="readSettings"
|
@requestUpdate="readSettings"
|
||||||
@sendValue="writeSettings"
|
@sendValue="writeSettings"
|
||||||
|
@animationShown="resetAnimate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -57,6 +59,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
backgroundDetectorStatus: undefined,
|
backgroundDetectorStatus: undefined,
|
||||||
|
animate: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -87,6 +90,11 @@ export default {
|
||||||
"update_detector_settings",
|
"update_detector_settings",
|
||||||
{"data": requestedValue}
|
{"data": requestedValue}
|
||||||
);
|
);
|
||||||
|
this.animate = true;
|
||||||
|
this.readSettings();
|
||||||
|
},
|
||||||
|
resetAnimate: function() {
|
||||||
|
this.animate = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
|
|
||||||
|
|
@ -85,9 +85,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
@keyup.enter="startMoveTask"
|
@keyup.enter="startMoveTask"
|
||||||
/>
|
/>
|
||||||
<a class="button-next-to-input" @click="updatePosition">
|
<sync-property-button @click="updatePosition" />
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<action-button
|
<action-button
|
||||||
|
|
@ -177,13 +175,15 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "PaneControl",
|
name: "PaneControl",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ActionButton
|
ActionButton,
|
||||||
|
syncPropertyButton
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
|
|
@ -375,11 +375,4 @@ export default {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.button-next-to-input {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue