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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Suggested values:
|
||||
|
|
@ -538,7 +538,9 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
|||
def settings_ui(self) -> list[PropertyControl]:
|
||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||
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, "stack_images_to_save", label="Images in Stack to Save"
|
||||
),
|
||||
|
|
@ -547,9 +549,13 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]):
|
|||
"stack_min_images_to_test",
|
||||
label="Minimum number of images to test for focus",
|
||||
),
|
||||
property_control_for(self, "stack_dz", label="Stack dz (steps)"),
|
||||
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
||||
property_control_for(self, "max_range", label="Maximum Distance (steps)"),
|
||||
property_control_for(self, "stack_dz", label="Stack dz (steps)", step=5),
|
||||
property_control_for(
|
||||
self, "autofocus_dz", label="Autofocus Range (steps)", step=200
|
||||
),
|
||||
property_control_for(
|
||||
self, "max_range", label="Maximum Distance (steps)", step=1000
|
||||
),
|
||||
property_control_for(
|
||||
self, "skip_background", label="Detect and Skip Empty Fields "
|
||||
),
|
||||
|
|
@ -662,7 +668,9 @@ class RegularGridWorkflow(RectGridWorkflow[RegularGridSettingsModel]):
|
|||
def settings_ui(self) -> list[PropertyControl]:
|
||||
"""A list of PropertyControl objects to create the settings in the scan tab."""
|
||||
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, "y_count", label="Number of rows"),
|
||||
property_control_for(self, "autofocus_dz", label="Autofocus Range (steps)"),
|
||||
|
|
|
|||
|
|
@ -97,6 +97,16 @@ class PropertyControl(BaseModel):
|
|||
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(
|
||||
thing: lt.Thing, property_name: str, **kwargs: Any
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="input-and-buttons-container">
|
||||
<select
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input dropdown"
|
||||
class="uk-form-small property-input dropdown"
|
||||
@change="sendValue"
|
||||
>
|
||||
<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 }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
ref="numericalInput"
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||
:disabled="isDisabled"
|
||||
type="number"
|
||||
@input="grabFocus"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
@animationend="animationEnd"
|
||||
/>
|
||||
<div class="uk-inline number-wrapper">
|
||||
<input
|
||||
ref="numericalInput"
|
||||
v-model="internalValue"
|
||||
class="uk-form-small property-input numeric-property"
|
||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||
:disabled="isDisabled"
|
||||
type="number"
|
||||
:min="minimum"
|
||||
:max="maximum"
|
||||
@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" />
|
||||
</div>
|
||||
</label>
|
||||
<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
|
||||
ref="checkbox"
|
||||
v-model="internalValue"
|
||||
|
|
@ -54,7 +62,7 @@
|
|||
v-for="i in valueLength"
|
||||
:key="i"
|
||||
v-model="internalValue[i - 1]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
class="uk-form-small property-input"
|
||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||
:disabled="isDisabled"
|
||||
type="number"
|
||||
|
|
@ -73,7 +81,7 @@
|
|||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="internalValue[key]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
class="uk-form-small property-input"
|
||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||
:disabled="isDisabled"
|
||||
type="number"
|
||||
|
|
@ -91,7 +99,7 @@
|
|||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
class="uk-form-small property-input"
|
||||
:class="{ edited: isEdited, flash: animateUpdate }"
|
||||
:disabled="isDisabled"
|
||||
type="text"
|
||||
|
|
@ -107,7 +115,7 @@
|
|||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
class="uk-form-small property-input"
|
||||
type="text"
|
||||
disabled="true"
|
||||
/>
|
||||
|
|
@ -149,6 +157,11 @@ export default {
|
|||
default: null,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
emits: ["requestUpdate", "sendValue", "animationShown"],
|
||||
|
|
@ -242,6 +255,27 @@ export default {
|
|||
}
|
||||
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: {
|
||||
|
|
@ -292,6 +326,37 @@ export default {
|
|||
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.
|
||||
*
|
||||
* This is needed for numerical elements to be in focus when a spinner
|
||||
|
|
@ -312,6 +377,17 @@ export default {
|
|||
if (event.keyCode == 13) {
|
||||
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 () {
|
||||
this.isEdited =
|
||||
|
|
@ -349,12 +425,50 @@ export default {
|
|||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.numeric-setting-line-input {
|
||||
.property-input {
|
||||
flex-grow: 1;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
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 {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
:label="label"
|
||||
:animate="animate"
|
||||
:options="options"
|
||||
:step="step"
|
||||
@request-update="readProperty"
|
||||
@send-value="writeProperty"
|
||||
@animation-shown="resetAnimate"
|
||||
|
|
@ -50,6 +51,11 @@ export default {
|
|||
default: null,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
:read-back="propertyData.read_back"
|
||||
:read-back-delay="propertyData.read_back_delay"
|
||||
:options="propertyData.options"
|
||||
:step="propertyData.step"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue