Merge branch 'reset-to-default' into 'v3'

Reset to default option for properties

Closes #574

See merge request openflexure/openflexure-microscope-server!640
This commit is contained in:
Joe Knapper 2026-07-02 09:40:08 +00:00
commit cae5ffa29b
7 changed files with 317 additions and 66 deletions

View file

@ -15,6 +15,8 @@
@darkened-primary-color: darken(@global-primary-background, 15%); @darkened-primary-color: darken(@global-primary-background, 15%);
@inverse-global-color: fade(@global-inverse-color, 80%); @inverse-global-color: fade(@global-inverse-color, 80%);
@very-light-primary-color: lighten(@global-primary-background, 40%);
@global-border: #d5d5d5; @global-border: #d5d5d5;
// UIkit // UIkit
@ -265,6 +267,32 @@
} }
} }
/*
* Text
*/
// A text label for an input control that is highlighted
.ofm-highlighted-label {
color: @darkened-primary-color;
// 500 is slightly more bold than normal text (400) and less than semi-bold (600)
font-weight: 500;
}
// Reducing the ui-kit margins from 20px to 5px
.ofm-close-pars p{
margin-bottom: 5px;
}
// Use CSS "next-sibling combinator" to get p-tags following another tag at the same level.
.ofm-close-pars * + p{
margin-top: 5px;
}
.hook-inverse() {
.ofm-highlighted-label{
color: @very-light-primary-color;
}
}
/* /*
* Links * Links
@ -277,11 +305,29 @@
} }
.uk-alert-success{ .uk-alert-success{
color: rgba(28, 131, 45); color: rgb(28, 131, 45);
background-color: rgba(130, 221, 145, 0.671); background-color: rgba(130, 221, 145, 0.671);
} }
} }
// For links that shouldn't look styled like links (such as in menus)
.ofm-unstyled-link {
text-decoration: none;
cursor: pointer;
color: @global-color;
}
.ofm-unstyled-link.disabled{
cursor: default;
color: @global-muted-color;
}
.hook-inverse() {
.ofm-unstyled-link{
color: @inverse-global-color;
}
}
/* /*
* Buttons * Buttons
*/ */

View file

