// This file is part of VSTGUI. It is subject to the license terms // in the LICENSE file found in the top-level directory of this // distribution and at http://github.com/steinbergmedia/vstgui/LICENSE #include "cknob.h" #include "../cbitmap.h" #include "../cdrawcontext.h" #include "../cframe.h" #include "../cgraphicspath.h" #include "../cvstguitimer.h" #include "../events.h" #include namespace VSTGUI { #if TARGET_OS_IPHONE static const float kCKnobRangeDefault = 300.f; #else static const float kCKnobRangeDefault = 200.f; #endif static constexpr CViewAttributeID kCKnobMouseStateAttribute = 'knms'; //------------------------------------------------------------------------ struct CKnobBase::MouseEditingState { CPoint firstPoint; CPoint lastPoint; float startValue; float entryState; float range; float coef; CButtonState oldButton; bool modeLinear; }; //------------------------------------------------------------------------ CKnobBase::CKnobBase (const CRect& size, IControlListener* listener, int32_t tag, CBitmap* background) : CControl (size, listener, tag, background) { rangeAngle = 1.f; setStartAngle ((float)(3.f * Constants::quarter_pi)); setRangeAngle ((float)(3.f * Constants::half_pi)); zoomFactor = 1.5f; knobRange = kCKnobRangeDefault; } //------------------------------------------------------------------------ CKnobBase::CKnobBase (const CKnobBase& k) : CControl (k) , startAngle (k.startAngle) , rangeAngle (k.rangeAngle) , zoomFactor (k.zoomFactor) , inset (k.inset) { } //------------------------------------------------------------------------ void CKnobBase::setViewSize (const CRect &rect, bool invalid) { CControl::setViewSize (rect, invalid); compute (); } //------------------------------------------------------------------------ bool CKnobBase::sizeToFit () { if (getDrawBackground ()) { CRect vs (getViewSize ()); vs.setWidth (getDrawBackground ()->getWidth ()); vs.setHeight (getDrawBackground ()->getHeight ()); setViewSize (vs); setMouseableArea (vs); return true; } return false; } //------------------------------------------------------------------------ auto CKnobBase::getMouseEditingState () -> MouseEditingState& { MouseEditingState* state = nullptr; if (!getAttribute (kCKnobMouseStateAttribute, state)) { state = new MouseEditingState; setAttribute (kCKnobMouseStateAttribute, state); } return *state; } //------------------------------------------------------------------------ void CKnobBase::clearMouseEditingState () { MouseEditingState* state = nullptr; if (!getAttribute (kCKnobMouseStateAttribute, state)) return; delete state; removeAttribute (kCKnobMouseStateAttribute); } //------------------------------------------------------------------------ CMouseEventResult CKnobBase::onMouseDown (CPoint& where, const CButtonState& buttons) { if (!buttons.isLeftButton ()) return kMouseEventNotHandled; invalidMouseWheelEditTimer (this); beginEdit (); auto& mouseState = getMouseEditingState (); mouseState.firstPoint = where; mouseState.lastPoint (-1, -1); mouseState.startValue = getOldValue (); mouseState.modeLinear = false; mouseState.entryState = value; mouseState.range = knobRange; mouseState.coef = (getMax () - getMin ()) / mouseState.range; mouseState.oldButton = buttons; int32_t mode = kCircularMode; int32_t newMode = getFrame ()->getKnobMode (); if (kLinearMode == newMode) { if (!(buttons & kAlt)) mode = newMode; } else if (buttons & kAlt) { mode = kLinearMode; } if (mode == kLinearMode) { if (buttons & kZoomModifier) mouseState.range *= zoomFactor; mouseState.lastPoint = where; mouseState.modeLinear = true; mouseState.coef = (getMax () - getMin ()) / mouseState.range; } else { CPoint where2 (where); where2.offset (-getViewSize ().left, -getViewSize ().top); mouseState.startValue = valueFromPoint (where2); mouseState.lastPoint = where; } return onMouseMoved (where, buttons); } //------------------------------------------------------------------------ CMouseEventResult CKnobBase::onMouseUp (CPoint& where, const CButtonState& buttons) { if (isEditing ()) { endEdit (); clearMouseEditingState (); } return kMouseEventHandled; } //------------------------------------------------------------------------ CMouseEventResult CKnobBase::onMouseCancel () { if (isEditing ()) { auto& mouseState = getMouseEditingState (); value = mouseState.startValue; if (isDirty ()) { valueChanged (); invalid (); } endEdit (); clearMouseEditingState (); } return kMouseEventHandled; } //------------------------------------------------------------------------ CMouseEventResult CKnobBase::onMouseMoved (CPoint& where, const CButtonState& buttons) { if (buttons.isLeftButton () && isEditing ()) { auto& mouseState = getMouseEditingState (); float middle = (getMax () - getMin ()) * 0.5f; if (where != mouseState.lastPoint) { mouseState.lastPoint = where; if (mouseState.modeLinear) { CCoord diff = (mouseState.firstPoint.y - where.y) + (where.x - mouseState.firstPoint.x); if (buttons != mouseState.oldButton) { mouseState.range = knobRange; if (buttons & kZoomModifier) mouseState.range *= zoomFactor; float coef2 = (getMax () - getMin ()) / mouseState.range; mouseState.entryState += (float)(diff * (mouseState.coef - coef2)); mouseState.coef = coef2; mouseState.oldButton = buttons; } value = (float)(mouseState.entryState + diff * mouseState.coef); bounceValue (); } else { where.offset (-getViewSize ().left, -getViewSize ().top); value = valueFromPoint (where); if (mouseState.startValue - value > middle) value = getMax (); else if (value - mouseState.startValue > middle) value = getMin (); else mouseState.startValue = value; } if (value != getOldValue ()) valueChanged (); if (isDirty ()) invalid (); } return kMouseEventHandled; } return kMouseEventNotHandled; } //------------------------------------------------------------------------ void CKnobBase::onMouseWheelEvent (MouseWheelEvent& event) { onMouseWheelEditing (this); float v = getValueNormalized (); if (buttonStateFromEventModifiers (event.modifiers) & kZoomModifier) v += 0.1f * static_cast (event.deltaY) * getWheelInc (); else v += static_cast (event.deltaY) * getWheelInc (); setValueNormalized (v); if (isDirty ()) { invalid (); valueChanged (); } event.consumed = true; } //------------------------------------------------------------------------ void CKnobBase::onKeyboardEvent (KeyboardEvent& event) { if (event.type != EventType::KeyDown) return; switch (event.virt) { case VirtualKey::Up : case VirtualKey::Right : case VirtualKey::Down : case VirtualKey::Left : { float distance = 1.f; if (event.virt == VirtualKey::Down || event.virt == VirtualKey::Left) distance = -distance; float v = getValueNormalized (); if (buttonStateFromEventModifiers (event.modifiers) & kZoomModifier) v += 0.1f * distance * getWheelInc (); else v += distance * getWheelInc (); setValueNormalized (v); if (isDirty ()) { invalid (); beginEdit (); valueChanged (); endEdit (); } event.consumed = true; } case VirtualKey::Escape: { if (isEditing ()) { onMouseCancel (); event.consumed = true; } break; } default: return; } } //------------------------------------------------------------------------ void CKnobBase::setStartAngle (float val) { startAngle = val; compute (); } //------------------------------------------------------------------------ void CKnobBase::setRangeAngle (float val) { rangeAngle = val; compute (); } //------------------------------------------------------------------------ void CKnobBase::compute () { setDirty (); } //------------------------------------------------------------------------ void CKnobBase::valueToPoint (CPoint &point) const { float alpha = (value - getMin()) / (getMax() - getMin()); alpha = startAngle + alpha*rangeAngle; CPoint c (getViewSize ().getWidth () / 2., getViewSize ().getHeight () / 2.); double xradius = c.x - inset; double yradius = c.y - inset; point.x = (CCoord)(c.x + cosf (alpha) * xradius + 0.5f); point.y = (CCoord)(c.y + sinf (alpha) * yradius + 0.5f); } //------------------------------------------------------------------------ float CKnobBase::valueFromPoint (CPoint &point) const { float v; double d = rangeAngle * 0.5; double a = startAngle + d; CPoint c (getViewSize ().getWidth () / 2., getViewSize ().getHeight () / 2.); double xradius = c.x - inset; double yradius = c.y - inset; double dx = (point.x - c.x) / xradius; double dy = (point.y - c.y) / yradius; double alpha = atan2 (dy, dx) - a; while (alpha >= Constants::pi) alpha -= Constants::double_pi; while (alpha < -Constants::pi) alpha += Constants::double_pi; if (d < 0.0) alpha = -alpha; if (alpha > d) v = getMax (); else if (alpha < -d) v = getMin (); else { v = float (0.5 + alpha / rangeAngle); v = getMin () + (v * getRange ()); } return v; } //------------------------------------------------------------------------ void CKnobBase::setMin (float val) { CControl::setMin (val); if (getValue () < val) setValue (val); compute (); } //------------------------------------------------------------------------ void CKnobBase::setMax (float val) { CControl::setMax (val); if (getValue () > val) setValue (val); compute (); } //------------------------------------------------------------------------ // CKnob //------------------------------------------------------------------------ /*! @class CKnob Define a knob with a given background and foreground handle. The handle describes a circle over the background (between -45deg and +225deg). By clicking alt modifier and left mouse button the default value is used. By clicking alt modifier and left mouse button the value changes with a vertical move (version 2.1) */ //------------------------------------------------------------------------ /** * CKnob constructor. * @param size the size of this view * @param listener the listener * @param tag the control tag * @param background background bitmap * @param handle handle bitmap * @param offset offset of background bitmap * @param drawStyle draw style */ //------------------------------------------------------------------------ CKnob::CKnob (const CRect& size, IControlListener* listener, int32_t tag, CBitmap* background, CBitmap* handle, const CPoint& offset, int32_t drawStyle) : CKnobBase (size, listener, tag, background) , offset (offset) , drawStyle (drawStyle) , handleLineWidth (1.) , coronaInset (0) , coronaOutlineWidthAdd (2.) , pHandle (handle) { if (pHandle) { pHandle->remember (); inset = (CCoord)((float)pHandle->getWidth () / 2.f + 2.5f); } else { inset = 3; } colorShadowHandle = kGreyCColor; colorHandle = kWhiteCColor; coronaLineStyle = kLineOnOffDash; coronaLineStyle.getDashLengths ()[1] = 2.; setWantsFocus (true); } //------------------------------------------------------------------------ CKnob::CKnob (const CKnob& v) : CKnobBase (v) , offset (v.offset) , drawStyle (v.drawStyle) , colorHandle (v.colorHandle) , colorShadowHandle (v.colorShadowHandle) , handleLineWidth (v.handleLineWidth) , coronaInset (v.coronaInset) , coronaOutlineWidthAdd (v.coronaInset) , coronaLineStyle (v.coronaLineStyle) , pHandle (v.pHandle) { if (pHandle) pHandle->remember (); } //------------------------------------------------------------------------ CKnob::~CKnob () noexcept { if (pHandle) pHandle->forget (); } //------------------------------------------------------------------------ bool CKnob::drawFocusOnTop () { if (drawStyle & kCoronaDrawing && wantsFocus ()) { return false; } return CKnobBase::drawFocusOnTop (); } //------------------------------------------------------------------------ bool CKnob::getFocusPath (CGraphicsPath &outPath) { if (drawStyle & kCoronaDrawing && wantsFocus ()) { CRect corona (getViewSize ()); corona.inset (coronaInset, coronaInset); corona.inset (handleLineWidth/2., handleLineWidth/2.); outPath.addEllipse (corona); return true; } return CKnobBase::getFocusPath (outPath); } //------------------------------------------------------------------------ void CKnob::draw (CDrawContext *pContext) { if (getDrawBackground ()) { getDrawBackground ()->draw (pContext, getViewSize (), offset); } if (pHandle) drawHandle (pContext); else { if (drawStyle & kCoronaOutline) drawCoronaOutline (pContext); if (drawStyle & kCoronaDrawing) drawCorona (pContext); if (!(drawStyle & kSkipHandleDrawing)) { if (drawStyle & kHandleCircleDrawing) drawHandleAsCircle (pContext); else drawHandleAsLine (pContext); } } setDirty (false); } //------------------------------------------------------------------------ void CKnob::addArc (CGraphicsPath* path, const CRect& r, double startAngle, double sweepAngle) { CCoord w = r.getWidth (); CCoord h = r.getHeight (); double endAngle = startAngle + sweepAngle; if (w != h) { startAngle = atan2 (sin (startAngle) * h, cos (startAngle) * w); endAngle = atan2 (sin (endAngle) * h, cos (endAngle) * w); } path->addArc (r, startAngle / Constants::pi * 180, endAngle / Constants::pi * 180, sweepAngle >= 0); } //------------------------------------------------------------------------ void CKnob::drawCoronaOutline (CDrawContext* pContext) const { auto path = owned (pContext->createGraphicsPath ()); if (path == nullptr) return; CRect corona (getViewSize ()); corona.inset (coronaInset, coronaInset); auto start = startAngle; auto range = rangeAngle; if (coronaOutlineWidthAdd && (drawStyle & kCoronaLineCapButt)) { auto a = static_cast (coronaOutlineWidthAdd / getWidth ()); start -= a; range += a * 2.f; } addArc (path, corona, start, range); pContext->setFrameColor (colorShadowHandle); CLineStyle lineStyle (kLineSolid); if (!(drawStyle & kCoronaLineCapButt)) lineStyle.setLineCap (CLineStyle::kLineCapRound); pContext->setLineStyle (lineStyle); pContext->setLineWidth (handleLineWidth+coronaOutlineWidthAdd); pContext->setDrawMode (kAntiAliasing | kNonIntegralMode); pContext->drawGraphicsPath (path, CDrawContext::kPathStroked); } //------------------------------------------------------------------------ void CKnob::drawCorona (CDrawContext* pContext) const { auto path = owned (pContext->createGraphicsPath ()); if (path == nullptr) return; float coronaValue = getValueNormalized (); if (drawStyle & kCoronaInverted) coronaValue = 1.f - coronaValue; CRect corona (getViewSize ()); corona.inset (coronaInset, coronaInset); if (drawStyle & kCoronaFromCenter) addArc (path, corona, 1.5 * Constants::pi, rangeAngle * (coronaValue - 0.5)); else { if (drawStyle & kCoronaInverted) addArc (path, corona, startAngle + rangeAngle, -rangeAngle * coronaValue); else addArc (path, corona, startAngle, rangeAngle * coronaValue); } pContext->setFrameColor (coronaColor); if (!(drawStyle & kCoronaLineCapButt)) { CLineStyle lineStyle (kLineSolid); lineStyle.setLineCap (CLineStyle::kLineCapRound); pContext->setLineStyle (lineStyle); } else if (drawStyle & kCoronaLineDashDot) pContext->setLineStyle (coronaLineStyle); else pContext->setLineStyle (kLineSolid); pContext->setLineWidth (handleLineWidth); pContext->setDrawMode (kAntiAliasing | kNonIntegralMode); pContext->drawGraphicsPath (path, CDrawContext::kPathStroked); } //------------------------------------------------------------------------ void CKnob::drawHandleAsCircle (CDrawContext* pContext) const { CPoint where; valueToPoint (where); where.offset (getViewSize ().left, getViewSize ().top); CRect r (where.x - 0.5, where.y - 0.5, where.x + 0.5, where.y + 0.5); r.extend (handleLineWidth, handleLineWidth); pContext->setDrawMode (kAntiAliasing); pContext->setFrameColor (colorShadowHandle); pContext->setFillColor (colorHandle); pContext->setLineWidth (0.5); pContext->setLineStyle (kLineSolid); pContext->setDrawMode (kAntiAliasing | kNonIntegralMode); pContext->drawEllipse (r, kDrawFilledAndStroked); } //------------------------------------------------------------------------ void CKnob::drawHandleAsLine (CDrawContext* pContext) const { CPoint where; valueToPoint (where); CPoint origin (getViewSize ().getWidth () / 2, getViewSize ().getHeight () / 2); where.offset (getViewSize ().left - 1, getViewSize ().top); origin.offset (getViewSize ().left - 1, getViewSize ().top); pContext->setFrameColor (colorShadowHandle); pContext->setLineWidth (handleLineWidth); pContext->setLineStyle (CLineStyle (CLineStyle::kLineCapRound)); pContext->setDrawMode (kAntiAliasing | kNonIntegralMode); pContext->drawLine (where, origin); where.offset (1, -1); origin.offset (1, -1); pContext->setFrameColor (colorHandle); pContext->drawLine (where, origin); } //------------------------------------------------------------------------ void CKnob::drawHandle (CDrawContext *pContext) { CPoint where; valueToPoint (where); CCoord width = pHandle->getWidth (); CCoord height = pHandle->getHeight (); where.offset (getViewSize ().left - width / 2, getViewSize ().top - height / 2); where.x = floor (where.x); where.y = floor (where.y); CRect handleSize (0, 0, width, height); handleSize.offset (where.x, where.y); pHandle->draw (pContext, handleSize); } //------------------------------------------------------------------------ void CKnob::setCoronaInset (CCoord inset) { if (inset != coronaInset) { coronaInset = inset; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setCoronaColor (CColor color) { if (color != coronaColor) { coronaColor = color; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setColorShadowHandle (CColor color) { if (color != colorShadowHandle) { colorShadowHandle = color; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setColorHandle (CColor color) { if (color != colorHandle) { colorHandle = color; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setHandleLineWidth (CCoord width) { if (width != handleLineWidth) { handleLineWidth = width; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setCoronaOutlineWidthAdd (CCoord width) { if (width != coronaOutlineWidthAdd) { coronaOutlineWidthAdd = width; setDirty (); } } //------------------------------------------------------------------------ const CLineStyle::CoordVector& CKnob::getCoronaDashDotLengths () const { return coronaLineStyle.getDashLengths (); } //------------------------------------------------------------------------ void CKnob::setCoronaDashDotLengths (const CLineStyle::CoordVector& lengths) { if (coronaLineStyle.getDashLengths () != lengths) { coronaLineStyle.getDashLengths () = lengths; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setDrawStyle (int32_t style) { if (style != drawStyle) { drawStyle = style; setDirty (); } } //------------------------------------------------------------------------ void CKnob::setHandleBitmap (CBitmap* bitmap) { if (pHandle) { pHandle->forget (); pHandle = nullptr; } if (bitmap) { pHandle = bitmap; pHandle->remember (); inset = (CCoord)((float)pHandle->getWidth () / 2.f + 2.5f); } setDirty (); } //------------------------------------------------------------------------ // CAnimKnob //------------------------------------------------------------------------ /*! @class CAnimKnob Such as a CKnob control object, but there is a unique bitmap which contains different views (subbitmaps) of this knob. According to the value, a specific subbitmap is displayed. Use a CMultiFrameBitmap for its background bitmap. */ //------------------------------------------------------------------------ /** * CAnimKnob constructor. * @param size the size of this view * @param listener the listener * @param tag the control tag * @param background the background bitmap */ //------------------------------------------------------------------------ CAnimKnob::CAnimKnob (const CRect& size, IControlListener* listener, int32_t tag, CBitmap* background) : CKnobBase (size, listener, tag, background), bInverseBitmap (false) { #if VSTGUI_ENABLE_DEPRECATED_METHODS heightOfOneImage = size.getHeight (); setNumSubPixmaps (0); if (background) { if (auto frameBitmap = dynamic_cast (background)) { heightOfOneImage = frameBitmap->getFrameSize ().y; setNumSubPixmaps (frameBitmap->getNumFrames ()); } else { setNumSubPixmaps ((int32_t)(background->getHeight () / heightOfOneImage)); } } #endif inset = 0; } //------------------------------------------------------------------------ CAnimKnob::CAnimKnob (const CAnimKnob& v) : CKnobBase (v) , bInverseBitmap (v.bInverseBitmap) { #if VSTGUI_ENABLE_DEPRECATED_METHODS setNumSubPixmaps (v.subPixmaps); setHeightOfOneImage (v.heightOfOneImage); #endif } //----------------------------------------------------------------------------------------------- bool CAnimKnob::sizeToFit () { if (auto bitmap = getDrawBackground ()) { CRect vs (getViewSize ()); if (auto frameBitmap = dynamic_cast (bitmap)) { vs.setSize (frameBitmap->getFrameSize ()); } else { vs.setWidth (bitmap->getWidth ()); #if VSTGUI_ENABLE_DEPRECATED_METHODS vs.setHeight (getHeightOfOneImage ()); #else vs.setHeight (bitmap->getHeight ()); #endif } setViewSize (vs); setMouseableArea (vs); return true; } return false; } #if VSTGUI_ENABLE_DEPRECATED_METHODS //------------------------------------------------------------------------ /** * CAnimKnob constructor. * @param size the size of this view * @param listener the listener * @param tag the control tag * @param subPixmaps number of sub bitmaps in background * @param heightOfOneImage the height of one sub bitmap * @param background the background bitmap * @param offset unused */ //------------------------------------------------------------------------ CAnimKnob::CAnimKnob (const CRect& size, IControlListener* listener, int32_t tag, int32_t subPixmaps, CCoord heightOfOneImage, CBitmap* background, const CPoint& offset) : CKnobBase (size, listener, tag, background), bInverseBitmap (false) { vstgui_assert (background && !dynamic_cast (background), "Use the other constrcutor when using a CMultiFrameBitmap"); setNumSubPixmaps (subPixmaps); setHeightOfOneImage (heightOfOneImage); inset = 0; } //----------------------------------------------------------------------------------------------- void CAnimKnob::setHeightOfOneImage (const CCoord& height) { if (dynamic_cast (getDrawBackground ())) return; IMultiBitmapControl::setHeightOfOneImage (height); if (getDrawBackground () && heightOfOneImage > 0) setNumSubPixmaps ((int32_t)(getDrawBackground ()->getHeight () / heightOfOneImage)); } #endif //----------------------------------------------------------------------------------------------- void CAnimKnob::setBackground (CBitmap *background) { CKnobBase::setBackground (background); #if VSTGUI_ENABLE_DEPRECATED_METHODS if (auto frameBitmap = dynamic_cast (background)) { heightOfOneImage = frameBitmap->getFrameSize ().y; setNumSubPixmaps (frameBitmap->getNumFrames ()); return; } if (heightOfOneImage == 0) heightOfOneImage = getViewSize ().getHeight (); if (background && heightOfOneImage > 0) setNumSubPixmaps ((int32_t)(background->getHeight () / heightOfOneImage)); #endif } //------------------------------------------------------------------------ void CAnimKnob::draw (CDrawContext *pContext) { if (auto bitmap = getDrawBackground ()) { if (auto mfb = dynamic_cast (bitmap)) { auto frameIndex = getMultiFrameBitmapIndex (*mfb, getValueNormalized ()); if (bInverseBitmap) frameIndex = getInverseIndex (*mfb, frameIndex); mfb->drawFrame (pContext, frameIndex, getViewSize ().getTopLeft ()); } else { #if VSTGUI_ENABLE_DEPRECATED_METHODS CPoint where (0, 0); float val = getValueNormalized (); if (val >= 0.f && heightOfOneImage > 0.) { CCoord tmp = heightOfOneImage * (getNumSubPixmaps () - 1); if (bInverseBitmap) where.y = floor ((1. - val) * tmp); else where.y = floor (val * tmp); where.y -= (int32_t)where.y % (int32_t)heightOfOneImage; } bitmap->draw (pContext, getViewSize (), where); #else CView::draw (pContext); #endif } } setDirty (false); } } // VSTGUI