248 lines
7.2 KiB
Python
248 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import math
|
|
|
|
from nanogui import Color, Cursor, FloatBox, Vector2f, Vector2i, Widget, glfw
|
|
from nanogui.nanovg import BUTT, RGB, RGBA, RGBAf, LerpRGBA, NVGwinding, RGBAf
|
|
|
|
|
|
class Knob(Widget):
|
|
def __init__(
|
|
self,
|
|
parent,
|
|
label,
|
|
default=0.0,
|
|
min_val=0.0,
|
|
max_val=127.0,
|
|
unit="%",
|
|
increment=0.1,
|
|
size=80,
|
|
color=RGBA(192, 192, 192, 255),
|
|
):
|
|
super().__init__(parent)
|
|
self.label = label
|
|
|
|
# sizes
|
|
self._size = size
|
|
# value gauge width as ratio of knob radius
|
|
self.gauge_width = 0.125
|
|
# value indicator line length as ratio of knob radius
|
|
self.indicator_size = 0.35
|
|
|
|
# value and range
|
|
self.value = default
|
|
self.min_val = min_val
|
|
self.max_val = max_val
|
|
self.default = default
|
|
self.unit = unit
|
|
self.increment = increment
|
|
|
|
# behaviour
|
|
self.fine_mode = False
|
|
self.scroll_speed = 2.0
|
|
self.scroll_increment = (self.max_val - self.min_val) / 100.0 * self.scroll_speed
|
|
self.drag_increment = (self.max_val - self.min_val) / 100.0
|
|
self.callback = None
|
|
self.set_cursor(Cursor.Hand)
|
|
|
|
# colors
|
|
self.color = color
|
|
self.knob_color_1 = RGB(86, 92, 95)
|
|
self.knob_color_2 = RGB(39, 42, 43)
|
|
self.outline_color_1 = RGBA(190, 190, 190, 0.0)
|
|
self.outline_color_2 = RGB(23, 23, 23)
|
|
|
|
@property
|
|
def color(self):
|
|
return self._color
|
|
|
|
@color.setter
|
|
def color(self, col):
|
|
if isinstance(col, Color):
|
|
col = RGBAf(col.r, col.g, col.b, col.w)
|
|
|
|
self._color = col
|
|
self.gauge_color = LerpRGBA(RGB(40, 40, 40), col, 0.3)
|
|
|
|
def preferred_size(self, ctx):
|
|
return Vector2i(self._size, self._size)
|
|
|
|
def _adjust_value(self, val, inc):
|
|
if self.fine_mode:
|
|
inc = self.increment
|
|
|
|
new_val = self.value + val * inc
|
|
new_val = max(self.min_val, min(self.max_val, new_val))
|
|
|
|
if new_val != self.value:
|
|
self.value = new_val
|
|
|
|
if self.callback:
|
|
self.callback(new_val)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
def mouse_enter_event(self, pos: Vector2i, enter: bool):
|
|
if enter:
|
|
self.request_focus()
|
|
|
|
self.set_focused(enter)
|
|
return True
|
|
|
|
def mouse_button_event(self, pos: Vector2i, button: int, down: bool, modifiers: int):
|
|
if button == glfw.MOUSE_BUTTON_1 and modifiers == glfw.MOD_CONTROL and down:
|
|
if self.value != self.default:
|
|
self.value = self.default
|
|
|
|
if self.callback:
|
|
self.callback(self.value)
|
|
|
|
return True
|
|
|
|
def mouse_drag_event(self, p: Vector2i, rel: Vector2i, button: int, modifiers: int):
|
|
self._adjust_value(-rel[1], self.drag_increment)
|
|
return True
|
|
|
|
def scroll_event(self, p: Vector2i, rel: Vector2f):
|
|
self._adjust_value(rel[1], self.scroll_increment)
|
|
return True
|
|
|
|
def keyboard_event(self, key, scancode, action, modifiers):
|
|
if key in (glfw.KEY_LEFT_SHIFT, glfw.KEY_RIGHT_SHIFT):
|
|
if action == glfw.PRESS:
|
|
self.fine_mode = True
|
|
elif action == glfw.RELEASE:
|
|
self.fine_mode = False
|
|
return True
|
|
elif action in (glfw.PRESS, glfw.REPEAT):
|
|
if key == glfw.KEY_UP:
|
|
self._adjust_value(1, self.drag_increment)
|
|
return True
|
|
elif key == glfw.KEY_DOWN:
|
|
self._adjust_value(-1, self.drag_increment)
|
|
return True
|
|
elif key == glfw.KEY_PAGE_UP:
|
|
self._adjust_value(10, self.drag_increment)
|
|
return True
|
|
elif key == glfw.KEY_PAGE_DOWN:
|
|
self._adjust_value(-10, self.drag_increment)
|
|
return True
|
|
|
|
return False
|
|
|
|
def set_callback(self, callback):
|
|
if callable(callback):
|
|
self.callback = callback
|
|
else:
|
|
raise TypeError("Not a callable.")
|
|
|
|
def set_value(self, value, trigger_callback=False):
|
|
new_val = max(self.min_val, min(self.max_val, value))
|
|
if new_val != self.value:
|
|
self.value = new_val
|
|
|
|
if trigger_callback and self.callback:
|
|
self.callback(value)
|
|
|
|
def create_value_entry(self, parent, editable=True, font_size=20, widget=FloatBox, **kwargs):
|
|
entry = widget(parent, **kwargs)
|
|
entry.set_size((int(self._size * 1.1), int(font_size * 1.2)))
|
|
entry.set_font_size(font_size)
|
|
entry.set_editable(editable)
|
|
entry.set_spinnable(True)
|
|
entry.set_value(self.value)
|
|
# entry.number_format("%02.2f") # method missing in Python bindings
|
|
entry.set_min_value(self.min_val)
|
|
entry.set_max_value(self.max_val)
|
|
entry.set_value_increment(self.increment)
|
|
|
|
if self.unit:
|
|
entry.set_units(self.unit)
|
|
|
|
self.set_callback(entry.set_value)
|
|
entry.set_callback(self.set_value)
|
|
|
|
return entry
|
|
|
|
def draw(self, ctx):
|
|
pos = self.position()
|
|
size = self.size()
|
|
height = float(size[1])
|
|
radius = height / 2.0
|
|
|
|
gauge_width = radius * self.gauge_width
|
|
margin = gauge_width / 2.0
|
|
percent_filled = (self.value - self.min_val) / (self.max_val - self.min_val)
|
|
knob_diameter = (radius - gauge_width) * 2.0 - margin
|
|
indicator_length = radius * self.indicator_size
|
|
|
|
ctx.Save()
|
|
ctx.Translate(pos[0], pos[1])
|
|
|
|
# Gauge (background)
|
|
ctx.BeginPath()
|
|
|
|
ctx.StrokeWidth(gauge_width)
|
|
ctx.StrokeColor(self.gauge_color)
|
|
ctx.LineCap(BUTT.ROUND)
|
|
ctx.Arc(radius, radius, radius - margin, 0.75 * math.pi, 0.25 * math.pi, NVGwinding.CW)
|
|
ctx.Stroke()
|
|
|
|
# Gauge (fill)
|
|
ctx.BeginPath()
|
|
|
|
ctx.StrokeWidth(gauge_width)
|
|
ctx.StrokeColor(self.color)
|
|
ctx.LineCap(BUTT.ROUND)
|
|
ctx.Arc(
|
|
radius,
|
|
radius,
|
|
radius - margin,
|
|
0.75 * math.pi,
|
|
(0.75 + 1.5 * percent_filled) * math.pi,
|
|
NVGwinding.CW,
|
|
)
|
|
ctx.Stroke()
|
|
|
|
# Knob
|
|
ctx.BeginPath()
|
|
|
|
ctx.StrokeWidth(2.0)
|
|
outline_paint = ctx.LinearGradient(
|
|
0,
|
|
0,
|
|
0,
|
|
height - 10,
|
|
self.outline_color_1,
|
|
self.outline_color_2,
|
|
)
|
|
ctx.StrokePaint(outline_paint)
|
|
|
|
knob_paint = ctx.LinearGradient(
|
|
radius, gauge_width, radius, knob_diameter, self.knob_color_1, self.knob_color_2
|
|
)
|
|
ctx.FillPaint(knob_paint)
|
|
|
|
ctx.Circle(radius, radius, knob_diameter / 2.0)
|
|
ctx.Fill()
|
|
ctx.Stroke()
|
|
|
|
# Indicator
|
|
ctx.BeginPath()
|
|
|
|
ctx.Translate(radius, radius)
|
|
ctx.Rotate((2.0 + ((percent_filled - 0.5) * 1.5)) * math.pi)
|
|
|
|
ctx.StrokeColor(self.color)
|
|
ctx.StrokeWidth(gauge_width)
|
|
ctx.LineCap(BUTT.ROUND)
|
|
indicator_start = radius - margin - indicator_length
|
|
ctx.MoveTo(0, -indicator_start)
|
|
ctx.LineTo(0, -(indicator_start + indicator_length))
|
|
ctx.Stroke()
|
|
|
|
ctx.Restore()
|
|
ctx.ClosePath()
|