1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 class ListBox::RowComponent  : public Component,
30                                public TooltipClient
31 {
32 public:
RowComponent(ListBox & lb)33     RowComponent (ListBox& lb) : owner (lb) {}
34 
paint(Graphics & g)35     void paint (Graphics& g) override
36     {
37         if (auto* m = owner.getModel())
38             m->paintListBoxItem (row, g, getWidth(), getHeight(), selected);
39     }
40 
update(const int newRow,const bool nowSelected)41     void update (const int newRow, const bool nowSelected)
42     {
43         if (row != newRow || selected != nowSelected)
44         {
45             repaint();
46             row = newRow;
47             selected = nowSelected;
48         }
49 
50         if (auto* m = owner.getModel())
51         {
52             setMouseCursor (m->getMouseCursorForRow (row));
53 
54             customComponent.reset (m->refreshComponentForRow (newRow, nowSelected, customComponent.release()));
55 
56             if (customComponent != nullptr)
57             {
58                 addAndMakeVisible (customComponent.get());
59                 customComponent->setBounds (getLocalBounds());
60             }
61         }
62     }
63 
performSelection(const MouseEvent & e,bool isMouseUp)64     void performSelection (const MouseEvent& e, bool isMouseUp)
65     {
66         owner.selectRowsBasedOnModifierKeys (row, e.mods, isMouseUp);
67 
68         if (auto* m = owner.getModel())
69             m->listBoxItemClicked (row, e);
70     }
71 
isInDragToScrollViewport() const72     bool isInDragToScrollViewport() const noexcept
73     {
74         if (auto* vp = owner.getViewport())
75             return vp->isScrollOnDragEnabled() && (vp->canScrollVertically() || vp->canScrollHorizontally());
76 
77         return false;
78     }
79 
mouseDown(const MouseEvent & e)80     void mouseDown (const MouseEvent& e) override
81     {
82         isDragging = false;
83         isDraggingToScroll = false;
84         selectRowOnMouseUp = false;
85 
86         if (isEnabled())
87         {
88             if (owner.selectOnMouseDown && ! (selected || isInDragToScrollViewport()))
89                 performSelection (e, false);
90             else
91                 selectRowOnMouseUp = true;
92         }
93     }
94 
mouseUp(const MouseEvent & e)95     void mouseUp (const MouseEvent& e) override
96     {
97         if (isEnabled() && selectRowOnMouseUp && ! (isDragging || isDraggingToScroll))
98             performSelection (e, true);
99     }
100 
mouseDoubleClick(const MouseEvent & e)101     void mouseDoubleClick (const MouseEvent& e) override
102     {
103         if (isEnabled())
104             if (auto* m = owner.getModel())
105                 m->listBoxItemDoubleClicked (row, e);
106     }
107 
mouseDrag(const MouseEvent & e)108     void mouseDrag (const MouseEvent& e) override
109     {
110         if (auto* m = owner.getModel())
111         {
112             if (isEnabled() && e.mouseWasDraggedSinceMouseDown() && ! isDragging)
113             {
114                 SparseSet<int> rowsToDrag;
115 
116                 if (owner.selectOnMouseDown || owner.isRowSelected (row))
117                     rowsToDrag = owner.getSelectedRows();
118                 else
119                     rowsToDrag.addRange (Range<int>::withStartAndLength (row, 1));
120 
121                 if (rowsToDrag.size() > 0)
122                 {
123                     auto dragDescription = m->getDragSourceDescription (rowsToDrag);
124 
125                     if (! (dragDescription.isVoid() || (dragDescription.isString() && dragDescription.toString().isEmpty())))
126                     {
127                         isDragging = true;
128                         owner.startDragAndDrop (e, rowsToDrag, dragDescription, true);
129                     }
130                 }
131             }
132         }
133 
134         if (! isDraggingToScroll)
135             if (auto* vp = owner.getViewport())
136                 isDraggingToScroll = vp->isCurrentlyScrollingOnDrag();
137     }
138 
resized()139     void resized() override
140     {
141         if (customComponent != nullptr)
142             customComponent->setBounds (getLocalBounds());
143     }
144 
getTooltip()145     String getTooltip() override
146     {
147         if (auto* m = owner.getModel())
148             return m->getTooltipForRow (row);
149 
150         return {};
151     }
152 
153     ListBox& owner;
154     std::unique_ptr<Component> customComponent;
155     int row = -1;
156     bool selected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false;
157 
158     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent)
159 };
160 
161 
162 //==============================================================================
163 class ListBox::ListViewport  : public Viewport
164 {
165 public:
ListViewport(ListBox & lb)166     ListViewport (ListBox& lb)  : owner (lb)
167     {
168         setWantsKeyboardFocus (false);
169 
170         auto content = new Component();
171         setViewedComponent (content);
172         content->setWantsKeyboardFocus (false);
173     }
174 
getComponentForRow(const int row) const175     RowComponent* getComponentForRow (const int row) const noexcept
176     {
177         return rows [row % jmax (1, rows.size())];
178     }
179 
getComponentForRowIfOnscreen(const int row) const180     RowComponent* getComponentForRowIfOnscreen (const int row) const noexcept
181     {
182         return (row >= firstIndex && row < firstIndex + rows.size())
183                  ? getComponentForRow (row) : nullptr;
184     }
185 
getRowNumberOfComponent(Component * const rowComponent) const186     int getRowNumberOfComponent (Component* const rowComponent) const noexcept
187     {
188         const int index = getViewedComponent()->getIndexOfChildComponent (rowComponent);
189         const int num = rows.size();
190 
191         for (int i = num; --i >= 0;)
192             if (((firstIndex + i) % jmax (1, num)) == index)
193                 return firstIndex + i;
194 
195         return -1;
196     }
197 
visibleAreaChanged(const Rectangle<int> &)198     void visibleAreaChanged (const Rectangle<int>&) override
199     {
200         updateVisibleArea (true);
201 
202         if (auto* m = owner.getModel())
203             m->listWasScrolled();
204     }
205 
updateVisibleArea(const bool makeSureItUpdatesContent)206     void updateVisibleArea (const bool makeSureItUpdatesContent)
207     {
208         hasUpdated = false;
209 
210         auto& content = *getViewedComponent();
211         auto newX = content.getX();
212         auto newY = content.getY();
213         auto newW = jmax (owner.minimumRowWidth, getMaximumVisibleWidth());
214         auto newH = owner.totalItems * owner.getRowHeight();
215 
216         if (newY + newH < getMaximumVisibleHeight() && newH > getMaximumVisibleHeight())
217             newY = getMaximumVisibleHeight() - newH;
218 
219         content.setBounds (newX, newY, newW, newH);
220 
221         if (makeSureItUpdatesContent && ! hasUpdated)
222             updateContents();
223     }
224 
updateContents()225     void updateContents()
226     {
227         hasUpdated = true;
228         auto rowH = owner.getRowHeight();
229         auto& content = *getViewedComponent();
230 
231         if (rowH > 0)
232         {
233             auto y = getViewPositionY();
234             auto w = content.getWidth();
235 
236             const int numNeeded = 2 + getMaximumVisibleHeight() / rowH;
237             rows.removeRange (numNeeded, rows.size());
238 
239             while (numNeeded > rows.size())
240             {
241                 auto newRow = new RowComponent (owner);
242                 rows.add (newRow);
243                 content.addAndMakeVisible (newRow);
244             }
245 
246             firstIndex = y / rowH;
247             firstWholeIndex = (y + rowH - 1) / rowH;
248             lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH;
249 
250             for (int i = 0; i < numNeeded; ++i)
251             {
252                 const int row = i + firstIndex;
253 
254                 if (auto* rowComp = getComponentForRow (row))
255                 {
256                     rowComp->setBounds (0, row * rowH, w, rowH);
257                     rowComp->update (row, owner.isRowSelected (row));
258                 }
259             }
260         }
261 
262         if (owner.headerComponent != nullptr)
263             owner.headerComponent->setBounds (owner.outlineThickness + content.getX(),
264                                               owner.outlineThickness,
265                                               jmax (owner.getWidth() - owner.outlineThickness * 2,
266                                                     content.getWidth()),
267                                               owner.headerComponent->getHeight());
268     }
269 
selectRow(const int row,const int rowH,const bool dontScroll,const int lastSelectedRow,const int totalRows,const bool isMouseClick)270     void selectRow (const int row, const int rowH, const bool dontScroll,
271                     const int lastSelectedRow, const int totalRows, const bool isMouseClick)
272     {
273         hasUpdated = false;
274 
275         if (row < firstWholeIndex && ! dontScroll)
276         {
277             setViewPosition (getViewPositionX(), row * rowH);
278         }
279         else if (row >= lastWholeIndex && ! dontScroll)
280         {
281             const int rowsOnScreen = lastWholeIndex - firstWholeIndex;
282 
283             if (row >= lastSelectedRow + rowsOnScreen
284                  && rowsOnScreen < totalRows - 1
285                  && ! isMouseClick)
286             {
287                 setViewPosition (getViewPositionX(),
288                                  jlimit (0, jmax (0, totalRows - rowsOnScreen), row) * rowH);
289             }
290             else
291             {
292                 setViewPosition (getViewPositionX(),
293                                  jmax (0, (row  + 1) * rowH - getMaximumVisibleHeight()));
294             }
295         }
296 
297         if (! hasUpdated)
298             updateContents();
299     }
300 
scrollToEnsureRowIsOnscreen(const int row,const int rowH)301     void scrollToEnsureRowIsOnscreen (const int row, const int rowH)
302     {
303         if (row < firstWholeIndex)
304         {
305             setViewPosition (getViewPositionX(), row * rowH);
306         }
307         else if (row >= lastWholeIndex)
308         {
309             setViewPosition (getViewPositionX(),
310                              jmax (0, (row  + 1) * rowH - getMaximumVisibleHeight()));
311         }
312     }
313 
paint(Graphics & g)314     void paint (Graphics& g) override
315     {
316         if (isOpaque())
317             g.fillAll (owner.findColour (ListBox::backgroundColourId));
318     }
319 
keyPressed(const KeyPress & key)320     bool keyPressed (const KeyPress& key) override
321     {
322         if (Viewport::respondsToKey (key))
323         {
324             const int allowableMods = owner.multipleSelection ? ModifierKeys::shiftModifier : 0;
325 
326             if ((key.getModifiers().getRawFlags() & ~allowableMods) == 0)
327             {
328                 // we want to avoid these keypresses going to the viewport, and instead allow
329                 // them to pass up to our listbox..
330                 return false;
331             }
332         }
333 
334         return Viewport::keyPressed (key);
335     }
336 
337 private:
338     ListBox& owner;
339     OwnedArray<RowComponent> rows;
340     int firstIndex = 0, firstWholeIndex = 0, lastWholeIndex = 0;
341     bool hasUpdated = false;
342 
343     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListViewport)
344 };
345 
346 //==============================================================================
347 struct ListBoxMouseMoveSelector  : public MouseListener
348 {
ListBoxMouseMoveSelectorjuce::ListBoxMouseMoveSelector349     ListBoxMouseMoveSelector (ListBox& lb) : owner (lb)
350     {
351         owner.addMouseListener (this, true);
352     }
353 
~ListBoxMouseMoveSelectorjuce::ListBoxMouseMoveSelector354     ~ListBoxMouseMoveSelector() override
355     {
356         owner.removeMouseListener (this);
357     }
358 
mouseMovejuce::ListBoxMouseMoveSelector359     void mouseMove (const MouseEvent& e) override
360     {
361         auto pos = e.getEventRelativeTo (&owner).position.toInt();
362         owner.selectRow (owner.getRowContainingPosition (pos.x, pos.y), true);
363     }
364 
mouseExitjuce::ListBoxMouseMoveSelector365     void mouseExit (const MouseEvent& e) override
366     {
367         mouseMove (e);
368     }
369 
370     ListBox& owner;
371     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListBoxMouseMoveSelector)
372 };
373 
374 
375 //==============================================================================
ListBox(const String & name,ListBoxModel * const m)376 ListBox::ListBox (const String& name, ListBoxModel* const m)
377     : Component (name), model (m)
378 {
379     viewport.reset (new ListViewport (*this));
380     addAndMakeVisible (viewport.get());
381 
382     ListBox::setWantsKeyboardFocus (true);
383     ListBox::colourChanged();
384 }
385 
~ListBox()386 ListBox::~ListBox()
387 {
388     headerComponent.reset();
389     viewport.reset();
390 }
391 
setModel(ListBoxModel * const newModel)392 void ListBox::setModel (ListBoxModel* const newModel)
393 {
394     if (model != newModel)
395     {
396         model = newModel;
397         repaint();
398         updateContent();
399     }
400 }
401 
setMultipleSelectionEnabled(bool b)402 void ListBox::setMultipleSelectionEnabled (bool b) noexcept         { multipleSelection = b; }
setClickingTogglesRowSelection(bool b)403 void ListBox::setClickingTogglesRowSelection (bool b) noexcept      { alwaysFlipSelection = b; }
setRowSelectedOnMouseDown(bool b)404 void ListBox::setRowSelectedOnMouseDown (bool b) noexcept           { selectOnMouseDown = b; }
405 
setMouseMoveSelectsRows(bool b)406 void ListBox::setMouseMoveSelectsRows (bool b)
407 {
408     if (b)
409     {
410         if (mouseMoveSelector == nullptr)
411             mouseMoveSelector.reset (new ListBoxMouseMoveSelector (*this));
412     }
413     else
414     {
415         mouseMoveSelector.reset();
416     }
417 }
418 
419 //==============================================================================
paint(Graphics & g)420 void ListBox::paint (Graphics& g)
421 {
422     if (! hasDoneInitialUpdate)
423         updateContent();
424 
425     g.fillAll (findColour (backgroundColourId));
426 }
427 
paintOverChildren(Graphics & g)428 void ListBox::paintOverChildren (Graphics& g)
429 {
430     if (outlineThickness > 0)
431     {
432         g.setColour (findColour (outlineColourId));
433         g.drawRect (getLocalBounds(), outlineThickness);
434     }
435 }
436 
resized()437 void ListBox::resized()
438 {
439     viewport->setBoundsInset (BorderSize<int> (outlineThickness + (headerComponent != nullptr ? headerComponent->getHeight() : 0),
440                                                outlineThickness, outlineThickness, outlineThickness));
441 
442     viewport->setSingleStepSizes (20, getRowHeight());
443 
444     viewport->updateVisibleArea (false);
445 }
446 
visibilityChanged()447 void ListBox::visibilityChanged()
448 {
449     viewport->updateVisibleArea (true);
450 }
451 
getViewport() const452 Viewport* ListBox::getViewport() const noexcept
453 {
454     return viewport.get();
455 }
456 
457 //==============================================================================
updateContent()458 void ListBox::updateContent()
459 {
460     hasDoneInitialUpdate = true;
461     totalItems = (model != nullptr) ? model->getNumRows() : 0;
462 
463     bool selectionChanged = false;
464 
465     if (selected.size() > 0 && selected [selected.size() - 1] >= totalItems)
466     {
467         selected.removeRange ({ totalItems, std::numeric_limits<int>::max() });
468         lastRowSelected = getSelectedRow (0);
469         selectionChanged = true;
470     }
471 
472     viewport->updateVisibleArea (isVisible());
473     viewport->resized();
474 
475     if (selectionChanged && model != nullptr)
476         model->selectedRowsChanged (lastRowSelected);
477 }
478 
479 //==============================================================================
selectRow(int row,bool dontScroll,bool deselectOthersFirst)480 void ListBox::selectRow (int row, bool dontScroll, bool deselectOthersFirst)
481 {
482     selectRowInternal (row, dontScroll, deselectOthersFirst, false);
483 }
484 
selectRowInternal(const int row,bool dontScroll,bool deselectOthersFirst,bool isMouseClick)485 void ListBox::selectRowInternal (const int row,
486                                  bool dontScroll,
487                                  bool deselectOthersFirst,
488                                  bool isMouseClick)
489 {
490     if (! multipleSelection)
491         deselectOthersFirst = true;
492 
493     if ((! isRowSelected (row))
494          || (deselectOthersFirst && getNumSelectedRows() > 1))
495     {
496         if (isPositiveAndBelow (row, totalItems))
497         {
498             if (deselectOthersFirst)
499                 selected.clear();
500 
501             selected.addRange ({ row, row + 1 });
502 
503             if (getHeight() == 0 || getWidth() == 0)
504                 dontScroll = true;
505 
506             viewport->selectRow (row, getRowHeight(), dontScroll,
507                                  lastRowSelected, totalItems, isMouseClick);
508 
509             lastRowSelected = row;
510             model->selectedRowsChanged (row);
511         }
512         else
513         {
514             if (deselectOthersFirst)
515                 deselectAllRows();
516         }
517     }
518 }
519 
deselectRow(const int row)520 void ListBox::deselectRow (const int row)
521 {
522     if (selected.contains (row))
523     {
524         selected.removeRange ({ row, row + 1 });
525 
526         if (row == lastRowSelected)
527             lastRowSelected = getSelectedRow (0);
528 
529         viewport->updateContents();
530         model->selectedRowsChanged (lastRowSelected);
531     }
532 }
533 
setSelectedRows(const SparseSet<int> & setOfRowsToBeSelected,const NotificationType sendNotificationEventToModel)534 void ListBox::setSelectedRows (const SparseSet<int>& setOfRowsToBeSelected,
535                                const NotificationType sendNotificationEventToModel)
536 {
537     selected = setOfRowsToBeSelected;
538     selected.removeRange ({ totalItems, std::numeric_limits<int>::max() });
539 
540     if (! isRowSelected (lastRowSelected))
541         lastRowSelected = getSelectedRow (0);
542 
543     viewport->updateContents();
544 
545     if (model != nullptr && sendNotificationEventToModel == sendNotification)
546         model->selectedRowsChanged (lastRowSelected);
547 }
548 
getSelectedRows() const549 SparseSet<int> ListBox::getSelectedRows() const
550 {
551     return selected;
552 }
553 
selectRangeOfRows(int firstRow,int lastRow,bool dontScrollToShowThisRange)554 void ListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToShowThisRange)
555 {
556     if (multipleSelection && (firstRow != lastRow))
557     {
558         const int numRows = totalItems - 1;
559         firstRow = jlimit (0, jmax (0, numRows), firstRow);
560         lastRow  = jlimit (0, jmax (0, numRows), lastRow);
561 
562         selected.addRange ({ jmin (firstRow, lastRow),
563                              jmax (firstRow, lastRow) + 1 });
564 
565         selected.removeRange ({ lastRow, lastRow + 1 });
566     }
567 
568     selectRowInternal (lastRow, dontScrollToShowThisRange, false, true);
569 }
570 
flipRowSelection(const int row)571 void ListBox::flipRowSelection (const int row)
572 {
573     if (isRowSelected (row))
574         deselectRow (row);
575     else
576         selectRowInternal (row, false, false, true);
577 }
578 
deselectAllRows()579 void ListBox::deselectAllRows()
580 {
581     if (! selected.isEmpty())
582     {
583         selected.clear();
584         lastRowSelected = -1;
585 
586         viewport->updateContents();
587 
588         if (model != nullptr)
589             model->selectedRowsChanged (lastRowSelected);
590     }
591 }
592 
selectRowsBasedOnModifierKeys(const int row,ModifierKeys mods,const bool isMouseUpEvent)593 void ListBox::selectRowsBasedOnModifierKeys (const int row,
594                                              ModifierKeys mods,
595                                              const bool isMouseUpEvent)
596 {
597     if (multipleSelection && (mods.isCommandDown() || alwaysFlipSelection))
598     {
599         flipRowSelection (row);
600     }
601     else if (multipleSelection && mods.isShiftDown() && lastRowSelected >= 0)
602     {
603         selectRangeOfRows (lastRowSelected, row);
604     }
605     else if ((! mods.isPopupMenu()) || ! isRowSelected (row))
606     {
607         selectRowInternal (row, false, ! (multipleSelection && (! isMouseUpEvent) && isRowSelected (row)), true);
608     }
609 }
610 
getNumSelectedRows() const611 int ListBox::getNumSelectedRows() const
612 {
613     return selected.size();
614 }
615 
getSelectedRow(const int index) const616 int ListBox::getSelectedRow (const int index) const
617 {
618     return (isPositiveAndBelow (index, selected.size()))
619                 ? selected [index] : -1;
620 }
621 
isRowSelected(const int row) const622 bool ListBox::isRowSelected (const int row) const
623 {
624     return selected.contains (row);
625 }
626 
getLastRowSelected() const627 int ListBox::getLastRowSelected() const
628 {
629     return isRowSelected (lastRowSelected) ? lastRowSelected : -1;
630 }
631 
632 //==============================================================================
getRowContainingPosition(const int x,const int y) const633 int ListBox::getRowContainingPosition (const int x, const int y) const noexcept
634 {
635     if (isPositiveAndBelow (x, getWidth()))
636     {
637         const int row = (viewport->getViewPositionY() + y - viewport->getY()) / rowHeight;
638 
639         if (isPositiveAndBelow (row, totalItems))
640             return row;
641     }
642 
643     return -1;
644 }
645 
getInsertionIndexForPosition(const int x,const int y) const646 int ListBox::getInsertionIndexForPosition (const int x, const int y) const noexcept
647 {
648     if (isPositiveAndBelow (x, getWidth()))
649         return jlimit (0, totalItems, (viewport->getViewPositionY() + y + rowHeight / 2 - viewport->getY()) / rowHeight);
650 
651     return -1;
652 }
653 
getComponentForRowNumber(const int row) const654 Component* ListBox::getComponentForRowNumber (const int row) const noexcept
655 {
656     if (auto* listRowComp = viewport->getComponentForRowIfOnscreen (row))
657         return listRowComp->customComponent.get();
658 
659     return nullptr;
660 }
661 
getRowNumberOfComponent(Component * const rowComponent) const662 int ListBox::getRowNumberOfComponent (Component* const rowComponent) const noexcept
663 {
664     return viewport->getRowNumberOfComponent (rowComponent);
665 }
666 
getRowPosition(int rowNumber,bool relativeToComponentTopLeft) const667 Rectangle<int> ListBox::getRowPosition (int rowNumber, bool relativeToComponentTopLeft) const noexcept
668 {
669     auto y = viewport->getY() + rowHeight * rowNumber;
670 
671     if (relativeToComponentTopLeft)
672         y -= viewport->getViewPositionY();
673 
674     return { viewport->getX(), y,
675              viewport->getViewedComponent()->getWidth(), rowHeight };
676 }
677 
setVerticalPosition(const double proportion)678 void ListBox::setVerticalPosition (const double proportion)
679 {
680     auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight();
681 
682     viewport->setViewPosition (viewport->getViewPositionX(),
683                                jmax (0, roundToInt (proportion * offscreen)));
684 }
685 
getVerticalPosition() const686 double ListBox::getVerticalPosition() const
687 {
688     auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight();
689 
690     return offscreen > 0 ? viewport->getViewPositionY() / (double) offscreen
691                          : 0;
692 }
693 
getVisibleRowWidth() const694 int ListBox::getVisibleRowWidth() const noexcept
695 {
696     return viewport->getViewWidth();
697 }
698 
scrollToEnsureRowIsOnscreen(const int row)699 void ListBox::scrollToEnsureRowIsOnscreen (const int row)
700 {
701     viewport->scrollToEnsureRowIsOnscreen (row, getRowHeight());
702 }
703 
704 //==============================================================================
keyPressed(const KeyPress & key)705 bool ListBox::keyPressed (const KeyPress& key)
706 {
707     const int numVisibleRows = viewport->getHeight() / getRowHeight();
708 
709     const bool multiple = multipleSelection
710                             && lastRowSelected >= 0
711                             && key.getModifiers().isShiftDown();
712 
713     if (key.isKeyCode (KeyPress::upKey))
714     {
715         if (multiple)
716             selectRangeOfRows (lastRowSelected, lastRowSelected - 1);
717         else
718             selectRow (jmax (0, lastRowSelected - 1));
719     }
720     else if (key.isKeyCode (KeyPress::downKey))
721     {
722         if (multiple)
723             selectRangeOfRows (lastRowSelected, lastRowSelected + 1);
724         else
725             selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected + 1)));
726     }
727     else if (key.isKeyCode (KeyPress::pageUpKey))
728     {
729         if (multiple)
730             selectRangeOfRows (lastRowSelected, lastRowSelected - numVisibleRows);
731         else
732             selectRow (jmax (0, jmax (0, lastRowSelected) - numVisibleRows));
733     }
734     else if (key.isKeyCode (KeyPress::pageDownKey))
735     {
736         if (multiple)
737             selectRangeOfRows (lastRowSelected, lastRowSelected + numVisibleRows);
738         else
739             selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected) + numVisibleRows));
740     }
741     else if (key.isKeyCode (KeyPress::homeKey))
742     {
743         if (multiple)
744             selectRangeOfRows (lastRowSelected, 0);
745         else
746             selectRow (0);
747     }
748     else if (key.isKeyCode (KeyPress::endKey))
749     {
750         if (multiple)
751             selectRangeOfRows (lastRowSelected, totalItems - 1);
752         else
753             selectRow (totalItems - 1);
754     }
755     else if (key.isKeyCode (KeyPress::returnKey) && isRowSelected (lastRowSelected))
756     {
757         if (model != nullptr)
758             model->returnKeyPressed (lastRowSelected);
759     }
760     else if ((key.isKeyCode (KeyPress::deleteKey) || key.isKeyCode (KeyPress::backspaceKey))
761                && isRowSelected (lastRowSelected))
762     {
763         if (model != nullptr)
764             model->deleteKeyPressed (lastRowSelected);
765     }
766     else if (multipleSelection && key == KeyPress ('a', ModifierKeys::commandModifier, 0))
767     {
768         selectRangeOfRows (0, std::numeric_limits<int>::max());
769     }
770     else
771     {
772         return false;
773     }
774 
775     return true;
776 }
777 
keyStateChanged(const bool isKeyDown)778 bool ListBox::keyStateChanged (const bool isKeyDown)
779 {
780     return isKeyDown
781             && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey)
782                 || KeyPress::isKeyCurrentlyDown (KeyPress::pageUpKey)
783                 || KeyPress::isKeyCurrentlyDown (KeyPress::downKey)
784                 || KeyPress::isKeyCurrentlyDown (KeyPress::pageDownKey)
785                 || KeyPress::isKeyCurrentlyDown (KeyPress::homeKey)
786                 || KeyPress::isKeyCurrentlyDown (KeyPress::endKey)
787                 || KeyPress::isKeyCurrentlyDown (KeyPress::returnKey));
788 }
789 
mouseWheelMove(const MouseEvent & e,const MouseWheelDetails & wheel)790 void ListBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
791 {
792     bool eventWasUsed = false;
793 
794     if (wheel.deltaX != 0.0f && getHorizontalScrollBar().isVisible())
795     {
796         eventWasUsed = true;
797         getHorizontalScrollBar().mouseWheelMove (e, wheel);
798     }
799 
800     if (wheel.deltaY != 0.0f && getVerticalScrollBar().isVisible())
801     {
802         eventWasUsed = true;
803         getVerticalScrollBar().mouseWheelMove (e, wheel);
804     }
805 
806     if (! eventWasUsed)
807         Component::mouseWheelMove (e, wheel);
808 }
809 
mouseUp(const MouseEvent & e)810 void ListBox::mouseUp (const MouseEvent& e)
811 {
812     if (e.mouseWasClicked() && model != nullptr)
813         model->backgroundClicked (e);
814 }
815 
816 //==============================================================================
setRowHeight(const int newHeight)817 void ListBox::setRowHeight (const int newHeight)
818 {
819     rowHeight = jmax (1, newHeight);
820     viewport->setSingleStepSizes (20, rowHeight);
821     updateContent();
822 }
823 
getNumRowsOnScreen() const824 int ListBox::getNumRowsOnScreen() const noexcept
825 {
826     return viewport->getMaximumVisibleHeight() / rowHeight;
827 }
828 
setMinimumContentWidth(const int newMinimumWidth)829 void ListBox::setMinimumContentWidth (const int newMinimumWidth)
830 {
831     minimumRowWidth = newMinimumWidth;
832     updateContent();
833 }
834 
getVisibleContentWidth() const835 int ListBox::getVisibleContentWidth() const noexcept            { return viewport->getMaximumVisibleWidth(); }
836 
getVerticalScrollBar() const837 ScrollBar& ListBox::getVerticalScrollBar() const noexcept       { return viewport->getVerticalScrollBar(); }
getHorizontalScrollBar() const838 ScrollBar& ListBox::getHorizontalScrollBar() const noexcept     { return viewport->getHorizontalScrollBar(); }
839 
colourChanged()840 void ListBox::colourChanged()
841 {
842     setOpaque (findColour (backgroundColourId).isOpaque());
843     viewport->setOpaque (isOpaque());
844     repaint();
845 }
846 
parentHierarchyChanged()847 void ListBox::parentHierarchyChanged()
848 {
849     colourChanged();
850 }
851 
setOutlineThickness(int newThickness)852 void ListBox::setOutlineThickness (int newThickness)
853 {
854     outlineThickness = newThickness;
855     resized();
856 }
857 
setHeaderComponent(std::unique_ptr<Component> newHeaderComponent)858 void ListBox::setHeaderComponent (std::unique_ptr<Component> newHeaderComponent)
859 {
860     headerComponent = std::move (newHeaderComponent);
861     addAndMakeVisible (headerComponent.get());
862     ListBox::resized();
863 }
864 
repaintRow(const int rowNumber)865 void ListBox::repaintRow (const int rowNumber) noexcept
866 {
867     repaint (getRowPosition (rowNumber, true));
868 }
869 
createSnapshotOfRows(const SparseSet<int> & rows,int & imageX,int & imageY)870 Image ListBox::createSnapshotOfRows (const SparseSet<int>& rows, int& imageX, int& imageY)
871 {
872     Rectangle<int> imageArea;
873     auto firstRow = getRowContainingPosition (0, viewport->getY());
874 
875     for (int i = getNumRowsOnScreen() + 2; --i >= 0;)
876     {
877         if (rows.contains (firstRow + i))
878         {
879             if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i))
880             {
881                 auto pos = getLocalPoint (rowComp, Point<int>());
882 
883                 imageArea = imageArea.getUnion ({ pos.x, pos.y, rowComp->getWidth(), rowComp->getHeight() });
884             }
885         }
886     }
887 
888     imageArea = imageArea.getIntersection (getLocalBounds());
889     imageX = imageArea.getX();
890     imageY = imageArea.getY();
891 
892     auto listScale = Component::getApproximateScaleFactorForComponent (this);
893     Image snapshot (Image::ARGB,
894                     roundToInt ((float) imageArea.getWidth() * listScale),
895                     roundToInt ((float) imageArea.getHeight() * listScale),
896                     true);
897 
898     for (int i = getNumRowsOnScreen() + 2; --i >= 0;)
899     {
900         if (rows.contains (firstRow + i))
901         {
902             if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i))
903             {
904                 Graphics g (snapshot);
905                 g.setOrigin (getLocalPoint (rowComp, Point<int>()) - imageArea.getPosition());
906 
907                 auto rowScale = Component::getApproximateScaleFactorForComponent (rowComp);
908 
909                 if (g.reduceClipRegion (rowComp->getLocalBounds() * rowScale))
910                 {
911                     g.beginTransparencyLayer (0.6f);
912                     g.addTransform (AffineTransform::scale (rowScale));
913                     rowComp->paintEntireComponent (g, false);
914                     g.endTransparencyLayer();
915                 }
916             }
917         }
918     }
919 
920     return snapshot;
921 }
922 
startDragAndDrop(const MouseEvent & e,const SparseSet<int> & rowsToDrag,const var & dragDescription,bool allowDraggingToOtherWindows)923 void ListBox::startDragAndDrop (const MouseEvent& e, const SparseSet<int>& rowsToDrag, const var& dragDescription, bool allowDraggingToOtherWindows)
924 {
925     if (auto* dragContainer = DragAndDropContainer::findParentDragContainerFor (this))
926     {
927         int x, y;
928         auto dragImage = createSnapshotOfRows (rowsToDrag, x, y);
929 
930         auto p = Point<int> (x, y) - e.getEventRelativeTo (this).position.toInt();
931         dragContainer->startDragging (dragDescription, this, dragImage, allowDraggingToOtherWindows, &p, &e.source);
932     }
933     else
934     {
935         // to be able to do a drag-and-drop operation, the listbox needs to
936         // be inside a component which is also a DragAndDropContainer.
937         jassertfalse;
938     }
939 }
940 
941 //==============================================================================
refreshComponentForRow(int,bool,Component * existingComponentToUpdate)942 Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate)
943 {
944     ignoreUnused (existingComponentToUpdate);
945     jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components
946     return nullptr;
947 }
948 
listBoxItemClicked(int,const MouseEvent &)949 void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {}
listBoxItemDoubleClicked(int,const MouseEvent &)950 void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {}
backgroundClicked(const MouseEvent &)951 void ListBoxModel::backgroundClicked (const MouseEvent&) {}
selectedRowsChanged(int)952 void ListBoxModel::selectedRowsChanged (int) {}
deleteKeyPressed(int)953 void ListBoxModel::deleteKeyPressed (int) {}
returnKeyPressed(int)954 void ListBoxModel::returnKeyPressed (int) {}
listWasScrolled()955 void ListBoxModel::listWasScrolled() {}
getDragSourceDescription(const SparseSet<int> &)956 var ListBoxModel::getDragSourceDescription (const SparseSet<int>&)      { return {}; }
getTooltipForRow(int)957 String ListBoxModel::getTooltipForRow (int)                             { return {}; }
getMouseCursorForRow(int)958 MouseCursor ListBoxModel::getMouseCursorForRow (int)                    { return MouseCursor::NormalCursor; }
959 
960 } // namespace juce
961