implement optional device fields

This commit is contained in:
Jakob Ketterl 2021-02-22 23:49:28 +01:00
parent f8beae5f46
commit 54a34b2084
10 changed files with 213 additions and 51 deletions

View File

@ -86,3 +86,20 @@ table.bookmarks .frequency {
.sdr-profile-list .list-group-item { .sdr-profile-list .list-group-item {
background: initial; 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;
}

View 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;
})
});
}

View File

@ -5,4 +5,5 @@ $(function(){
$('.wsjt-decoding-depths').wsjtDecodingDepthsInput(); $('.wsjt-decoding-depths').wsjtDecodingDepthsInput();
$('#waterfall_scheme').waterfallDropdown(); $('#waterfall_scheme').waterfallDropdown();
$('#rf_gain').gainInput(); $('#rf_gain').gainInput();
$('.optional-section').optionalSection();
}); });

View File

@ -151,6 +151,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/settings/WsjtDecodingDepthsInput.js", "lib/settings/WsjtDecodingDepthsInput.js",
"lib/settings/WaterfallDropdown.js", "lib/settings/WaterfallDropdown.js",
"lib/settings/GainInput.js", "lib/settings/GainInput.js",
"lib/settings/OptionalSection.js",
"settings.js", "settings.js",
], ],
} }

View File

@ -10,19 +10,25 @@ class Section(object):
self.title = title self.title = title
self.inputs = inputs self.inputs = inputs
def render_input(self, input, data):
return input.render(data)
def render_inputs(self, 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): def render(self, data):
return """ return """
<div class="col-12 settings-section"> <div class="{classes}">
<h3 class="settings-header"> <h3 class="settings-header">
{title} {title}
</h3> </h3>
{inputs} {inputs}
</div> </div>
""".format( """.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): def parse(self, data):

View File

@ -71,7 +71,7 @@ class SdrDeviceController(SettingsFormController):
def getSections(self): def getSections(self):
try: try:
description = SdrDeviceDescription.getByType(self.device["type"]) description = SdrDeviceDescription.getByType(self.device["type"])
return [description.getSection(self.device)] return [description.getSection()]
except SdrDeviceDescriptionMissing: except SdrDeviceDescriptionMissing:
# TODO provide a generic interface that allows to switch the type # TODO provide a generic interface that allows to switch the type
return [] return []

View File

