Loading...
Searching...
No Matches
DefaultGraphicsKnobImpl.hpp
1#pragma once
2#include <score/graphics/WidgetPresentation.hpp>
3#include <score/graphics/DefaultControlImpl.hpp>
4#include <score/graphics/RightClickWidget.hpp>
5#include <score/graphics/InfiniteScroller.hpp>
6#include <score/graphics/widgets/Constants.hpp>
7#include <score/model/Skin.hpp>
8#include <score/tools/Cursor.hpp>
9#include <score/widgets/DoubleSpinBox.hpp>
10#include <score/widgets/SignalUtils.hpp>
11
12#include <ossia/detail/math.hpp>
13
14#include <QDoubleSpinBox>
15#include <QGraphicsProxyWidget>
16#include <QGraphicsScene>
17#include <QGraphicsSceneMouseEvent>
18#include <QGuiApplication>
19#include <QPainter>
20#include <QPointer>
21#include <QScreen>
22#include <QTimer>
23
24namespace score
25{
27{
28 template <typename T>
29 static void paint(
30 T& self, const score::Skin& skin, const QString& text, QPainter* painter,
31 QWidget* widget, const QStyleOptionGraphicsItem* option = nullptr)
32 {
33 painter->setRenderHint(QPainter::Antialiasing, true);
34
35 constexpr const double space = 50.;
36 constexpr const double start = (270. - space) * 16.;
37 constexpr const double totalSpan = (360. - 2. * space) * 16.;
38
39 // Only knobs have m_rect (WidgetPresentation); e.g. the time chooser does not.
40 QRectF srect = defaultKnobSize;
41 if constexpr(requires { self.m_rect; })
42 srect = self.m_rect;
43 // Taller than wide: dial in the top square, value below
44 const double side = std::min(srect.width(), srect.height());
45
46 // Geometry of the design at the 35 px reference size: the arc's centre
47 // line at a 10.5 px radius, 2 px wide; the tick from the arc's outer
48 // edge (11.5 px) to 4.94 px from the centre, 2 px wide, round ends; the
49 // body filled out to the arc's outer edge, so the arc lies on its rim. A
50 // round cap reaches half the pen width past its endpoint: the endpoints
51 // are pulled in so the ends are at those radii.
52 const double k = side / 35.;
53 const QPointF c{side / 2., side / 2.};
54 const double arcRadius = 10.5 * k;
55 // The arc and the tick share one pen
56 const double arcWidth = std::max(1., 2. * k);
57 const double tickWidth = arcWidth;
58 // Antialiased, a round end still bleeds past its geometric edge by a
59 // fraction of a device pixel more than the arc's own edge does, most at
60 // small zooms: half a device pixel of margin keeps it inside the rim.
61 const QTransform& dt = painter->deviceTransform();
62 const double devicePx
63 = 1. / std::max(1e-6, std::sqrt(std::abs(dt.m11() * dt.m22() - dt.m12() * dt.m21())));
64 const double tickOuter
65 = arcRadius + arcWidth / 2. - tickWidth / 2. - 0.5 * devicePx;
66 const double tickInner = 4.94 * k + tickWidth / 2.;
67 const QRectF r{
68 c.x() - arcRadius, c.y() - arcRadius, 2. * arcRadius, 2. * arcRadius};
69 const double bodyRadius = arcRadius + arcWidth / 2.;
70 const QRectF body{
71 c.x() - bodyRadius, c.y() - bodyRadius, 2. * bodyRadius, 2. * bodyRadius};
72
73 const QColor accent = skin.Base4.main.brush.color();
74
75 // Body
76 painter->setPen(Qt::NoPen);
77 painter->setBrush(skin.Emphasis2.main.brush);
78 painter->drawChord(body, start, -totalSpan);
79 painter->setBrush(Qt::NoBrush);
80
81 // The value; no track around the rest of the course (the body shows it)
82 const double valueSpan = -self.m_value * totalSpan;
83 painter->setPen(QPen{accent, arcWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin});
84 if(self.m_value > 0.)
85 painter->drawArc(r, start, valueSpan);
86
87 // Tick, joined to the arc
88 const double theta = -0.0174533 * (start + valueSpan) / 16.;
89 const QPointF dir{std::cos(theta), std::sin(theta)};
90 painter->drawLine(c + tickInner * dir, c + tickOuter * dir);
91
92 // Where the execution is, inside the arc
93 if(self.m_hasExec)
94 {
95 painter->setPen(skin.Base4.lighter180.pen1);
96 const double er = arcRadius - arcWidth / 2. - 1.;
97 const QRectF erect{c.x() - er, c.y() - er, 2. * er, 2. * er};
98 painter->drawArc(erect, start, -self.m_execValue * totalSpan);
99 }
100
101 const double textDelta = side >= 30. ? -10. : side >= 20. ? -9. : -8.;
102 // The value in the same colour as under a slider.
103 painter->setPen(skin.Base4.lighter180.pen1);
104
105 // Draw text
106 // Non-item wrappers (multi-slider rows) always show their value
107 bool showValue = true;
108 if constexpr(std::is_base_of_v<QGraphicsItem, std::remove_cvref_t<T>>)
109 {
110 bool grabbed = false;
111 if constexpr(requires { self.m_grab; })
112 grabbed = self.m_grab;
113 showValue = showsValue(self, grabbed, option);
114 }
115 if(showValue && !text.isEmpty())
116 {
117 painter->setFont(skin.Medium8Pt);
118 const double text_y
119 = srect.height() > srect.width() ? side : srect.height() + textDelta;
120 painter->drawText(
121 QRectF{0., text_y, srect.width(), 10.}, text, QTextOption(Qt::AlignCenter));
122 }
123
124 painter->setRenderHint(QPainter::Antialiasing, false);
125 }
126
127 template <typename T>
128 static void mousePressEvent(T& self, QGraphicsSceneMouseEvent* event)
129 {
130 if(event->button() == Qt::LeftButton)
131 {
132 self.m_grab = true;
133 InfiniteScroller::start(self, self.m_value);
134 }
135
136 event->accept();
137 }
138
139 template <typename T>
140 static void mouseMoveEvent(T& self, QGraphicsSceneMouseEvent* event)
141 {
142 if((event->buttons() & Qt::LeftButton) && self.m_grab)
143 {
144 double v = InfiniteScroller::move(event);
145 if(v != self.m_value)
146 {
147 self.m_value = v;
148 self.sliderMoved();
149 self.update();
150 }
151 }
152 event->accept();
153 }
154
155 template <typename T>
156 static void mouseReleaseEvent(T& self, QGraphicsSceneMouseEvent* event)
157 {
158 // Read the value before ending the session: once the relative-motion
159 // session is over, move() falls back to pointer positions, and under a
160 // pointer lock those never left the press point -- applying that as a delta
161 // unwinds the whole drag.
162 if(self.m_grab)
163 {
164 double v = InfiniteScroller::move(event);
165 if(v != self.m_value)
166 {
167 self.m_value = v;
168 self.update();
169 }
170 InfiniteScroller::stop(self, event);
171 }
172
173 self.m_grab = false;
174 self.sliderReleased();
175
176 if(event->button() == Qt::RightButton)
177 {
178 contextMenuEvent(self, event->scenePos());
179 }
180
181 event->accept();
182 }
183
187 template <typename T>
188 static void ungrabMouseEvent(T& self, QEvent* event)
189 {
190 if(!self.m_grab)
191 return;
192
193 self.m_grab = false;
195 self.sliderReleased();
196 }
197
198 template <typename T>
199 requires std::is_integral_v<std::decay_t<decltype(std::declval<T>().value())>>
200 static void contextMenuEvent(T& self, QPointF pos)
201 {
202 auto build = [&, self_p = &self, pos] {
203 // Whatever box is open belongs to the previous right-click.
205
206 auto w = new SpinboxWithEnter;
207 w->setRange(self.min, self.max);
208
209 w->setValue(self.map(self.m_value));
210 auto obj = self.scene()->addWidget(
211 w, Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint);
212 obj->setPos(pos);
214
215#if defined(__EMSCRIPTEN__)
216 w->setFocus();
217#else
218 QTimer::singleShot(0, w, [w] { w->setFocus(); });
219#endif
220
221 auto con = QObject::connect(
222 w, SignalUtils::QSpinBox_valueChanged_int(), &self,
223 [&self, obj, scene = self.scene()](double v) {
224 DefaultControlImpl::editWidgetInContextMenu(self, scene, obj, v);
225 });
226
227 QObject::connect(
228 w, &SpinboxWithEnter::editingFinished, &self, [obj, con, self_p]() mutable {
229 if(obj != nullptr)
230 {
231 self_p->sliderReleased();
232 QObject::disconnect(con);
233 QTimer::singleShot(0, obj, [scene = self_p->scene(), obj] {
234 scene->removeItem(obj);
235 delete obj;
236 });
237 }
238 obj = nullptr;
239 });
240 };
241#if defined(__EMSCRIPTEN__)
242 build();
243#else
244 QTimer::singleShot(0, &self, build);
245#endif
246 }
247
250 template <typename T>
251 requires(!std::is_integral_v<std::decay_t<decltype(std::declval<T>().value())>>)
252 static void contextMenuEvent(T& self, QPointF pos)
253 {
254 auto build = [&, self_p = &self, pos] {
255 // Whatever box is open belongs to the previous right-click.
257
258 auto w = new DoubleSpinboxWithEnter;
259 w->setRange(self.min, self.max);
260
261 w->setDecimals(6);
262 w->setValue(self.map(self.m_value));
263 auto obj = self.scene()->addWidget(
264 w, Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint);
265 obj->setPos(pos);
267
268#if defined(__EMSCRIPTEN__)
269 w->setFocus();
270#else
271 QTimer::singleShot(0, w, [w] { w->setFocus(); });
272#endif
273
274 auto con = QObject::connect(
275 w, SignalUtils::QDoubleSpinBox_valueChanged_double(), &self,
276 [&self, obj, scene = self.scene()](double v) {
277 DefaultControlImpl::editWidgetInContextMenu(self, scene, obj, v);
278 });
279
280 QObject::connect(
281 w, &DoubleSpinboxWithEnter::editingFinished, &self,
282 [obj, con, self_p]() mutable {
283 if(obj != nullptr)
284 {
285 self_p->sliderReleased();
286 QObject::disconnect(con);
287 QTimer::singleShot(0, obj, [scene = self_p->scene(), obj] {
288 scene->removeItem(obj);
289 delete obj;
290 });
291 }
292 obj = nullptr;
293 });
294 };
295#if defined(__EMSCRIPTEN__)
296 build();
297#else
298 QTimer::singleShot(0, &self, build);
299#endif
300 }
301
302 template <typename T>
303 static void mouseDoubleClickEvent(T& self, QGraphicsSceneMouseEvent* event)
304 {
305 self.m_value = self.unmap(self.init);
306
307 self.m_grab = true;
308 self.sliderMoved();
309 self.sliderReleased();
310 self.m_grab = false;
311
312 self.update();
313
314 event->accept();
315 }
316};
317}
Definition Skin.hpp:100
Base toolkit upon which the software is built.
Definition Application.cpp:83
bool showsValue(const QGraphicsItem &item, bool grabbed, const QStyleOptionGraphicsItem *option) noexcept
Whether to draw the value text. Called on every paint, must stay cheap.
Definition WidgetPresentation.hpp:21
QPointer< QGraphicsProxyWidget > & currentRightClickWidget()
The one type-in box a right-click may have open.
Definition GraphicWidgets.cpp:26
void closeRightClickWidget()
Takes down whatever right-click box is open, if any.
Definition RightClickWidget.hpp:23
Definition DefaultGraphicsKnobImpl.hpp:27
static void contextMenuEvent(T &self, QPointF pos)
Definition DefaultGraphicsKnobImpl.hpp:252
static void ungrabMouseEvent(T &self, QEvent *event)
Definition DefaultGraphicsKnobImpl.hpp:188
Definition DoubleSpinBox.hpp:17
static void abort(QGraphicsItem &self)
Definition InfiniteScroller.cpp:350
Definition DoubleSpinBox.hpp:9