1
0
Fork 0

Implemented dial with optional value display, ticks and wrapping

This commit is contained in:
TheFGFSEagle 2023-02-28 22:21:35 +01:00 committed by James Turner
parent 4fb22e821a
commit 638b6182d6
8 changed files with 481 additions and 33 deletions

View file

@ -39,6 +39,7 @@ loadGUIFile("styles/DefaultStyle.nas");
# widgets
loadWidget("Button");
loadWidget("CheckBox");
loadWidget("Dial");
loadWidget("Label");
loadWidget("LineEdit");
loadWidget("List");

View file

@ -221,6 +221,48 @@ var WidgetsFactoryDialog = {
})
.setValue(42);
m.numericControlsTab.addItem(m.slider);
m.dialBox = HBoxLayout.new();
m.dialBox.setContentsMargin(10);
m.numericControlsTab.addItem(m.dialBox);
m.dial = gui.widgets.Dial.new(m.tabsContent, style, {
"min-value": 5,
"max-value": 50,
"step-size": 0.5,
"page-size": 5,
"tick-step": 2,
"show-value": 0,
"show-ticks": 0,
"value": 14,
"value-format": "%.1f",
"wrap": 0,
});
m.dialBox.addItem(m.dial);
m.dialOptionsBox =VBoxLayout.new();
m.dialBox.addItem(m.dialOptionsBox);
m.dialShowValueCheckBox = gui.widgets.CheckBox.new(m.tabsContent, canvas.style, {})
.setText("Show value")
.listen("toggled", func(e) {
m.dial.setShowValue(e.detail.checked);
});
m.dialShowValueCheckBox.setAlignment(canvas.AlignTop);
m.dialOptionsBox.addItem(m.dialShowValueCheckBox);
m.dialShowTicksCheckBox = gui.widgets.CheckBox.new(m.tabsContent, canvas.style, {})
.setText("Show ticks")
.listen("toggled", func(e) {
m.dial.setShowTicks(e.detail.checked);
});
m.dialShowTicksCheckBox.setAlignment(canvas.AlignTop);
m.dialOptionsBox.addItem(m.dialShowTicksCheckBox);
m.dialWrapCheckBox = gui.widgets.CheckBox.new(m.tabsContent, canvas.style, {})
.setText("Wrap value")
.listen("toggled", func(e) {
m.dial.setWrap(e.detail.checked);
});
m.dialWrapCheckBox.setAlignment(canvas.AlignTop);
m.dialOptionsBox.addItem(m.dialWrapCheckBox);
return m;

View file

