Merge branch 'numerical-input-limits' into 'v3'
Use lt.property/setting validatiors to set numerical input range and step Closes #457 See merge request openflexure/openflexure-microscope-server!494
This commit is contained in:
commit
0709a7b118
5 changed files with 166 additions and 27 deletions
|
|
@ -295,13 +295,13 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
|
|
||||||
# Stacking settings
|
# Stacking settings
|
||||||
|
|
||||||
stack_images_to_save: int = lt.setting(default=1)
|
stack_images_to_save: int = lt.setting(default=1, ge=1, le=9)
|
||||||
"""The number of images to save in a stack.
|
"""The number of images to save in a stack.
|
||||||
|
|
||||||
Defaults to 1 unless you need to see either side of focus
|
Defaults to 1 unless you need to see either side of focus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stack_min_images_to_test: int = lt.setting(default=9)
|
stack_min_images_to_test: int = lt.setting(default=9, ge=5, le=13)
|
||||||
"""The minimum number of images to capture in a stack.
|
"""The minimum number of images to capture in a stack.
|
||||||
|
|
||||||
This many images are captures and tested for focus, if the focus is not central
|
This many images are captures and tested for focus, if the focus is not central
|
||||||
|
|
@ -311,7 +311,7 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
Defaults to 9 which balances reliability and speed.
|
Defaults to 9 which balances reliability and speed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
stack_dz: int = lt.setting(default=50)
|
stack_dz: int = lt.setting(default=50, ge=10, le=400)
|
||||||
"""Distance in steps between images in a z-stack.
|
"""Distance in steps between images in a z-stack.
|
||||||
|
|
||||||
Suggested values:
|
Suggested values:
|
||||||
|
|
@ -538,7 +538,9 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
def settings_ui(self) -> list[PropertyControl]:
|
def settings_ui(self) -> list[PropertyControl]:
|
||||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||||
return [
|
return [
|
||||||
property_control_for(self, "overlap", label="Image Overlap (0.1-0.7)"),
|
property_control_for(
|
||||||
|
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
||||||
|
),
|
||||||
property_control_for(
|
property_control_for(
|
||||||
self, "stack_images_to_save", label="Images in Stack to Save"
|
self, "stack_images_to_save", label="Images in Stack to Save"
|
||||||
),
|
),
|
||||||
|
|
@ -547,9 +549,13 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
||||||
"stack_min_images_to_test",
|
"stack_min_images_to_test",
|
||||||
label="Minimum number of images to test for focus",
|
label="Minimum number of images to test for focus",
|
||||||
),
|
),
|
||||||
property_control_for(self, "stack_dz", label="Stack dz (steps)"),
|
property_control_for(self, "stack_dz", label="Stack dz (steps)", step=5),
|
||||||
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
property_control_for(
|
||||||
property_control_for(self, "max_range", label="Maximum Distance (steps)"),
|
self, "autofocus_dz", label="Autofocus Range (steps)", step=200
|
||||||
|
),
|
||||||
|
property_control_for(
|
||||||
|
self, "max_range", label="Maximum Distance (steps)", step=1000
|
||||||
|
),
|
||||||
property_control_for(
|
property_control_for(
|
||||||
self, "skip_background", label="Detect and Skip Empty Fields "
|
self, "skip_background", label="Detect and Skip Empty Fields "
|
||||||
),
|
),
|
||||||
|
|
@ -662,7 +668,9 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
|
||||||
def settings_ui(self) -> list[PropertyControl]:
|
def settings_ui(self) -> list[PropertyControl]:
|
||||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||||
return [
|
return [
|
||||||
property_control_for(self, "overlap", label="Image Overlap (0.1-0.7)"),
|
property_control_for(
|
||||||
|
self, "overlap", label="Image Overlap (0.1-0.7)", step=0.05
|
||||||
|
),
|
||||||
property_control_for(self, "x_count", label="Number of columns"),
|
property_control_for(self, "x_count", label="Number of columns"),
|
||||||
property_control_for(self, "y_count", label="Number of rows"),
|
property_control_for(self, "y_count", label="Number of rows"),
|
||||||
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,16 @@ class PropertyControl(BaseModel):
|
||||||
when selected.
|
when selected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
step: Optional[int | float] = None
|
||||||
|
"""The step size for a numerical input.
|
||||||
|
|
||||||
|
If the property is not numeric this will be ignored.
|
||||||
|
|
||||||
|
If this is left as None, then the UI will try to use the ``multipleOf`` field in
|
||||||
|
the property dataSchema. If ``multipleOf`` is not set then the browser default is
|
||||||
|
used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def property_control_for(
|
def property_control_for(
|
||||||
thing: lt.Thing, property_name: str, **kwargs: Any
|
thing: lt.Thing, property_name: str, **kwargs: Any
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<select
|
<select
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
class="uk-form-small numeric-setting-line-input dropdown"
|
class="uk-form-small property-input dropdown"
|
||||||
@change="sendValue"
|
@change="sendValue"
|
||||||
>
|
>
|
||||||
<option v-for="(value, display) in options" :key="value" :value="value">
|
<option v-for="(value, display) in options" :key="value" :value="value">
|
||||||
|
|
@ -18,23 +18,31 @@
|
||||||
<label v-if="!useDropdown && dataType == 'number'" class="uk-form-label"
|
<label v-if="!useDropdown && dataType == 'number'" class="uk-form-label"
|
||||||
>{{ label }}
|
>{{ label }}
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<div class="uk-inline number-wrapper">
|
||||||
ref="numericalInput"
|
<input
|
||||||
v-model="internalValue"
|
ref="numericalInput"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
v-model="internalValue"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
class="uk-form-small property-input numeric-property"
|
||||||
:disabled="isDisabled"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
type="number"
|
:disabled="isDisabled"
|
||||||
@input="grabFocus"
|
type="number"
|
||||||
@focusout="focusOut"
|
:min="minimum"
|
||||||
@keydown="keyDown"
|
:max="maximum"
|
||||||
@animationend="animationEnd"
|
@input="grabFocus"
|
||||||
/>
|
@focusout="focusOut"
|
||||||
|
@keydown="keyDown"
|
||||||
|
@animationend="animationEnd"
|
||||||
|
/>
|
||||||
|
<div class="spinner-buttons">
|
||||||
|
<button type="button" @click="increment(1)" @mousedown="spinnerClick">▲</button>
|
||||||
|
<button type="button" @click="increment(-1)" @mousedown="spinnerClick">▼</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<sync-property-button @click="requestUpdate" />
|
<sync-property-button @click="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 numeric-setting-line-input">
|
<label class="uk-form-label property-input">
|
||||||
<input
|
<input
|
||||||
ref="checkbox"
|
ref="checkbox"
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
|
|
@ -54,7 +62,7 @@
|
||||||
v-for="i in valueLength"
|
v-for="i in valueLength"
|
||||||
: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 property-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -73,7 +81,7 @@
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<input
|
||||||
v-model="internalValue[key]"
|
v-model="internalValue[key]"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small property-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -91,7 +99,7 @@
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<input
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small property-input"
|
||||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -107,7 +115,7 @@
|
||||||
<div class="input-and-buttons-container">
|
<div class="input-and-buttons-container">
|
||||||
<input
|
<input
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small property-input"
|
||||||
type="text"
|
type="text"
|
||||||
disabled="true"
|
disabled="true"
|
||||||
/>
|
/>
|
||||||
|
|
@ -149,6 +157,11 @@ export default {
|
||||||
default: null,
|
default: null,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
emits: ["requestUpdate", "sendValue", "animationShown"],
|
emits: ["requestUpdate", "sendValue", "animationShown"],
|
||||||
|
|
@ -242,6 +255,27 @@ export default {
|
||||||
}
|
}
|
||||||
return "other";
|
return "other";
|
||||||
},
|
},
|
||||||
|
maximum() {
|
||||||
|
if (this.dataType !== "number") return undefined;
|
||||||
|
return this.dataSchema.maximum;
|
||||||
|
},
|
||||||
|
minimum() {
|
||||||
|
if (this.dataType !== "number") return undefined;
|
||||||
|
return this.dataSchema.minimum;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The step size for numerical spinners.
|
||||||
|
*
|
||||||
|
* If not a number this is `undefined`. If `step` is set as a prop then it is used
|
||||||
|
* otherwise `multipleOf` from the property `dataSchema` is used. If `multipleOf`
|
||||||
|
* is not set in the `dataSchema` JS will return `undefined` and the browser will
|
||||||
|
* use its default value.
|
||||||
|
*/
|
||||||
|
spinner_step() {
|
||||||
|
if (this.dataType !== "number") return undefined;
|
||||||
|
if (this.step !== null) return this.step;
|
||||||
|
return this.dataSchema.multipleOf;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -292,6 +326,37 @@ export default {
|
||||||
this.sendValue();
|
this.sendValue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
increment(sign) {
|
||||||
|
const step = this.spinner_step || 1;
|
||||||
|
const multipleOf = this.dataSchema.multipleOf;
|
||||||
|
let newValue = Number(this.internalValue) || 0;
|
||||||
|
newValue += sign * step;
|
||||||
|
// Apply minimum if defined
|
||||||
|
if (this.minimum !== undefined && this.minimum !== null) {
|
||||||
|
newValue = Math.max(newValue, this.minimum);
|
||||||
|
}
|
||||||
|
// Apply maximum if defined
|
||||||
|
if (this.maximum !== undefined && this.maximum !== null) {
|
||||||
|
newValue = Math.min(newValue, this.maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mulipleOf is validated on server then enforce it.
|
||||||
|
if (multipleOf) {
|
||||||
|
newValue = Math.round(newValue / multipleOf) * multipleOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounding to about 5 sig figs to stop weird javascript rounding.
|
||||||
|
const magnitude = Math.floor(Math.log10(Math.abs(newValue)));
|
||||||
|
const factor = 10 ** -(Math.floor(magnitude) - 4);
|
||||||
|
this.internalValue = Math.round(newValue * factor) / factor;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Grab focus for the numerical input when a spinner is clicked.
|
||||||
|
*/
|
||||||
|
spinnerClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.$refs.numericalInput.focus();
|
||||||
|
},
|
||||||
/** Grab the browser focus.
|
/** Grab the browser focus.
|
||||||
*
|
*
|
||||||
* This is needed for numerical elements to be in focus when a spinner
|
* This is needed for numerical elements to be in focus when a spinner
|
||||||
|
|
@ -312,6 +377,17 @@ export default {
|
||||||
if (event.keyCode == 13) {
|
if (event.keyCode == 13) {
|
||||||
this.sendValue();
|
this.sendValue();
|
||||||
}
|
}
|
||||||
|
// If numeric then capture Up and Down keys for incrementing.
|
||||||
|
if (this.dataType === "number") {
|
||||||
|
if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.increment(1);
|
||||||
|
}
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.increment(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateIsEdited: function () {
|
updateIsEdited: function () {
|
||||||
this.isEdited =
|
this.isEdited =
|
||||||
|
|
@ -349,12 +425,50 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.numeric-setting-line-input {
|
.property-input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
}
|
}
|
||||||
|
/*Hide standard spinners as it isn't possible to customise the browser ones.*/
|
||||||
|
.numeric-property {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
.numeric-property::-webkit-outer-spin-button,
|
||||||
|
.numeric-property::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-content: stretch;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-buttons {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 1px;
|
||||||
|
bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-buttons button {
|
||||||
|
height: 45%;
|
||||||
|
padding: 0 6px;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
.dropdown {
|
.dropdown {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
:label="label"
|
:label="label"
|
||||||
:animate="animate"
|
:animate="animate"
|
||||||
:options="options"
|
:options="options"
|
||||||
|
:step="step"
|
||||||
@request-update="readProperty"
|
@request-update="readProperty"
|
||||||
@send-value="writeProperty"
|
@send-value="writeProperty"
|
||||||
@animation-shown="resetAnimate"
|
@animation-shown="resetAnimate"
|
||||||
|
|
@ -50,6 +51,11 @@ export default {
|
||||||
default: null,
|
default: null,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
:read-back="propertyData.read_back"
|
:read-back="propertyData.read_back"
|
||||||
:read-back-delay="propertyData.read_back_delay"
|
:read-back-delay="propertyData.read_back_delay"
|
||||||
:options="propertyData.options"
|
:options="propertyData.options"
|
||||||
|
:step="propertyData.step"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue