The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

632 lines
21KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 7 technical preview.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For the technical preview this file cannot be licensed commercially.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. static bool viewportWouldScrollOnEvent (const Viewport* vp, const MouseInputSource& src) noexcept
  16. {
  17. if (vp != nullptr)
  18. {
  19. switch (vp->getScrollOnDragMode())
  20. {
  21. case Viewport::ScrollOnDragMode::all: return true;
  22. case Viewport::ScrollOnDragMode::nonHover: return ! src.canHover();
  23. case Viewport::ScrollOnDragMode::never: return false;
  24. }
  25. }
  26. return false;
  27. }
  28. using ViewportDragPosition = AnimatedPosition<AnimatedPositionBehaviours::ContinuousWithMomentum>;
  29. struct Viewport::DragToScrollListener : private MouseListener,
  30. private ViewportDragPosition::Listener
  31. {
  32. DragToScrollListener (Viewport& v) : viewport (v)
  33. {
  34. viewport.contentHolder.addMouseListener (this, true);
  35. offsetX.addListener (this);
  36. offsetY.addListener (this);
  37. offsetX.behaviour.setMinimumVelocity (60);
  38. offsetY.behaviour.setMinimumVelocity (60);
  39. }
  40. ~DragToScrollListener() override
  41. {
  42. viewport.contentHolder.removeMouseListener (this);
  43. Desktop::getInstance().removeGlobalMouseListener (this);
  44. }
  45. void positionChanged (ViewportDragPosition&, double) override
  46. {
  47. viewport.setViewPosition (originalViewPos - Point<int> ((int) offsetX.getPosition(),
  48. (int) offsetY.getPosition()));
  49. }
  50. void mouseDown (const MouseEvent& e) override
  51. {
  52. if (! isGlobalMouseListener && viewportWouldScrollOnEvent (&viewport, e.source))
  53. {
  54. offsetX.setPosition (offsetX.getPosition());
  55. offsetY.setPosition (offsetY.getPosition());
  56. // switch to a global mouse listener so we still receive mouseUp events
  57. // if the original event component is deleted
  58. viewport.contentHolder.removeMouseListener (this);
  59. Desktop::getInstance().addGlobalMouseListener (this);
  60. isGlobalMouseListener = true;
  61. scrollSource = e.source;
  62. }
  63. }
  64. void mouseDrag (const MouseEvent& e) override
  65. {
  66. if (e.source == scrollSource
  67. && ! doesMouseEventComponentBlockViewportDrag (e.eventComponent))
  68. {
  69. auto totalOffset = e.getEventRelativeTo (&viewport).getOffsetFromDragStart().toFloat();
  70. if (! isDragging && totalOffset.getDistanceFromOrigin() > 8.0f && viewportWouldScrollOnEvent (&viewport, e.source))
  71. {
  72. isDragging = true;
  73. originalViewPos = viewport.getViewPosition();
  74. offsetX.setPosition (0.0);
  75. offsetX.beginDrag();
  76. offsetY.setPosition (0.0);
  77. offsetY.beginDrag();
  78. }
  79. if (isDragging)
  80. {
  81. offsetX.drag (totalOffset.x);
  82. offsetY.drag (totalOffset.y);
  83. }
  84. }
  85. }
  86. void mouseUp (const MouseEvent& e) override
  87. {
  88. if (isGlobalMouseListener && e.source == scrollSource)
  89. endDragAndClearGlobalMouseListener();
  90. }
  91. void endDragAndClearGlobalMouseListener()
  92. {
  93. offsetX.endDrag();
  94. offsetY.endDrag();
  95. isDragging = false;
  96. viewport.contentHolder.addMouseListener (this, true);
  97. Desktop::getInstance().removeGlobalMouseListener (this);
  98. isGlobalMouseListener = false;
  99. }
  100. bool doesMouseEventComponentBlockViewportDrag (const Component* eventComp)
  101. {
  102. for (auto c = eventComp; c != nullptr && c != &viewport; c = c->getParentComponent())
  103. if (c->getViewportIgnoreDragFlag())
  104. return true;
  105. return false;
  106. }
  107. Viewport& viewport;
  108. ViewportDragPosition offsetX, offsetY;
  109. Point<int> originalViewPos;
  110. MouseInputSource scrollSource = Desktop::getInstance().getMainMouseSource();
  111. bool isDragging = false;
  112. bool isGlobalMouseListener = false;
  113. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DragToScrollListener)
  114. };
  115. //==============================================================================
  116. Viewport::Viewport (const String& name)
  117. : Component (name),
  118. dragToScrollListener (std::make_unique<DragToScrollListener> (*this))
  119. {
  120. // content holder is used to clip the contents so they don't overlap the scrollbars
  121. addAndMakeVisible (contentHolder);
  122. contentHolder.setInterceptsMouseClicks (false, true);
  123. scrollBarThickness = getLookAndFeel().getDefaultScrollbarWidth();
  124. setInterceptsMouseClicks (false, true);
  125. setWantsKeyboardFocus (true);
  126. recreateScrollbars();
  127. }
  128. Viewport::~Viewport()
  129. {
  130. deleteOrRemoveContentComp();
  131. }
  132. //==============================================================================
  133. void Viewport::visibleAreaChanged (const Rectangle<int>&) {}
  134. void Viewport::viewedComponentChanged (Component*) {}
  135. //==============================================================================
  136. void Viewport::deleteOrRemoveContentComp()
  137. {
  138. if (contentComp != nullptr)
  139. {
  140. contentComp->removeComponentListener (this);
  141. if (deleteContent)
  142. {
  143. // This sets the content comp to a null pointer before deleting the old one, in case
  144. // anything tries to use the old one while it's in mid-deletion..
  145. std::unique_ptr<Component> oldCompDeleter (contentComp.get());
  146. contentComp = nullptr;
  147. }
  148. else
  149. {
  150. contentHolder.removeChildComponent (contentComp);
  151. contentComp = nullptr;
  152. }
  153. }
  154. }
  155. void Viewport::setViewedComponent (Component* const newViewedComponent, const bool deleteComponentWhenNoLongerNeeded)
  156. {
  157. if (contentComp.get() != newViewedComponent)
  158. {
  159. deleteOrRemoveContentComp();
  160. contentComp = newViewedComponent;
  161. deleteContent = deleteComponentWhenNoLongerNeeded;
  162. if (contentComp != nullptr)
  163. {
  164. contentHolder.addAndMakeVisible (contentComp);
  165. setViewPosition (Point<int>());
  166. contentComp->addComponentListener (this);
  167. }
  168. viewedComponentChanged (contentComp);
  169. updateVisibleArea();
  170. }
  171. }
  172. void Viewport::recreateScrollbars()
  173. {
  174. verticalScrollBar.reset();
  175. horizontalScrollBar.reset();
  176. verticalScrollBar .reset (createScrollBarComponent (true));
  177. horizontalScrollBar.reset (createScrollBarComponent (false));
  178. addChildComponent (verticalScrollBar.get());
  179. addChildComponent (horizontalScrollBar.get());
  180. getVerticalScrollBar().addListener (this);
  181. getHorizontalScrollBar().addListener (this);
  182. resized();
  183. }
  184. int Viewport::getMaximumVisibleWidth() const { return contentHolder.getWidth(); }
  185. int Viewport::getMaximumVisibleHeight() const { return contentHolder.getHeight(); }
  186. bool Viewport::canScrollVertically() const noexcept { return contentComp->getY() < 0 || contentComp->getBottom() > getHeight(); }
  187. bool Viewport::canScrollHorizontally() const noexcept { return contentComp->getX() < 0 || contentComp->getRight() > getWidth(); }
  188. Point<int> Viewport::viewportPosToCompPos (Point<int> pos) const
  189. {
  190. jassert (contentComp != nullptr);
  191. auto contentBounds = contentHolder.getLocalArea (contentComp.get(), contentComp->getLocalBounds());
  192. Point<int> p (jmax (jmin (0, contentHolder.getWidth() - contentBounds.getWidth()), jmin (0, -(pos.x))),
  193. jmax (jmin (0, contentHolder.getHeight() - contentBounds.getHeight()), jmin (0, -(pos.y))));
  194. return p.transformedBy (contentComp->getTransform().inverted());
  195. }
  196. void Viewport::setViewPosition (const int xPixelsOffset, const int yPixelsOffset)
  197. {
  198. setViewPosition ({ xPixelsOffset, yPixelsOffset });
  199. }
  200. void Viewport::setViewPosition (Point<int> newPosition)
  201. {
  202. if (contentComp != nullptr)
  203. contentComp->setTopLeftPosition (viewportPosToCompPos (newPosition));
  204. }
  205. void Viewport::setViewPositionProportionately (const double x, const double y)
  206. {
  207. if (contentComp != nullptr)
  208. setViewPosition (jmax (0, roundToInt (x * (contentComp->getWidth() - getWidth()))),
  209. jmax (0, roundToInt (y * (contentComp->getHeight() - getHeight()))));
  210. }
  211. bool Viewport::autoScroll (const int mouseX, const int mouseY, const int activeBorderThickness, const int maximumSpeed)
  212. {
  213. if (contentComp != nullptr)
  214. {
  215. int dx = 0, dy = 0;
  216. if (getHorizontalScrollBar().isVisible() || canScrollHorizontally())
  217. {
  218. if (mouseX < activeBorderThickness)
  219. dx = activeBorderThickness - mouseX;
  220. else if (mouseX >= contentHolder.getWidth() - activeBorderThickness)
  221. dx = (contentHolder.getWidth() - activeBorderThickness) - mouseX;
  222. if (dx < 0)
  223. dx = jmax (dx, -maximumSpeed, contentHolder.getWidth() - contentComp->getRight());
  224. else
  225. dx = jmin (dx, maximumSpeed, -contentComp->getX());
  226. }
  227. if (getVerticalScrollBar().isVisible() || canScrollVertically())
  228. {
  229. if (mouseY < activeBorderThickness)
  230. dy = activeBorderThickness - mouseY;
  231. else if (mouseY >= contentHolder.getHeight() - activeBorderThickness)
  232. dy = (contentHolder.getHeight() - activeBorderThickness) - mouseY;
  233. if (dy < 0)
  234. dy = jmax (dy, -maximumSpeed, contentHolder.getHeight() - contentComp->getBottom());
  235. else
  236. dy = jmin (dy, maximumSpeed, -contentComp->getY());
  237. }
  238. if (dx != 0 || dy != 0)
  239. {
  240. contentComp->setTopLeftPosition (contentComp->getX() + dx,
  241. contentComp->getY() + dy);
  242. return true;
  243. }
  244. }
  245. return false;
  246. }
  247. void Viewport::componentMovedOrResized (Component&, bool, bool)
  248. {
  249. updateVisibleArea();
  250. }
  251. //==============================================================================
  252. void Viewport::setScrollOnDragMode (const ScrollOnDragMode mode)
  253. {
  254. scrollOnDragMode = mode;
  255. }
  256. bool Viewport::isCurrentlyScrollingOnDrag() const noexcept
  257. {
  258. return dragToScrollListener->isDragging;
  259. }
  260. //==============================================================================
  261. void Viewport::lookAndFeelChanged()
  262. {
  263. if (! customScrollBarThickness)
  264. {
  265. scrollBarThickness = getLookAndFeel().getDefaultScrollbarWidth();
  266. resized();
  267. }
  268. }
  269. void Viewport::resized()
  270. {
  271. updateVisibleArea();
  272. }
  273. //==============================================================================
  274. void Viewport::updateVisibleArea()
  275. {
  276. auto scrollbarWidth = getScrollBarThickness();
  277. const bool canShowAnyBars = getWidth() > scrollbarWidth && getHeight() > scrollbarWidth;
  278. const bool canShowHBar = showHScrollbar && canShowAnyBars;
  279. const bool canShowVBar = showVScrollbar && canShowAnyBars;
  280. bool hBarVisible = false, vBarVisible = false;
  281. Rectangle<int> contentArea;
  282. for (int i = 3; --i >= 0;)
  283. {
  284. hBarVisible = canShowHBar && ! getHorizontalScrollBar().autoHides();
  285. vBarVisible = canShowVBar && ! getVerticalScrollBar().autoHides();
  286. contentArea = getLocalBounds();
  287. if (contentComp != nullptr && ! contentArea.contains (contentComp->getBounds()))
  288. {
  289. hBarVisible = canShowHBar && (hBarVisible || contentComp->getX() < 0 || contentComp->getRight() > contentArea.getWidth());
  290. vBarVisible = canShowVBar && (vBarVisible || contentComp->getY() < 0 || contentComp->getBottom() > contentArea.getHeight());
  291. if (vBarVisible)
  292. contentArea.setWidth (getWidth() - scrollbarWidth);
  293. if (hBarVisible)
  294. contentArea.setHeight (getHeight() - scrollbarWidth);
  295. if (! contentArea.contains (contentComp->getBounds()))
  296. {
  297. hBarVisible = canShowHBar && (hBarVisible || contentComp->getRight() > contentArea.getWidth());
  298. vBarVisible = canShowVBar && (vBarVisible || contentComp->getBottom() > contentArea.getHeight());
  299. }
  300. }
  301. if (vBarVisible) contentArea.setWidth (getWidth() - scrollbarWidth);
  302. if (hBarVisible) contentArea.setHeight (getHeight() - scrollbarWidth);
  303. if (! vScrollbarRight && vBarVisible)
  304. contentArea.setX (scrollbarWidth);
  305. if (! hScrollbarBottom && hBarVisible)
  306. contentArea.setY (scrollbarWidth);
  307. if (contentComp == nullptr)
  308. {
  309. contentHolder.setBounds (contentArea);
  310. break;
  311. }
  312. auto oldContentBounds = contentComp->getBounds();
  313. contentHolder.setBounds (contentArea);
  314. // If the content has changed its size, that might affect our scrollbars, so go round again and re-calculate..
  315. if (oldContentBounds == contentComp->getBounds())
  316. break;
  317. }
  318. Rectangle<int> contentBounds;
  319. if (auto cc = contentComp.get())
  320. contentBounds = contentHolder.getLocalArea (cc, cc->getLocalBounds());
  321. auto visibleOrigin = -contentBounds.getPosition();
  322. auto& hbar = getHorizontalScrollBar();
  323. auto& vbar = getVerticalScrollBar();
  324. hbar.setBounds (contentArea.getX(), hScrollbarBottom ? contentArea.getHeight() : 0, contentArea.getWidth(), scrollbarWidth);
  325. hbar.setRangeLimits (0.0, contentBounds.getWidth());
  326. hbar.setCurrentRange (visibleOrigin.x, contentArea.getWidth());
  327. hbar.setSingleStepSize (singleStepX);
  328. if (canShowHBar && ! hBarVisible)
  329. visibleOrigin.setX (0);
  330. vbar.setBounds (vScrollbarRight ? contentArea.getWidth() : 0, contentArea.getY(), scrollbarWidth, contentArea.getHeight());
  331. vbar.setRangeLimits (0.0, contentBounds.getHeight());
  332. vbar.setCurrentRange (visibleOrigin.y, contentArea.getHeight());
  333. vbar.setSingleStepSize (singleStepY);
  334. if (canShowVBar && ! vBarVisible)
  335. visibleOrigin.setY (0);
  336. // Force the visibility *after* setting the ranges to avoid flicker caused by edge conditions in the numbers.
  337. hbar.setVisible (hBarVisible);
  338. vbar.setVisible (vBarVisible);
  339. if (contentComp != nullptr)
  340. {
  341. auto newContentCompPos = viewportPosToCompPos (visibleOrigin);
  342. if (contentComp->getBounds().getPosition() != newContentCompPos)
  343. {
  344. contentComp->setTopLeftPosition (newContentCompPos); // (this will re-entrantly call updateVisibleArea again)
  345. return;
  346. }
  347. }
  348. const Rectangle<int> visibleArea (visibleOrigin.x, visibleOrigin.y,
  349. jmin (contentBounds.getWidth() - visibleOrigin.x, contentArea.getWidth()),
  350. jmin (contentBounds.getHeight() - visibleOrigin.y, contentArea.getHeight()));
  351. if (lastVisibleArea != visibleArea)
  352. {
  353. lastVisibleArea = visibleArea;
  354. visibleAreaChanged (visibleArea);
  355. }
  356. hbar.handleUpdateNowIfNeeded();
  357. vbar.handleUpdateNowIfNeeded();
  358. }
  359. //==============================================================================
  360. void Viewport::setSingleStepSizes (const int stepX, const int stepY)
  361. {
  362. if (singleStepX != stepX || singleStepY != stepY)
  363. {
  364. singleStepX = stepX;
  365. singleStepY = stepY;
  366. updateVisibleArea();
  367. }
  368. }
  369. void Viewport::setScrollBarsShown (const bool showVerticalScrollbarIfNeeded,
  370. const bool showHorizontalScrollbarIfNeeded,
  371. const bool allowVerticalScrollingWithoutScrollbar,
  372. const bool allowHorizontalScrollingWithoutScrollbar)
  373. {
  374. allowScrollingWithoutScrollbarV = allowVerticalScrollingWithoutScrollbar;
  375. allowScrollingWithoutScrollbarH = allowHorizontalScrollingWithoutScrollbar;
  376. if (showVScrollbar != showVerticalScrollbarIfNeeded
  377. || showHScrollbar != showHorizontalScrollbarIfNeeded)
  378. {
  379. showVScrollbar = showVerticalScrollbarIfNeeded;
  380. showHScrollbar = showHorizontalScrollbarIfNeeded;
  381. updateVisibleArea();
  382. }
  383. }
  384. void Viewport::setScrollBarThickness (const int thickness)
  385. {
  386. int newThickness;
  387. // To stay compatible with the previous code: use the
  388. // default thickness if thickness parameter is zero
  389. // or negative
  390. if (thickness <= 0)
  391. {
  392. customScrollBarThickness = false;
  393. newThickness = getLookAndFeel().getDefaultScrollbarWidth();
  394. }
  395. else
  396. {
  397. customScrollBarThickness = true;
  398. newThickness = thickness;
  399. }
  400. if (scrollBarThickness != newThickness)
  401. {
  402. scrollBarThickness = newThickness;
  403. updateVisibleArea();
  404. }
  405. }
  406. int Viewport::getScrollBarThickness() const
  407. {
  408. return scrollBarThickness;
  409. }
  410. void Viewport::scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart)
  411. {
  412. auto newRangeStartInt = roundToInt (newRangeStart);
  413. if (scrollBarThatHasMoved == horizontalScrollBar.get())
  414. {
  415. setViewPosition (newRangeStartInt, getViewPositionY());
  416. }
  417. else if (scrollBarThatHasMoved == verticalScrollBar.get())
  418. {
  419. setViewPosition (getViewPositionX(), newRangeStartInt);
  420. }
  421. }
  422. void Viewport::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
  423. {
  424. if (! useMouseWheelMoveIfNeeded (e, wheel))
  425. Component::mouseWheelMove (e, wheel);
  426. }
  427. static int rescaleMouseWheelDistance (float distance, int singleStepSize) noexcept
  428. {
  429. if (distance == 0.0f)
  430. return 0;
  431. distance *= 14.0f * (float) singleStepSize;
  432. return roundToInt (distance < 0 ? jmin (distance, -1.0f)
  433. : jmax (distance, 1.0f));
  434. }
  435. bool Viewport::useMouseWheelMoveIfNeeded (const MouseEvent& e, const MouseWheelDetails& wheel)
  436. {
  437. if (! (e.mods.isAltDown() || e.mods.isCtrlDown() || e.mods.isCommandDown()))
  438. {
  439. const bool canScrollVert = (allowScrollingWithoutScrollbarV || getVerticalScrollBar().isVisible());
  440. const bool canScrollHorz = (allowScrollingWithoutScrollbarH || getHorizontalScrollBar().isVisible());
  441. if (canScrollHorz || canScrollVert)
  442. {
  443. auto deltaX = rescaleMouseWheelDistance (wheel.deltaX, singleStepX);
  444. auto deltaY = rescaleMouseWheelDistance (wheel.deltaY, singleStepY);
  445. auto pos = getViewPosition();
  446. if (deltaX != 0 && deltaY != 0 && canScrollHorz && canScrollVert)
  447. {
  448. pos.x -= deltaX;
  449. pos.y -= deltaY;
  450. }
  451. else if (canScrollHorz && (deltaX != 0 || e.mods.isShiftDown() || ! canScrollVert))
  452. {
  453. pos.x -= deltaX != 0 ? deltaX : deltaY;
  454. }
  455. else if (canScrollVert && deltaY != 0)
  456. {
  457. pos.y -= deltaY;
  458. }
  459. if (pos != getViewPosition())
  460. {
  461. setViewPosition (pos);
  462. return true;
  463. }
  464. }
  465. }
  466. return false;
  467. }
  468. static bool isUpDownKeyPress (const KeyPress& key)
  469. {
  470. return key == KeyPress::upKey
  471. || key == KeyPress::downKey
  472. || key == KeyPress::pageUpKey
  473. || key == KeyPress::pageDownKey
  474. || key == KeyPress::homeKey
  475. || key == KeyPress::endKey;
  476. }
  477. static bool isLeftRightKeyPress (const KeyPress& key)
  478. {
  479. return key == KeyPress::leftKey
  480. || key == KeyPress::rightKey;
  481. }
  482. bool Viewport::keyPressed (const KeyPress& key)
  483. {
  484. const bool isUpDownKey = isUpDownKeyPress (key);
  485. if (getVerticalScrollBar().isVisible() && isUpDownKey)
  486. return getVerticalScrollBar().keyPressed (key);
  487. const bool isLeftRightKey = isLeftRightKeyPress (key);
  488. if (getHorizontalScrollBar().isVisible() && (isUpDownKey || isLeftRightKey))
  489. return getHorizontalScrollBar().keyPressed (key);
  490. return false;
  491. }
  492. bool Viewport::respondsToKey (const KeyPress& key)
  493. {
  494. return isUpDownKeyPress (key) || isLeftRightKeyPress (key);
  495. }
  496. ScrollBar* Viewport::createScrollBarComponent (bool isVertical)
  497. {
  498. return new ScrollBar (isVertical);
  499. }
  500. void Viewport::setScrollBarPosition (bool verticalScrollbarOnRight,
  501. bool horizontalScrollbarAtBottom)
  502. {
  503. vScrollbarRight = verticalScrollbarOnRight;
  504. hScrollbarBottom = horizontalScrollbarAtBottom;
  505. resized();
  506. }
  507. } // namespace juce