implement optional device fields
This commit is contained in:
parent
f8beae5f46
commit
54a34b2084
@ -85,4 +85,21 @@ table.bookmarks .frequency {
|
||||
.sdr-device-list .list-group-item,
|
||||
.sdr-profile-list .list-group-item {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.removable-group.removable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.removable-group.removable .removable-item {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.removable-group.removable .option-remove-button {
|
||||
flex: 0 0 70px;
|
||||
}
|
||||
|
||||
.option-add-button, .option-remove-button {
|
||||
width: 70px;
|
||||
}
|
26
htdocs/lib/settings/OptionalSection.js
Normal file
26
htdocs/lib/settings/OptionalSection.js
Normal file
@ -0,0 +1,26 @@
|
||||
$.fn.optionalSection = function(){
|
||||
this.each(function() {
|
||||
var $section = $(this);
|
||||
var $select = $section.find('.optional-select');
|
||||
var $optionalInputs = $section.find('.optional-inputs');
|
||||
$section.on('click', '.option-add-button', function(e){
|
||||
var field = $select.val();
|
||||
var group = $optionalInputs.find(".form-group[data-field='" + field + "']");
|
||||
group.find('input, select').prop('disabled', false);
|
||||
$select.parents('.form-group').before(group);
|
||||
$select.find('option[value=\'' + field + '\']').remove();
|
||||
|
||||
return false;
|
||||
});
|
||||
$section.on('click', '.option-remove-button', function(e) {
|
||||
var group = $(e.target).parents('.form-group')
|
||||
group.find('input, select').prop('disabled', true);
|
||||
$optionalInputs.append(group);
|
||||
var $label = group.find('> label');
|
||||
var $option = $('<option value="' + group.data('field') + '">' + $label.text() + '</option>');
|
||||
$select.append($option);
|
||||
|
||||
return false;
|
||||
})
|
||||
});
|
||||
}
|
@ -5,4 +5,5 @@ $(function(){
|
||||
$('.wsjt-decoding-depths').wsjtDecodingDepthsInput();
|
||||
$('#waterfall_scheme').waterfallDropdown();
|
||||
$('#rf_gain').gainInput();
|
||||
$('.optional-section').optionalSection();
|
||||
});
|
@ -151,6 +151,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
||||
"lib/settings/WsjtDecodingDepthsInput.js",
|
||||
"lib/settings/WaterfallDropdown.js",
|
||||
"lib/settings/GainInput.js",
|
||||
"lib/settings/OptionalSection.js",
|
||||
"settings.js",
|
||||
],
|
||||
}
|
||||
|
@ -10,19 +10,25 @@ class Section(object):
|
||||
self.title = title
|
||||
self.inputs = inputs
|
||||
|
||||
def render_input(self, input, data):
|
||||
return input.render(data)
|
||||
|
||||
def render_inputs(self, data):
|
||||
return "".join([i.render(data) for i in self.inputs])
|
||||
return "".join([self.render_input(i, data) for i in self.inputs])
|
||||
|
||||
def classes(self):
|
||||
return ["col-12", "settings-section"]
|
||||
|
||||
def render(self, data):
|
||||
return """
|
||||
<div class="col-12 settings-section">
|
||||
<div class="{classes}">
|
||||
<h3 class="settings-header">
|
||||
{title}
|
||||
</h3>
|
||||
{inputs}
|
||||
</div>
|
||||
""".format(
|
||||
title=self.title, inputs=self.render_inputs(data)
|
||||
classes=" ".join(self.classes()), title=self.title, inputs=self.render_inputs(data)
|
||||
)
|
||||
|
||||
def parse(self, data):
|
||||
|
@ -71,7 +71,7 @@ class SdrDeviceController(SettingsFormController):
|
||||
def getSections(self):
|
||||
try:
|
||||
description = SdrDeviceDescription.getByType(self.device["type"])
|
||||
return [description.getSection(self.device)]
|
||||
return [description.getSection()]
|
||||
except SdrDeviceDescriptionMissing:
|
||||
# TODO provide a generic interface that allows to switch the type
|
||||
return []
|
||||
|
@ -6,11 +6,19 @@ from enum import Enum
|
||||
|
||||
|
||||
class Input(ABC):
|
||||
def __init__(self, id, label, infotext=None, converter: Converter = None):
|
||||
def __init__(self, id, label, infotext=None, converter: Converter = None, disabled=False, removable=False):
|
||||
self.id = id
|
||||
self.label = label
|
||||
self.infotext = infotext
|
||||
self.converter = self.defaultConverter() if converter is None else converter
|
||||
self.disabled = disabled
|
||||
self.removable = removable
|
||||
|
||||
def setDisabled(self, disabled=True):
|
||||
self.disabled = disabled
|
||||
|
||||
def setRemovable(self, removable=True):
|
||||
self.removable = removable
|
||||
|
||||
def defaultConverter(self):
|
||||
return NullConverter()
|
||||
@ -18,23 +26,47 @@ class Input(ABC):
|
||||
def bootstrap_decorate(self, input):
|
||||
infotext = "<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
|
||||
return """
|
||||
<div class="form-group row">
|
||||
<div class="form-group row" data-field="{id}">
|
||||
<label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label>
|
||||
<div class="col-9 p-0">
|
||||
{input}
|
||||
{infotext}
|
||||
<div class="col-9 p-0 removable-group {removable}">
|
||||
<div class="removeable-item">
|
||||
{input}
|
||||
{infotext}
|
||||
</div>
|
||||
{removebutton}
|
||||
</div>
|
||||
</div>
|
||||
""".format(
|
||||
id=self.id, label=self.label, input=input, infotext=infotext
|
||||
id=self.id,
|
||||
label=self.label,
|
||||
input=input,
|
||||
infotext=infotext,
|
||||
removable="removable" if self.removable else "",
|
||||
removebutton='<button class="btn btn-sm btn-danger option-remove-button">Remove</button>'
|
||||
if self.removable
|
||||
else "",
|
||||
)
|
||||
|
||||
def input_classes(self):
|
||||
return " ".join(["form-control", "form-control-sm"])
|
||||
|
||||
@abstractmethod
|
||||
def input_properties(self, value):
|
||||
props = {
|
||||
"class": self.input_classes(),
|
||||
"id": self.id,
|
||||
"name": self.id,
|
||||
"placeholder": self.label,
|
||||
"value": value,
|
||||
}
|
||||
if self.disabled:
|
||||
props["disabled"] = "disabled"
|
||||
return props
|
||||
|
||||
def render_input_properties(self, value):
|
||||
return " ".join('{}="{}"'.format(prop, value) for prop, value in self.input_properties(value).items())
|
||||
|
||||
def render_input(self, value):
|
||||
pass
|
||||
return "<input {properties} />".format(properties=self.render_input_properties(value))
|
||||
|
||||
def render(self, config):
|
||||
value = config[self.id] if self.id in config else None
|
||||
@ -43,14 +75,15 @@ class Input(ABC):
|
||||
def parse(self, data):
|
||||
return {self.id: self.converter.convert_from_form(data[self.id][0])} if self.id in data else {}
|
||||
|
||||
def getLabel(self):
|
||||
return self.label
|
||||
|
||||
|
||||
class TextInput(Input):
|
||||
def render_input(self, value):
|
||||
return """
|
||||
<input type="text" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}">
|
||||
""".format(
|
||||
id=self.id, label=self.label, classes=self.input_classes(), value=value
|
||||
)
|
||||
def input_properties(self, value):
|
||||
props = super().input_properties(value)
|
||||
props["type"] = "text"
|
||||
return props
|
||||
|
||||
|
||||
class NumberInput(Input):
|
||||
@ -62,6 +95,13 @@ class NumberInput(Input):
|
||||
def defaultConverter(self):
|
||||
return IntConverter()
|
||||
|
||||
def input_properties(self, value):
|
||||
props = super().input_properties(value)
|
||||
props["type"] = "number"
|
||||
if self.step:
|
||||
props["step"] = self.step
|
||||
return props
|
||||
|
||||
def render_input(self, value):
|
||||
if self.append:
|
||||
append = """
|
||||
@ -76,15 +116,11 @@ class NumberInput(Input):
|
||||
|
||||
return """
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}" {step}>
|
||||
{input}
|
||||
{append}
|
||||
</div>
|
||||
""".format(
|
||||
id=self.id,
|
||||
label=self.label,
|
||||
classes=self.input_classes(),
|
||||
value=value,
|
||||
step='step="{0}"'.format(self.step) if self.step else "",
|
||||
input=super().render_input(value),
|
||||
append=append,
|
||||
)
|
||||
|
||||
@ -116,13 +152,15 @@ class LocationInput(Input):
|
||||
def render_sub_input(self, value, id):
|
||||
return """
|
||||
<div class="col">
|
||||
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}" step="any">
|
||||
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
|
||||
step="any" {disabled}>
|
||||
</div>
|
||||
""".format(
|
||||
id="{0}-{1}".format(self.id, id),
|
||||
label=self.label,
|
||||
classes=self.input_classes(),
|
||||
value=value[id],
|
||||
disabled="disabled" if self.disabled else "",
|
||||
)
|
||||
|
||||
def parse(self, data):
|
||||
@ -132,9 +170,9 @@ class LocationInput(Input):
|
||||
class TextAreaInput(Input):
|
||||
def render_input(self, value):
|
||||
return """
|
||||
<textarea class="{classes}" id="{id}" name="{id}" style="height:200px;">{value}</textarea>
|
||||
<textarea class="{classes}" id="{id}" name="{id}" style="height:200px;" {disabled}>{value}</textarea>
|
||||
""".format(
|
||||
id=self.id, classes=self.input_classes(), value=value
|
||||
id=self.id, classes=self.input_classes(), value=value, disabled="disabled" if self.disabled else ""
|
||||
)
|
||||
|
||||
|
||||
@ -145,16 +183,17 @@ class CheckboxInput(Input):
|
||||
|
||||
def render_input(self, value):
|
||||
return """
|
||||
<div class="{classes}">
|
||||
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked}>
|
||||
<label class="form-check-label" for="{id}">
|
||||
{checkboxText}
|
||||
</label>
|
||||
</div>
|
||||
<div class="{classes}">
|
||||
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked} {disabled}>
|
||||
<label class="form-check-label" for="{id}">
|
||||
{checkboxText}
|
||||
</label>
|
||||
</div>
|
||||
""".format(
|
||||
id=self.id,
|
||||
classes=self.input_classes(),
|
||||
checked="checked" if value else "",
|
||||
disabled="disabled" if self.disabled else "",
|
||||
checkboxText=self.checkboxText,
|
||||
)
|
||||
|
||||
@ -164,6 +203,9 @@ class CheckboxInput(Input):
|
||||
def parse(self, data):
|
||||
return {self.id: self.converter.convert_from_form(self.id in data and data[self.id][0] == "on")}
|
||||
|
||||
def getLabel(self):
|
||||
return self.checkboxText
|
||||
|
||||
|
||||
class Option(object):
|
||||
# used for both MultiCheckboxInput and DropdownInput
|
||||
@ -186,7 +228,7 @@ class MultiCheckboxInput(Input):
|
||||
def render_checkbox(self, option, value):
|
||||
return """
|
||||
<div class="{classes}">
|
||||
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked}>
|
||||
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked} {disabled}>
|
||||
<label class="form-check-label" for="{id}">
|
||||
{checkboxText}
|
||||
</label>
|
||||
@ -196,6 +238,7 @@ class MultiCheckboxInput(Input):
|
||||
classes=self.input_classes(),
|
||||
checked="checked" if option.value in value else "",
|
||||
checkboxText=option.text,
|
||||
disabled="disabled" if self.disabled else "",
|
||||
)
|
||||
|
||||
def parse(self, data):
|
||||
@ -242,9 +285,12 @@ class DropdownInput(Input):
|
||||
|
||||
def render_input(self, value):
|
||||
return """
|
||||
<select class="{classes}" id="{id}" name="{id}">{options}</select>
|
||||
<select class="{classes}" id="{id}" name="{id}" {disabled}>{options}</select>
|
||||
""".format(
|
||||
classes=self.input_classes(), id=self.id, options=self.render_options(value)
|
||||
classes=self.input_classes(),
|
||||
id=self.id,
|
||||
options=self.render_options(value),
|
||||
disabled="disabled" if self.disabled else "",
|
||||
)
|
||||
|
||||
def render_options(self, value):
|
||||
|
@ -16,12 +16,12 @@ class GainInput(Input):
|
||||
|
||||
return """
|
||||
<div id="{id}">
|
||||
<select class="{classes}" id="{id}-select" name="{id}-select">
|
||||
<select class="{classes}" id="{id}-select" name="{id}-select" {disabled}>
|
||||
{options}
|
||||
</select>
|
||||
<div class="option manual" style="display: none;">
|
||||
<input type="number" id="{id}-manual" name="{id}-manual" value="{value}" class="{classes}"
|
||||
placeholder="Manual device gain" step="any">
|
||||
placeholder="Manual device gain" step="any" {disabled}>
|
||||
</div>
|
||||
{stageoption}
|
||||
</div>
|
||||
@ -32,6 +32,7 @@ class GainInput(Input):
|
||||
label=self.label,
|
||||
options=self.render_options(value),
|
||||
stageoption="" if self.gain_stages is None else self.render_stage_option(value),
|
||||
disabled="disabled" if self.disabled else ""
|
||||
)
|
||||
|
||||
def render_options(self, value):
|
||||
@ -79,15 +80,16 @@ class GainInput(Input):
|
||||
inputs="".join(
|
||||
"""
|
||||
<div class="row">
|
||||
<div class="col-3">{stage}</div>
|
||||
<label class="col-form-label col-form-label-sm col-3">{stage}</label>
|
||||
<input type="number" id="{id}-{stage}" name="{id}-{stage}" value="{value}"
|
||||
class="col-9 {classes}" placeholder="{stage}" step="any">
|
||||
class="col-9 {classes}" placeholder="{stage}" step="any" {disabled}>
|
||||
</div>
|
||||
""".format(
|
||||
id=self.id,
|
||||
stage=stage,
|
||||
value=value_dict[stage] if stage in value_dict else "",
|
||||
classes=self.input_classes(),
|
||||
disabled="disabled" if self.disabled else "",
|
||||
)
|
||||
for stage in self.gain_stages
|
||||
)
|
||||
|
@ -22,7 +22,7 @@ class Q65ModeMatrix(Input):
|
||||
id=self.checkbox_id(mode, interval),
|
||||
checked="checked" if "{}{}".format(mode.name, interval.value) in value else "",
|
||||
checkboxText="Mode {} interval {}s".format(mode.name, interval.value),
|
||||
disabled="" if interval.is_available(mode) else "disabled",
|
||||
disabled="" if interval.is_available(mode) and not self.disabled else "disabled",
|
||||
)
|
||||
|
||||
def render_input(self, value):
|
||||
@ -69,7 +69,7 @@ class WsjtDecodingDepthsInput(Input):
|
||||
)
|
||||
|
||||
return """
|
||||
<input type="hidden" class="{classes}" id="{id}" name="{id}" value="{value}">
|
||||
<input type="hidden" class="{classes}" id="{id}" name="{id}" value="{value}" {disabled}>
|
||||
<div class="inputs" style="display:none;">
|
||||
<select class="form-control form-control-sm">{options}</select>
|
||||
<input class="form-control form-control-sm" type="number" step="1">
|
||||
@ -79,6 +79,7 @@ class WsjtDecodingDepthsInput(Input):
|
||||
classes=self.input_classes(),
|
||||
value=html.escape(value),
|
||||
options="".join(render_mode(m) for m in Modes.getAvailableModes() if isinstance(m, WsjtMode)),
|
||||
disabled="disabled" if self.disabled else ""
|
||||
)
|
||||
|
||||
def input_classes(self):
|
||||
|
@ -375,10 +375,76 @@ class SdrDeviceDescriptionMissing(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SdrDeviceDescription(object):
|
||||
def __init__(self):
|
||||
self.indexedInputs = {input.id: input for input in self.getInputs()}
|
||||
class OptionalSection(Section):
|
||||
def __init__(self, title, inputs: List[Input], mandatory, optional):
|
||||
super().__init__(title, *inputs)
|
||||
self.mandatory = mandatory
|
||||
self.optional = optional
|
||||
self.optional_inputs = []
|
||||
|
||||
def classes(self):
|
||||
classes = super().classes()
|
||||
classes.append("optional-section")
|
||||
return classes
|
||||
|
||||
def _is_optional(self, input):
|
||||
return input.id in self.optional
|
||||
|
||||
def render_optional_select(self):
|
||||
return """
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-form-label-sm col-3">
|
||||
Additional optional settings
|
||||
</label>
|
||||
<div class="input-group input-group-sm col-9 p-0">
|
||||
<select class="form-control from-control-sm optional-select">
|
||||
{options}
|
||||
</select>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-success option-add-button">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".format(
|
||||
options="".join(
|
||||
"""
|
||||
<option value="{value}">{name}</option>
|
||||
""".format(
|
||||
value=input.id,
|
||||
name=input.getLabel(),
|
||||
)
|
||||
for input in self.optional_inputs
|
||||
)
|
||||
)
|
||||
|
||||
def render_optional_inputs(self, data):
|
||||
return """
|
||||
<div class="optional-inputs" style="display: none;">
|
||||
{inputs}
|
||||
</div>
|
||||
""".format(
|
||||
inputs="".join(self.render_input(input, data) for input in self.optional_inputs)
|
||||
)
|
||||
|
||||
def render_inputs(self, data):
|
||||
return super().render_inputs(data) + self.render_optional_select() + self.render_optional_inputs(data)
|
||||
|
||||
def render(self, data):
|
||||
indexed_inputs = {input.id: input for input in self.inputs}
|
||||
visible_keys = set(self.mandatory + [k for k in self.optional if k in data])
|
||||
optional_keys = set(k for k in self.optional if k not in data)
|
||||
self.inputs = [input for k, input in indexed_inputs.items() if k in visible_keys]
|
||||
for input in self.inputs:
|
||||
if self._is_optional(input):
|
||||
input.setRemovable()
|
||||
self.optional_inputs = [input for k, input in indexed_inputs.items() if k in optional_keys]
|
||||
for input in self.optional_inputs:
|
||||
input.setRemovable()
|
||||
input.setDisabled()
|
||||
return super().render(data)
|
||||
|
||||
|
||||
class SdrDeviceDescription(object):
|
||||
@staticmethod
|
||||
def getByType(sdr_type: str) -> "SdrDeviceDescription":
|
||||
try:
|
||||
@ -407,7 +473,6 @@ class SdrDeviceDescription(object):
|
||||
),
|
||||
CheckboxInput(
|
||||
"services",
|
||||
"",
|
||||
"Run background services on this device",
|
||||
converter=OptionalConverter(defaultFormValue=True),
|
||||
),
|
||||
@ -431,8 +496,5 @@ class SdrDeviceDescription(object):
|
||||
def getOptionalKeys(self):
|
||||
return ["ppm", "always-on", "services", "rf_gain", "lfo_offset", "waterfall_min_level", "waterfall_max_level"]
|
||||
|
||||
def getSection(self, data):
|
||||
visible_keys = set(self.getMandatoryKeys() + [k for k in self.getOptionalKeys() if k in data])
|
||||
inputs = [input for k, input in self.indexedInputs.items() if k in visible_keys]
|
||||
# TODO: render remaining keys in optional area
|
||||
return Section("Device settings", *inputs)
|
||||
def getSection(self):
|
||||
return OptionalSection("Device settings", self.getInputs(), self.getMandatoryKeys(), self.getOptionalKeys())
|
||||
|
Loading…
Reference in New Issue
Block a user