@ -1138,6 +1138,205 @@ DefaultStyle.widgets.slider = {
}
};
DefaultStyle.widgets.dial = {
new: func(parent, cfg) {
me._root = parent.createChild("group", "dial");
me._knob = me._root.createChild("path", "dial-knob");
me._handle = me._root.createChild("image", "dial-handle");
me._handleTranslateTransform = me._handle.createTransform();
me._value = me._root.createChild("text", "dial-value")
.set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", me._style.getSize("dial-value-font-size", me._style.getSize("base-font-size", 14)))
.set("alignment", "center-center")
.setText(0);
me._ticks = me._root.createChild("path", "dial-ticks");
me._maxValueWidth = 20;
},
_updateLayoutSizes: func(model) {
var handleSize = me._handle.imageSize()[1];
var borderHandleMargin = math.clamp(
math.min(model._size[0], model._size[1]) * 0.0625,
2,
me._style.getSize("dial-border-handle-margin", 8),
);
var valueSize = [0, 0];
var valueHandleMargin = me._style.getSize("dial-value-handle-margin", 5);
if (model._showValue) {
valueSize = [
me._maxValueWidth + valueHandleMargin * 2,
me._style.getSize("dial-value-font-size", me._style.getSize("base-font-size", 14)) + valueHandleMargin * 2
];
} else {
valueSize = me._style.getSize("dial-center-handle-margin", 2) * 2;
valueSize = [valueSize, valueSize];
}
var minW = borderHandleMargin * 2 + valueSize[0] + handleSize * 2;
var minH = borderHandleMargin * 2 + valueSize[1] + handleSize * 2;
var length = math.max(minW, minH);
if (model._showTicks) {
length += me._style.getSize("dial-knob-ticks-margin", 2) + me._style.getSize("dial-ticks-length", 10);
}
model.setLayoutMinimumSize([length, length]);
},
setSize: func(model, length) {
var knobTicksMargin = me._style.getSize("dial-knob-ticks-margin", 2);
var ticksLength = me._style.getSize("dial-ticks-length", 10);
var knobRadius = (length - me._style.getSize("dial-knob-border-width", 1)) / 2;
var halfLength = length / 2;
if (model._showTicks) {
knobRadius -= knobTicksMargin + ticksLength;
}
me._knob.reset()
.circle(knobRadius, halfLength, halfLength);
me._value.setTranslation(
halfLength,
halfLength
);
var handleOffset = [
halfLength - me._handle.imageSize()[0] / 2,
math.clamp(knobRadius * 0.125, 2, me._style.getSize("dial-border-handle-margin", 8))
];
if (model._showTicks) {
handleOffset[1] += knobTicksMargin + ticksLength;
}
me._handleTranslateTransform.setTranslation(handleOffset[0], handleOffset[1]);
me._handle.setCenter(
me._handle.imageSize()[0] / 2,
halfLength - handleOffset[1]
);
var degreesRange = 360;
var nowrapMargin = me._style.getSize("dial-nowrap-margin-deg", 30);
var offset = 0;
if (!model._wraps) {
degreesRange -= nowrapMargin;
offset = nowrapMargin / 2;
}
me._handle.setRotation((model._normValue() * degreesRange + offset) * D2R);
me._drawTicks(model);
},
_drawTicks: func(model) {
var length = math.min(model._size[0], model._size[1]);
var halfLength = length / 2;
var knobTicksMargin = me._style.getSize("dial-knob-ticks-margin", 2);
var ticksLength = me._style.getSize("dial-ticks-length", 10);
var knobRadius = (length - me._style.getSize("dial-knob-border-width", 1)) / 2 - knobTicksMargin - ticksLength;
var degreesRange = 360;
var nowrapMargin = me._style.getSize("dial-nowrap-margin-deg", 30);
var offset = 0;
if (!model._wraps) {
degreesRange -= nowrapMargin;
offset = nowrapMargin / 2;
}
var range = model._maxValue - model._minValue;
var center = [halfLength, halfLength];
var degreesPerTickStep = (degreesRange / range) * model._tickStep;
var lengthBegin = knobRadius + knobTicksMargin;
var lengthEnd = lengthBegin + ticksLength;
me._ticks.reset();
for (var i = 0; i < degreesRange / degreesPerTickStep; i += 1) {
var radians = (i * degreesPerTickStep + offset) * D2R;
var sin = math.sin(radians);
var cos = -math.cos(radians);
var start = [center[0] + sin * lengthBegin, center[1] + cos * lengthBegin];
var tick = [center[0] + sin * lengthEnd, center[1] + cos * lengthEnd];
me._ticks.moveTo(start[0], start[1]).lineTo(tick[0], tick[1]);
}
},
_updateMaxValueWidth: func(model) {
if (model._valueFormat != nil) {
me._value.setText(sprintf(model._valueFormat, model._minValue));
var min = me._value.maxWidth();
me._value.setText(sprintf(model._valueFormat, model._maxValue));
var max = me._value.maxWidth();
me._value.setText(sprintf(model._valueFormat, model._minValue + model._stepSize));
var minStep = me._value.maxWidth();
me._value.setText(sprintf(model._valueFormat, model._value));
} else {
me._value.setText(model._minValue);
var min = me._value.maxWidth();
me._value.setText(model._maxValue);
var max = me._value.maxWidth();
me._value.setText(model._minValue + model._stepSize);
var minStep = me._value.maxWidth();
me._value.setText(model._value);
}
me._maxValueWidth = math.max(min, max, minStep);
},
setValue: func(model, value) {
var degreesRange = 360;
var nowrapMargin = me._style.getSize("dial-nowrap-margin-deg", 30);
var offset = 0;
if (!model._wraps) {
degreesRange -= nowrapMargin;
offset = nowrapMargin / 2;
}
me._handle.setRotation((model._normValue() * degreesRange + offset) * D2R);
if (model._valueFormat != nil) {
me._value.setText(sprintf(model._valueFormat, value));
} else {
me._value.setText(value);
}
},
update: func(model) {
var color_name = model._windowFocus() ? "fg_color" : "backdrop_fg_color";
me._value.set("fill", me._style.getColor(color_name));
color_name = "dial_knob_bg";
if (!model._enabled) {
color_name ~= "_disabled";
} elsif (model._focused and model._windowFocus()) {
color_name ~= "_focused";
} elsif (!model._windowFocus()) {
color_name ~= "_backdrop";
}
if (model._hover and model._enabled and model._windowFocus()) {
color_name ~= "_hovered";
}
me._knob.set("fill", me._style.getColor(color_name));
color_name = "dial_knob_border";
if (!model._enabled) {
color_name ~= "_disabled";
} elsif (model._focused and model._windowFocus()) {
color_name ~= "_focused";
}
if (model._hover and model._enabled and model._windowFocus()) {
color_name ~= "_hovered";
}
me._knob.set("stroke", me._style.getColor(color_name));
var file = me._style._dir_widgets ~ "/dial-handle";
if (!model._enabled) {
file ~= "-disabled";
} elsif (model._handleDown) {
file ~= "-down";
}
me._handle.set("src", file ~ ".png");
me._ticks.set("stroke", me._style.getColor("dial_ticks"));
if (model._showValue) {
me._value.show();
} else {
me._value.hide();
}
if (model._showTicks) {
me._ticks.show();
} else {
me._ticks.hide();
}
}
};
DefaultStyle.widgets["menu-item"] = {
new: func(parent, cfg) {
me._root = parent.createChild("group", "menu-item");

View file

@ -1,61 +1,195 @@
# Dial.nas : show a user-rotable dial knob
# with optional tick marks and value display
# SPDX-FileCopyrightText: (C) 2022 James Turner <james@flightgear.org>
# SPDX-License-Identifier: GPL-2.0-or-later
gui.widgets.Dial = {
new: func(parent, style, cfg)
{
var cfg = Config.new(cfg);
new: func(parent, style = nil, cfg = nil) {
style = style or canvas.style;
var m = gui.Widget.new(gui.widgets.Dial);
m._cfg = Config.new(cfg or {});
m._focus_policy = m.StrongFocus;
m._down = 0;
m._minValue = 0;
m._maxValue = cfg.get("max-value", 100);
m._value = 50;
m._minValue = m._cfg.get("min-value", 0);
m._maxValue = m._cfg.get("max-value", 100);
m._value = m._mouseValue = m._cfg.get("value", 50);
m._stepSize = m._cfg.get("step-size", 1);
m._pageSize = m._cfg.get("page-size", 10);
m._tickStep = m._cfg.get("tick-step", 10);
m._showTicks = m._cfg.get("show-ticks", 0);
m._showValue = m._cfg.get("show-value", 1);
m._valueFormat = m._cfg.get("value-format", nil);
m._wraps = m._cfg.get("wrap", 0);
m._wraps = cfg.get("wrap", 0);
m._pageStep = cfg.get("page-step", 0);
m._numTicks = cfg.get("tick-count", 0);
m._tickStyle = cfg.get("ticks-style", 0);
m._handleDown = 0;
m._lastMouseAngle = 0;
m._dragging = 0;
# todo : optional value display in the center
if( style != nil ) {
m._setView( style.createWidget(parent, cfg.get("type", "slider"), cfg) );
m._view.updateRanges(m._minValue, m._maxValue, m._numTicks);
}
m._setView(style.createWidget(parent, m._cfg.get("type", "dial"), m._cfg));
m.setValueFormat(m._valueFormat);
m.setValue(m._value);
m.setMinValue(m._minValue);
m.setMaxValue(m._maxValue);
m.setShowTicks(m._showTicks);
m.setShowValue(m._showValue);
m._onStateChange();
return m;
},
setValue: func(val)
{
if( me._view != nil ) {
me._view.setNormValue(me._normValue());
setShowTicks: func(showTicks) {
me._showTicks = showTicks;
me._view._updateLayoutSizes(me);
me._onStateChange();
},
setShowValue: func(showValue) {
me._showValue = showValue;
me._view._updateLayoutSizes(me);
me._onStateChange();
},
setWrap: func(wrap) {
me._wraps = wrap;
me._view.setValue(me, me._value);
me._view._drawTicks(me);
},
setSize: func(w, h) {
var size = math.min(w, h);
me._size[0] = size;
me._size[1] = size;
me._view.setSize(me, size, size);
return me;
},
setValueFormat: func(format) {
me._valueFormat = format;
me._view._updateMaxValueWidth(me);
me._view._updateLayoutSizes(me);
me.setValue(me._value);
},
setValue: func(value) {
value = math.clamp(value, me._minValue, me._maxValue);
me._view.setValue(me, value);
if (me._value != value) {
me._value = value;
me._trigger("value-changed", {"value": value});
}
return me;
},
setMinValue: func(minValue) {
me._minValue = minValue;
me._view._updateMaxValueWidth(me);
me._view._updateLayoutSizes(me);
me._view._drawTicks(me);
},
setMaxValue: func(maxValue) {
me._maxValue = maxValue;
me._view._updateMaxValueWidth(me);
me._view._updateLayoutSizes(me);
me._view._drawTicks(me);
},
# protected:
_setView: func(view)
{
_setView: func(view) {
call(gui.Widget._setView, [view], me);
# var el = view._root;
# el.addEventListener("mousedown", func if( me._enabled ) me.setDown(1));
# el.addEventListener("mouseup", func if( me._enabled ) me.setDown(0));
# el.addEventListener("click", func if( me._enabled ) me.toggle());
var el = view._root;
el.addEventListener("drag", func(e) {
me._dragDial(e);
e.stopPropagation();
});
el.addEventListener("mousedown", func(e) {
me._handleDown = 1;
me._dragDial(e);
me._onStateChange();
});
el.addEventListener("mouseup", func(e) {
me._handleDown = 0;
me._dragging = 0;
me._onStateChange();
});
el.addEventListener("mouseleave", func(e) {
me._handleDown = 0;
me._dragging = 0;
me._onStateChange();
});
el.addEventListener("wheel", func(e) {
if (!me._enabled) {
return;
}
# el.addEventListener("mouseleave",func me.setDown(0));
# el.addEventListener("drag", func(e) e.stopPropagation());
me.setValue(me._value + e.deltaY * me._stepSize);
e.stopPropagation();
});
el.addEventListener("keydown", func(e) {
var value = me._value;
if (contains([
keyboard.FunctionKeys.Left, keyboard.FunctionKeys.KP_Left,
keyboard.FunctionKeys.Down, keyboard.FunctionKeys.KP_Down,
keyboard.PrintableKeys.Minus, keyboard.FunctionKeys.KP_Subtract,
], e.keyCode)) {
value -= me._stepSize;
} elsif (contains([
keyboard.FunctionKeys.Right, keyboard.FunctionKeys.KP_Right,
keyboard.FunctionKeys.Up, keyboard.FunctionKeys.KP_Up,
keyboard.PrintableKeys.Plus, keyboard.FunctionKeys.KP_Add,
], e.keyCode)) {
value += me._stepSize;
} elsif (contains([keyboard.FunctionKeys.Page_Down, keyboard.FunctionKeys.KP_Page_Down], e.keyCode)) {
value -= me._pageSize;
} elsif (contains([keyboard.FunctionKeys.Page_Up, keyboard.FunctionKeys.KP_Page_Up], e.keyCode)) {
value += me._pageSize;
} elsif (contains([keyboard.FunctionKeys.Home, keyboard.FunctionKeys.KP_Home], e.keyCode)) {
value = me._minValue;
} elsif (contains([keyboard.FunctionKeys.End, keyboard.FunctionKeys.KP_End], e.keyCode)) {
value = me._maxValue;
}
me.setValue(value);
});
},
_dragDial: func(e) {
if (!me._enabled) {
return;
}
var vr = me._view._root;
var localPos = vr.canvasToLocal([e.clientX - me._size[0] / 2, e.clientY - me._size[1] / 2]);
var mouseAngle = math.atan2(localPos[0], -localPos[1]) * R2D + 180;
var deltaAngle = math.periodic(-180, 180, mouseAngle - me._lastMouseAngle);
me._lastMouseAngle = mouseAngle;
if (!me._dragging) {
me._dragging = 1;
return;
}
var value = me._mouseValue + (me._maxValue - me._minValue) * (deltaAngle / 360);
if (!me._wraps) {
if (value > me._maxValue) {
value = me._maxValue;
} elsif (value < me._minValue) {
value = me._minValue;
}
} else {
value = math.periodic(me._minValue, me._maxValue, value);
}
me._mouseValue = value;
if (me._stepSize != 0) {
value = math.round(value / me._stepSize) * me._stepSize;
}
me.setValue(value);
},
# return value as its normalised equivalent
_normValue: func
{
_normValue: func {
var range = me._maxValue - me._minValue;
var v = math.clamp(me._value, me._minValue, me._maxValue) - me._minValue;
return v / range;
}
},
};

View file

@ -317,6 +317,78 @@
<green type="float">0.5</green>
<blue type="float">0.5</blue>
</slider_ticks>
<dial_knob_bg>
<red type="float">0.949</red>
<green type="float">0.945</green>
<blue type="float">0.941</blue>
</dial_knob_bg>
<dial_knob_bg_backdrop>
<red type="float">0.934</red>
<green type="float">0.930</green>
<blue type="float">0.926</blue>
</dial_knob_bg_backdrop>
<dial_knob_bg_focused>
<red type="float">0.966</red>
<green type="float">0.962</green>
<blue type="float">0.958</blue>
</dial_knob_bg_focused>
<dial_knob_bg_hovered>
<red type="float">0.966</red>
<green type="float">0.962</green>
<blue type="float">0.988</blue>
</dial_knob_bg_hovered>
<dial_knob_bg_focused_hovered>
<red type="float">0.986</red>
<green type="float">0.982</green>
<blue type="float">0.998</blue>
</dial_knob_bg_focused_hovered>
<dial_knob_bg_disabled>
<red type="float">0.89</red>
<green type="float">0.89</green>
<blue type="float">0.89</blue>
</dial_knob_bg_disabled>
<dial_knob_border>
<red type="float">0.6</red>
<green type="float">0.6</green>
<blue type="float">0.6</blue>
</dial_knob_border>
<dial_knob_border_hovered>
<red type="float">0.65</red>
<green type="float">0.65</green>
<blue type="float">0.65</blue>
</dial_knob_border_hovered>
<dial_knob_border_focused>
<red type="float">0.83</red>
<green type="float">0.54</green>
<blue type="float">0.43</blue>
</dial_knob_border_focused>
<dial_knob_border_focused_hovered>
<red type="float">0.79</red>
<green type="float">0.41</green>
<blue type="float">0.25</blue>
</dial_knob_border_focused_hovered>
<dial_knob_border_disabled>
<red type="float">0.71</red>
<green type="float">0.71</green>
<blue type="float">0.71</blue>
</dial_knob_border_disabled>
<dial_ticks>
<red type="float">0.5</red>
<green type="float">0.5</green>
<blue type="float">0.5</blue>
</dial_ticks>
</colors>
<sizes>

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B