@ -6,11 +6,19 @@ from enum import Enum
class Input(ABC): 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.id = id
self.label = label self.label = label
self.infotext = infotext self.infotext = infotext
self.converter = self.defaultConverter() if converter is None else converter 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): def defaultConverter(self):
return NullConverter() return NullConverter()
@ -18,23 +26,47 @@ class Input(ABC):
def bootstrap_decorate(self, input): def bootstrap_decorate(self, input):
infotext = "<small>{text}</small>".format(text=self.infotext) if self.infotext else "" infotext = "<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
return """ 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> <label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label>
<div class="col-9 p-0"> <div class="col-9 p-0 removable-group {removable}">
<div class="removeable-item">
{input} {input}
{infotext} {infotext}
</div> </div>
{removebutton}
</div>
</div> </div>
""".format( """.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): def input_classes(self):
return " ".join(["form-control", "form-control-sm"]) 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): def render_input(self, value):
pass return "<input {properties} />".format(properties=self.render_input_properties(value))
def render(self, config): def render(self, config):
value = config[self.id] if self.id in config else None value = config[self.id] if self.id in config else None
@ -43,14 +75,15 @@ class Input(ABC):
def parse(self, data): def parse(self, data):
return {self.id: self.converter.convert_from_form(data[self.id][0])} if self.id in data else {} 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): class TextInput(Input):
def render_input(self, value): def input_properties(self, value):
return """ props = super().input_properties(value)
<input type="text" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"> props["type"] = "text"
""".format( return props
id=self.id, label=self.label, classes=self.input_classes(), value=value
)
class NumberInput(Input): class NumberInput(Input):
@ -62,6 +95,13 @@ class NumberInput(Input):
def defaultConverter(self): def defaultConverter(self):
return IntConverter() 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): def render_input(self, value):
if self.append: if self.append:
append = """ append = """
@ -76,15 +116,11 @@ class NumberInput(Input):
return """ return """
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}" {step}> {input}
{append} {append}
</div> </div>
""".format( """.format(
id=self.id, input=super().render_input(value),
label=self.label,
classes=self.input_classes(),
value=value,
step='step="{0}"'.format(self.step) if self.step else "",
append=append, append=append,
) )
@ -116,13 +152,15 @@ class LocationInput(Input):
def render_sub_input(self, value, id): def render_sub_input(self, value, id):
return """ return """
<div class="col"> <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> </div>
""".format( """.format(
id="{0}-{1}".format(self.id, id), id="{0}-{1}".format(self.id, id),
label=self.label, label=self.label,
classes=self.input_classes(), classes=self.input_classes(),
value=value[id], value=value[id],
disabled="disabled" if self.disabled else "",
) )
def parse(self, data): def parse(self, data):
@ -132,9 +170,9 @@ class LocationInput(Input):
class TextAreaInput(Input): class TextAreaInput(Input):
def render_input(self, value): def render_input(self, value):
return """ 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( """.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 ""
) )
@ -146,7 +184,7 @@ class CheckboxInput(Input):
def render_input(self, value): def render_input(self, value):
return """ return """
<div class="{classes}"> <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}"> <label class="form-check-label" for="{id}">
{checkboxText} {checkboxText}
</label> </label>
@ -155,6 +193,7 @@ class CheckboxInput(Input):
id=self.id, id=self.id,
classes=self.input_classes(), classes=self.input_classes(),
checked="checked" if value else "", checked="checked" if value else "",
disabled="disabled" if self.disabled else "",
checkboxText=self.checkboxText, checkboxText=self.checkboxText,
) )
@ -164,6 +203,9 @@ class CheckboxInput(Input):
def parse(self, data): def parse(self, data):
return {self.id: self.converter.convert_from_form(self.id in data and data[self.id][0] == "on")} 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): class Option(object):
# used for both MultiCheckboxInput and DropdownInput # used for both MultiCheckboxInput and DropdownInput
@ -186,7 +228,7 @@ class MultiCheckboxInput(Input):
def render_checkbox(self, option, value): def render_checkbox(self, option, value):
return """ return """
<div class="{classes}"> <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}"> <label class="form-check-label" for="{id}">
{checkboxText} {checkboxText}
</label> </label>
@ -196,6 +238,7 @@ class MultiCheckboxInput(Input):
classes=self.input_classes(), classes=self.input_classes(),
checked="checked" if option.value in value else "", checked="checked" if option.value in value else "",
checkboxText=option.text, checkboxText=option.text,
disabled="disabled" if self.disabled else "",
) )
def parse(self, data): def parse(self, data):
@ -242,9 +285,12 @@ class DropdownInput(Input):
def render_input(self, value): def render_input(self, value):
return """ return """
<select class="{classes}" id="{id}" name="{id}">{options}</select> <select class="{classes}" id="{id}" name="{id}" {disabled}>{options}</select>
""".format( """.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): def render_options(self, value):

View File

@ -16,12 +16,12 @@ class GainInput(Input):
return """ return """
<div id="{id}"> <div id="{id}">
<select class="{classes}" id="{id}-select" name="{id}-select"> <select class="{classes}" id="{id}-select" name="{id}-select" {disabled}>
{options} {options}
</select> </select>
<div class="option manual" style="display: none;"> <div class="option manual" style="display: none;">
<input type="number" id="{id}-manual" name="{id}-manual" value="{value}" class="{classes}" <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> </div>
{stageoption} {stageoption}
</div> </div>
@ -32,6 +32,7 @@ class GainInput(Input):
label=self.label, label=self.label,
options=self.render_options(value), options=self.render_options(value),
stageoption="" if self.gain_stages is None else self.render_stage_option(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): def render_options(self, value):
@ -79,15 +80,16 @@ class GainInput(Input):
inputs="".join( inputs="".join(
""" """
<div class="row"> <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}" <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> </div>
""".format( """.format(
id=self.id, id=self.id,
stage=stage, stage=stage,
value=value_dict[stage] if stage in value_dict else "", value=value_dict[stage] if stage in value_dict else "",
classes=self.input_classes(), classes=self.input_classes(),
disabled="disabled" if self.disabled else "",
) )
for stage in self.gain_stages for stage in self.gain_stages
) )

View File

@ -22,7 +22,7 @@ class Q65ModeMatrix(Input):
id=self.checkbox_id(mode, interval), id=self.checkbox_id(mode, interval),
checked="checked" if "{}{}".format(mode.name, interval.value) in value else "", checked="checked" if "{}{}".format(mode.name, interval.value) in value else "",
checkboxText="Mode {} interval {}s".format(mode.name, interval.value), 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): def render_input(self, value):
@ -69,7 +69,7 @@ class WsjtDecodingDepthsInput(Input):
) )
return """ 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;"> <div class="inputs" style="display:none;">
<select class="form-control form-control-sm">{options}</select> <select class="form-control form-control-sm">{options}</select>
<input class="form-control form-control-sm" type="number" step="1"> <input class="form-control form-control-sm" type="number" step="1">
@ -79,6 +79,7 @@ class WsjtDecodingDepthsInput(Input):
classes=self.input_classes(), classes=self.input_classes(),
value=html.escape(value), value=html.escape(value),
options="".join(render_mode(m) for m in Modes.getAvailableModes() if isinstance(m, WsjtMode)), options="".join(render_mode(m) for m in Modes.getAvailableModes() if isinstance(m, WsjtMode)),
disabled="disabled" if self.disabled else ""
) )
def input_classes(self): def input_classes(self):

View File

@ -375,10 +375,76 @@ class SdrDeviceDescriptionMissing(Exception):
pass pass
class SdrDeviceDescription(object): class OptionalSection(Section):
def __init__(self): def __init__(self, title, inputs: List[Input], mandatory, optional):
self.indexedInputs = {input.id: input for input in self.getInputs()} 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 @staticmethod
def getByType(sdr_type: str) -> "SdrDeviceDescription": def getByType(sdr_type: str) -> "SdrDeviceDescription":
try: try:
@ -407,7 +473,6 @@ class SdrDeviceDescription(object):
), ),
CheckboxInput( CheckboxInput(
"services", "services",
"",
"Run background services on this device", "Run background services on this device",
converter=OptionalConverter(defaultFormValue=True), converter=OptionalConverter(defaultFormValue=True),
), ),
@ -431,8 +496,5 @@ class SdrDeviceDescription(object):
def getOptionalKeys(self): def getOptionalKeys(self):
return ["ppm", "always-on", "services", "rf_gain", "lfo_offset", "waterfall_min_level", "waterfall_max_level"] return ["ppm", "always-on", "services", "rf_gain", "lfo_offset", "waterfall_min_level", "waterfall_max_level"]
def getSection(self, data): def getSection(self):
visible_keys = set(self.getMandatoryKeys() + [k for k in self.getOptionalKeys() if k in data]) return OptionalSection("Device settings", self.getInputs(), self.getMandatoryKeys(), self.getOptionalKeys())
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)