@ -0,0 +1,74 @@
<template>
<a class="sync-button" :class="{ disabled: disabled }" @click.prevent="$emit('click')">
<span class="material-symbols-outlined sync-icon">
{{ icon }}
</span>
<span v-if="showLabel" class="icon-label ofm-unstyled-link" :class="{ disabled: disabled }">
{{ label }}
</span>
</a>
</template>
<script>
export default {
name: "IconButton",
props: {
icon: {
type: String,
default: "sync_alt",
},
showLabel: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ["click"],
};
</script>
<style scoped>
.sync-button {
display: inline-flex;
flex-grow: 0;
padding-left: 5px;
padding-right: 5px;
vertical-align: middle;
cursor: pointer;
user-select: none;
text-decoration: none !important;
}
.sync-button.disabled {
cursor: default;
}
.sync-button .material-symbols-outlined.sync-icon {
color: #888;
transition: color 0.3s ease;
}
.sync-button.disabled .material-symbols-outlined.sync-icon {
color: #aaa;
transition: color 0.3s ease;
}
.sync-button:not(.disabled):hover .material-symbols-outlined.sync-icon {
color: #c5247f;
text-decoration: none !important;
}
.icon-label {
padding-left: 10px;
padding-top: 3px;
}
</style>

View file

@ -0,0 +1,73 @@
<template>
<div ref="mini-menu" class="mini-menu">
<a class="icon" @click.prevent="isOpen = !isOpen">
<span class="material-symbols-outlined"> more_vert </span>
</a>
<div v-if="isOpen" class="dropdown-menu ofm-close-pars ofm-opaque-element">
<slot :close="close"></slot>
</div>
</div>
</template>
<script>
export default {
name: "MiniMenu",
props: {},
data() {
return {
isOpen: false,
};
},
mounted() {
window.addEventListener("click", this.handleClickOutside);
},
beforeUnmount() {
window.removeEventListener("click", this.handleClickOutside);
},
methods: {
handleClickOutside(event) {
if (!this.$refs["mini-menu"].contains(event.target)) {
this.isOpen = false;
}
},
close() {
this.isOpen = false;
},
},
};
</script>
<style lang="less" scoped>
@import url("../../assets/less/variables.less");
.mini-menu {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 25px;
right: 0;
border: 1px solid #ccc;
border-radius: 4px;
max-height: 250px;
width: 180px;
overflow-y: auto;
z-index: 1000;
padding-top: 5px;
}
.icon {
user-select: none;
color: #888;
}
.icon:hover {
color: #c5247f;
}
</style>

View file

@ -1,7 +1,9 @@
<template> <template>
<div> <div>
<label v-if="useDropdown" class="uk-form-label"> <label v-if="useDropdown">
{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<select <select
v-model="internalValue" v-model="internalValue"
@ -12,11 +14,17 @@
{{ display }} {{ display }}
</option> </option>
</select> </select>
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
</label> </label>
<label v-if="!useDropdown && dataType == 'number'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'number'">
>{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<div class="uk-inline number-wrapper"> <div class="uk-inline number-wrapper">
<input <input
@ -42,11 +50,15 @@
</button> </button>
</div> </div>
</div> </div>
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
</label> </label>
<div v-if="!useDropdown && dataType == 'boolean'" class="input-and-buttons-container"> <div v-if="!useDropdown && dataType == 'boolean'" class="input-and-buttons-container">
<label class="uk-form-label property-input"> <label class="property-input">
<input <input
ref="checkbox" ref="checkbox"
v-model="internalValue" v-model="internalValue"
@ -55,12 +67,20 @@
type="checkbox" type="checkbox"
@change="sendValue" @change="sendValue"
/> />
{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
</label> </label>
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
<label v-if="!useDropdown && dataType == 'number_array'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'number_array'">
>{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
v-for="i in valueLength" v-for="i in valueLength"
@ -75,11 +95,17 @@
@keydown="keyDown" @keydown="keyDown"
@animationend="animationEnd" @animationend="animationEnd"
/> />
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
</label> </label>
<label v-if="!useDropdown && dataType == 'number_object'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'number_object'">
>{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div v-for="(val, key) in modelValue" :key="key"> <div v-for="(val, key) in modelValue" :key="key">
<label>{{ internalLabels[key] }}</label> <label>{{ internalLabels[key] }}</label>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
@ -94,12 +120,18 @@
@keydown="keyDown" @keydown="keyDown"
@animationend="animationEnd" @animationend="animationEnd"
/> />
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
</div> </div>
</label> </label>
<label v-if="!useDropdown && dataType == 'string'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'string'">
>{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
v-model="internalValue" v-model="internalValue"
@ -111,11 +143,17 @@
@keydown="keyDown" @keydown="keyDown"
@animationend="animationEnd" @animationend="animationEnd"
/> />
<sync-property-button @click="requestUpdate" /> <property-control-menu
:can-reset="isChangedFomDefault"
@reset-default="resetDefault"
@request-update="requestUpdate"
/>
</div> </div>
</label> </label>
<label v-if="!useDropdown && dataType == 'other'" class="uk-form-label" <label v-if="!useDropdown && dataType == 'other'">
>{{ label }} <span class="uk-form-label" :class="{ 'ofm-highlighted-label': isChangedFomDefault }">
{{ label }}
</span>
<div class="input-and-buttons-container"> <div class="input-and-buttons-container">
<input <input
v-model="internalValue" v-model="internalValue"
@ -129,13 +167,13 @@
</template> </template>
<script> <script>
import syncPropertyButton from "./syncPropertyButton.vue"; import PropertyControlMenu from "./propertyControlMenu.vue";
export default { export default {
name: "InputFromSchema", name: "InputFromSchema",
components: { components: {
syncPropertyButton, PropertyControlMenu,
}, },
props: { props: {
dataSchema: { dataSchema: {
@ -267,6 +305,13 @@ export default {
if (this.dataType !== "number") return undefined; if (this.dataType !== "number") return undefined;
return this.dataSchema.minimum; return this.dataSchema.minimum;
}, },
/**
* Is the value changed from default (If no default is set this returns false)
*/
isChangedFomDefault() {
if (this.dataSchema.default === undefined) return false;
return this.dataSchema.default !== this.modelValue;
},
/** /**
* The step size for numerical spinners. * The step size for numerical spinners.
* *
@ -321,6 +366,11 @@ export default {
this.internalValue = this.modelValue; this.internalValue = this.modelValue;
this.$emit("requestUpdate"); this.$emit("requestUpdate");
}, },
resetDefault: async function () {
if (this.isChangedFomDefault) {
this.$emit("sendValue", this.dataSchema.default);
}
},
sendValue: async function () { sendValue: async function () {
this.$emit("sendValue", this.internalValue); this.$emit("sendValue", this.internalValue);
}, },

View file

@ -0,0 +1,48 @@
<template>
<mini-menu v-slot="{ close }">
<p>
<icon-button
icon="refresh"
:show-label="true"
label="Reset to default"
:disabled="!canReset"
@click="
$emit('reset-default');
close();
"
/>
</p>
<p>
<icon-button
icon="sync_alt"
:show-label="true"
label="Re-sync value"
@click="
$emit('request-update');
close();
"
/>
</p>
</mini-menu>
</template>
<script>
import iconButton from "@/components/genericComponents/iconButton.vue";
import MiniMenu from "@/components/genericComponents/miniMenu.vue";
export default {
name: "PropertyControlMenu",
components: {
iconButton,
MiniMenu,
},
props: {
canReset: {
type: Boolean,
default: null,
},
},
emits: ["request-update", "reset-default"],
};
</script>

View file

@ -1,40 +0,0 @@
<template>
<a class="sync-button" @click.prevent="$emit('click')">
<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",
emits: ["click"],
};
</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>

View file

@ -22,7 +22,7 @@ and zero position buttons. It also includes the d-pad.
type="number" type="number"
@keyup.enter="startMoveTask" @keyup.enter="startMoveTask"
/> />
<sync-property-button @click="updatePosition" /> <icon-button icon="sync_alt" @click="updatePosition" />
</div> </div>
<p> <p>
<action-button <action-button
@ -59,7 +59,7 @@ and zero position buttons. It also includes the d-pad.
<script> <script>
import ActionButton from "../../labThingsComponents/actionButton.vue"; import ActionButton from "../../labThingsComponents/actionButton.vue";
import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue"; import iconButton from "@/components/genericComponents/iconButton.vue";
import stageControlButtons from "./stageControlButtons.vue"; import stageControlButtons from "./stageControlButtons.vue";
import { eventBus } from "../../../eventBus.js"; import { eventBus } from "../../../eventBus.js";
@ -68,7 +68,7 @@ export default {
components: { components: {
ActionButton, ActionButton,
syncPropertyButton, iconButton,
stageControlButtons, stageControlButtons,
}, },