/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { template static AccessibilityActions getListRowAccessibilityActions (RowComponentType& rowComponent) { auto onFocus = [&rowComponent] { rowComponent.owner.scrollToEnsureRowIsOnscreen (rowComponent.row); rowComponent.owner.selectRow (rowComponent.row); }; auto onPress = [&rowComponent, onFocus] { onFocus(); rowComponent.owner.keyPressed (KeyPress (KeyPress::returnKey)); }; auto onToggle = [&rowComponent] { rowComponent.owner.flipRowSelection (rowComponent.row); }; return AccessibilityActions().addAction (AccessibilityActionType::focus, std::move (onFocus)) .addAction (AccessibilityActionType::press, std::move (onPress)) .addAction (AccessibilityActionType::toggle, std::move (onToggle)); } void ListBox::checkModelPtrIsValid() const { #if ! JUCE_DISABLE_ASSERTIONS // If this is hit, the model was destroyed while the ListBox was still using it. // You should ensure that the model remains alive for as long as the ListBox holds a pointer to it. // If this assertion is hit in the destructor of a ListBox instance, do one of the following: // - Adjust the order in which your destructors run, so that the ListBox destructor runs // before the destructor of your ListBoxModel, or // - Call ListBox::setModel (nullptr) before destroying your ListBoxModel. jassert ((model == nullptr) == (weakModelPtr.lock() == nullptr)); #endif } class ListBox::RowComponent : public Component, public TooltipClient { public: RowComponent (ListBox& lb) : owner (lb) {} void paint (Graphics& g) override { if (auto* m = owner.getModel()) m->paintListBoxItem (row, g, getWidth(), getHeight(), isSelected); } void update (const int newRow, const bool nowSelected) { const auto rowHasChanged = (row != newRow); const auto selectionHasChanged = (isSelected != nowSelected); if (rowHasChanged || selectionHasChanged) { repaint(); if (rowHasChanged) row = newRow; if (selectionHasChanged) isSelected = nowSelected; } if (auto* m = owner.getModel()) { setMouseCursor (m->getMouseCursorForRow (row)); customComponent.reset (m->refreshComponentForRow (newRow, nowSelected, customComponent.release())); if (customComponent != nullptr) { addAndMakeVisible (customComponent.get()); customComponent->setBounds (getLocalBounds()); setFocusContainerType (FocusContainerType::focusContainer); } else { setFocusContainerType (FocusContainerType::none); } } } void performSelection (const MouseEvent& e, bool isMouseUp) { owner.selectRowsBasedOnModifierKeys (row, e.mods, isMouseUp); if (auto* m = owner.getModel()) m->listBoxItemClicked (row, e); } void mouseDown (const MouseEvent& e) override { isDragging = false; isDraggingToScroll = false; selectRowOnMouseUp = false; if (isEnabled()) { if (owner.selectOnMouseDown && ! isSelected && ! viewportWouldScrollOnEvent (owner.getViewport(), e.source)) performSelection (e, false); else selectRowOnMouseUp = true; } } void mouseUp (const MouseEvent& e) override { if (isEnabled() && selectRowOnMouseUp && ! (isDragging || isDraggingToScroll)) performSelection (e, true); } void mouseDoubleClick (const MouseEvent& e) override { if (isEnabled()) if (auto* m = owner.getModel()) m->listBoxItemDoubleClicked (row, e); } void mouseDrag (const MouseEvent& e) override { if (auto* m = owner.getModel()) { if (isEnabled() && e.mouseWasDraggedSinceMouseDown() && ! isDragging) { SparseSet rowsToDrag; if (owner.selectOnMouseDown || owner.isRowSelected (row)) rowsToDrag = owner.getSelectedRows(); else rowsToDrag.addRange (Range::withStartAndLength (row, 1)); if (rowsToDrag.size() > 0) { auto dragDescription = m->getDragSourceDescription (rowsToDrag); if (! (dragDescription.isVoid() || (dragDescription.isString() && dragDescription.toString().isEmpty()))) { isDragging = true; owner.startDragAndDrop (e, rowsToDrag, dragDescription, true); } } } } if (! isDraggingToScroll) if (auto* vp = owner.getViewport()) isDraggingToScroll = vp->isCurrentlyScrollingOnDrag(); } void resized() override { if (customComponent != nullptr) customComponent->setBounds (getLocalBounds()); } String getTooltip() override { if (auto* m = owner.getModel()) return m->getTooltipForRow (row); return {}; } //============================================================================== class RowAccessibilityHandler : public AccessibilityHandler { public: explicit RowAccessibilityHandler (RowComponent& rowComponentToWrap) : AccessibilityHandler (rowComponentToWrap, AccessibilityRole::listItem, getListRowAccessibilityActions (rowComponentToWrap), { std::make_unique (*this) }), rowComponent (rowComponentToWrap) { } String getTitle() const override { if (auto* m = rowComponent.owner.getModel()) return m->getNameForRow (rowComponent.row); return {}; } String getHelp() const override { return rowComponent.getTooltip(); } AccessibleState getCurrentState() const override { if (auto* m = rowComponent.owner.getModel()) if (rowComponent.row >= m->getNumRows()) return AccessibleState().withIgnored(); auto state = AccessibilityHandler::getCurrentState().withAccessibleOffscreen(); if (rowComponent.owner.multipleSelection) state = state.withMultiSelectable(); else state = state.withSelectable(); if (rowComponent.isSelected) state = state.withSelected(); return state; } private: class RowCellInterface : public AccessibilityCellInterface { public: explicit RowCellInterface (RowAccessibilityHandler& h) : handler (h) {} int getColumnIndex() const override { return 0; } int getColumnSpan() const override { return 1; } int getRowIndex() const override { const auto index = handler.rowComponent.row; if (handler.rowComponent.owner.hasAccessibleHeaderComponent()) return index + 1; return index; } int getRowSpan() const override { return 1; } int getDisclosureLevel() const override { return 0; } const AccessibilityHandler* getTableHandler() const override { return handler.rowComponent.owner.getAccessibilityHandler(); } private: RowAccessibilityHandler& handler; }; RowComponent& rowComponent; }; std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this); } //============================================================================== ListBox& owner; std::unique_ptr customComponent; int row = -1; bool isSelected = false, isDragging = false, isDraggingToScroll = false, selectRowOnMouseUp = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComponent) }; //============================================================================== class ListBox::ListViewport : public Viewport, private Timer { public: ListViewport (ListBox& lb) : owner (lb) { setWantsKeyboardFocus (false); auto content = std::make_unique(); content->setWantsKeyboardFocus (false); setViewedComponent (content.release()); } RowComponent* getComponentForRow (int row) const noexcept { if (isPositiveAndBelow (row, rows.size())) return rows[row]; return nullptr; } RowComponent* getComponentForRowWrapped (int row) const noexcept { return rows[row % jmax (1, rows.size())]; } RowComponent* getComponentForRowIfOnscreen (int row) const noexcept { return (row >= firstIndex && row < firstIndex + rows.size()) ? getComponentForRowWrapped (row) : nullptr; } int getRowNumberOfComponent (Component* const rowComponent) const noexcept { const int index = getViewedComponent()->getIndexOfChildComponent (rowComponent); const int num = rows.size(); for (int i = num; --i >= 0;) if (((firstIndex + i) % jmax (1, num)) == index) return firstIndex + i; return -1; } void visibleAreaChanged (const Rectangle&) override { updateVisibleArea (true); if (auto* m = owner.getModel()) m->listWasScrolled(); startTimer (50); } void updateVisibleArea (const bool makeSureItUpdatesContent) { hasUpdated = false; auto& content = *getViewedComponent(); auto newX = content.getX(); auto newY = content.getY(); auto newW = jmax (owner.minimumRowWidth, getMaximumVisibleWidth()); auto newH = owner.totalItems * owner.getRowHeight(); if (newY + newH < getMaximumVisibleHeight() && newH > getMaximumVisibleHeight()) newY = getMaximumVisibleHeight() - newH; content.setBounds (newX, newY, newW, newH); if (makeSureItUpdatesContent && ! hasUpdated) updateContents(); } void updateContents() { hasUpdated = true; auto rowH = owner.getRowHeight(); auto& content = *getViewedComponent(); if (rowH > 0) { auto y = getViewPositionY(); auto w = content.getWidth(); const int numNeeded = 4 + getMaximumVisibleHeight() / rowH; rows.removeRange (numNeeded, rows.size()); while (numNeeded > rows.size()) { auto* newRow = rows.add (new RowComponent (owner)); content.addAndMakeVisible (newRow); } firstIndex = y / rowH; firstWholeIndex = (y + rowH - 1) / rowH; lastWholeIndex = (y + getMaximumVisibleHeight() - 1) / rowH; auto startIndex = jmax (0, firstIndex - 1); for (int i = 0; i < numNeeded; ++i) { const int row = i + startIndex; if (auto* rowComp = getComponentForRowWrapped (row)) { rowComp->setBounds (0, row * rowH, w, rowH); rowComp->update (row, owner.isRowSelected (row)); } } } if (owner.headerComponent != nullptr) owner.headerComponent->setBounds (owner.outlineThickness + content.getX(), owner.outlineThickness, jmax (owner.getWidth() - owner.outlineThickness * 2, content.getWidth()), owner.headerComponent->getHeight()); } void selectRow (const int row, const int rowH, const bool dontScroll, const int lastSelectedRow, const int totalRows, const bool isMouseClick) { hasUpdated = false; if (row < firstWholeIndex && ! dontScroll) { setViewPosition (getViewPositionX(), row * rowH); } else if (row >= lastWholeIndex && ! dontScroll) { const int rowsOnScreen = lastWholeIndex - firstWholeIndex; if (row >= lastSelectedRow + rowsOnScreen && rowsOnScreen < totalRows - 1 && ! isMouseClick) { setViewPosition (getViewPositionX(), jlimit (0, jmax (0, totalRows - rowsOnScreen), row) * rowH); } else { setViewPosition (getViewPositionX(), jmax (0, (row + 1) * rowH - getMaximumVisibleHeight())); } } if (! hasUpdated) updateContents(); } void scrollToEnsureRowIsOnscreen (const int row, const int rowH) { if (row < firstWholeIndex) { setViewPosition (getViewPositionX(), row * rowH); } else if (row >= lastWholeIndex) { setViewPosition (getViewPositionX(), jmax (0, (row + 1) * rowH - getMaximumVisibleHeight())); } } void paint (Graphics& g) override { if (isOpaque()) g.fillAll (owner.findColour (ListBox::backgroundColourId)); } bool keyPressed (const KeyPress& key) override { if (Viewport::respondsToKey (key)) { const int allowableMods = owner.multipleSelection ? ModifierKeys::shiftModifier : 0; if ((key.getModifiers().getRawFlags() & ~allowableMods) == 0) { // we want to avoid these keypresses going to the viewport, and instead allow // them to pass up to our listbox.. return false; } } return Viewport::keyPressed (key); } private: std::unique_ptr createAccessibilityHandler() override { return createIgnoredAccessibilityHandler (*this); } void timerCallback() override { stopTimer(); if (auto* handler = owner.getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::structureChanged); } ListBox& owner; OwnedArray rows; int firstIndex = 0, firstWholeIndex = 0, lastWholeIndex = 0; bool hasUpdated = false; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListViewport) }; //============================================================================== struct ListBoxMouseMoveSelector : public MouseListener { ListBoxMouseMoveSelector (ListBox& lb) : owner (lb) { owner.addMouseListener (this, true); } ~ListBoxMouseMoveSelector() override { owner.removeMouseListener (this); } void mouseMove (const MouseEvent& e) override { auto pos = e.getEventRelativeTo (&owner).position.toInt(); owner.selectRow (owner.getRowContainingPosition (pos.x, pos.y), true); } void mouseExit (const MouseEvent& e) override { mouseMove (e); } ListBox& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ListBoxMouseMoveSelector) }; //============================================================================== ListBox::ListBox (const String& name, ListBoxModel* const m) : Component (name) { viewport.reset (new ListViewport (*this)); addAndMakeVisible (viewport.get()); setWantsKeyboardFocus (true); setFocusContainerType (FocusContainerType::focusContainer); colourChanged(); setModel (m); } ListBox::~ListBox() { headerComponent.reset(); viewport.reset(); } void ListBox::assignModelPtr (ListBoxModel* const newModel) { model = newModel; #if ! JUCE_DISABLE_ASSERTIONS weakModelPtr = model != nullptr ? model->sharedState : nullptr; #endif } void ListBox::setModel (ListBoxModel* const newModel) { if (model != newModel) { assignModelPtr (newModel); repaint(); updateContent(); } } void ListBox::setMultipleSelectionEnabled (bool b) noexcept { multipleSelection = b; } void ListBox::setClickingTogglesRowSelection (bool b) noexcept { alwaysFlipSelection = b; } void ListBox::setRowSelectedOnMouseDown (bool b) noexcept { selectOnMouseDown = b; } void ListBox::setMouseMoveSelectsRows (bool b) { if (b) { if (mouseMoveSelector == nullptr) mouseMoveSelector.reset (new ListBoxMouseMoveSelector (*this)); } else { mouseMoveSelector.reset(); } } //============================================================================== void ListBox::paint (Graphics& g) { if (! hasDoneInitialUpdate) updateContent(); g.fillAll (findColour (backgroundColourId)); } void ListBox::paintOverChildren (Graphics& g) { if (outlineThickness > 0) { g.setColour (findColour (outlineColourId)); g.drawRect (getLocalBounds(), outlineThickness); } } void ListBox::resized() { viewport->setBoundsInset (BorderSize (outlineThickness + (headerComponent != nullptr ? headerComponent->getHeight() : 0), outlineThickness, outlineThickness, outlineThickness)); viewport->setSingleStepSizes (20, getRowHeight()); viewport->updateVisibleArea (false); } void ListBox::visibilityChanged() { viewport->updateVisibleArea (true); } Viewport* ListBox::getViewport() const noexcept { return viewport.get(); } //============================================================================== void ListBox::updateContent() { checkModelPtrIsValid(); hasDoneInitialUpdate = true; totalItems = (model != nullptr) ? model->getNumRows() : 0; bool selectionChanged = false; if (selected.size() > 0 && selected [selected.size() - 1] >= totalItems) { selected.removeRange ({ totalItems, std::numeric_limits::max() }); lastRowSelected = getSelectedRow (0); selectionChanged = true; } viewport->updateVisibleArea (isVisible()); viewport->resized(); if (selectionChanged) { if (model != nullptr) model->selectedRowsChanged (lastRowSelected); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } } //============================================================================== void ListBox::selectRow (int row, bool dontScroll, bool deselectOthersFirst) { selectRowInternal (row, dontScroll, deselectOthersFirst, false); } void ListBox::selectRowInternal (const int row, bool dontScroll, bool deselectOthersFirst, bool isMouseClick) { checkModelPtrIsValid(); if (! multipleSelection) deselectOthersFirst = true; if ((! isRowSelected (row)) || (deselectOthersFirst && getNumSelectedRows() > 1)) { if (isPositiveAndBelow (row, totalItems)) { if (deselectOthersFirst) selected.clear(); selected.addRange ({ row, row + 1 }); if (getHeight() == 0 || getWidth() == 0) dontScroll = true; viewport->selectRow (row, getRowHeight(), dontScroll, lastRowSelected, totalItems, isMouseClick); lastRowSelected = row; model->selectedRowsChanged (row); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } else { if (deselectOthersFirst) deselectAllRows(); } } } void ListBox::deselectRow (const int row) { checkModelPtrIsValid(); if (selected.contains (row)) { selected.removeRange ({ row, row + 1 }); if (row == lastRowSelected) lastRowSelected = getSelectedRow (0); viewport->updateContents(); model->selectedRowsChanged (lastRowSelected); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } } void ListBox::setSelectedRows (const SparseSet& setOfRowsToBeSelected, const NotificationType sendNotificationEventToModel) { checkModelPtrIsValid(); selected = setOfRowsToBeSelected; selected.removeRange ({ totalItems, std::numeric_limits::max() }); if (! isRowSelected (lastRowSelected)) lastRowSelected = getSelectedRow (0); viewport->updateContents(); if (model != nullptr && sendNotificationEventToModel == sendNotification) model->selectedRowsChanged (lastRowSelected); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } SparseSet ListBox::getSelectedRows() const { return selected; } void ListBox::selectRangeOfRows (int firstRow, int lastRow, bool dontScrollToShowThisRange) { if (multipleSelection && (firstRow != lastRow)) { const int numRows = totalItems - 1; firstRow = jlimit (0, jmax (0, numRows), firstRow); lastRow = jlimit (0, jmax (0, numRows), lastRow); selected.addRange ({ jmin (firstRow, lastRow), jmax (firstRow, lastRow) + 1 }); selected.removeRange ({ lastRow, lastRow + 1 }); } selectRowInternal (lastRow, dontScrollToShowThisRange, false, true); } void ListBox::flipRowSelection (const int row) { if (isRowSelected (row)) deselectRow (row); else selectRowInternal (row, false, false, true); } void ListBox::deselectAllRows() { checkModelPtrIsValid(); if (! selected.isEmpty()) { selected.clear(); lastRowSelected = -1; viewport->updateContents(); if (model != nullptr) model->selectedRowsChanged (lastRowSelected); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::rowSelectionChanged); } } void ListBox::selectRowsBasedOnModifierKeys (const int row, ModifierKeys mods, const bool isMouseUpEvent) { if (multipleSelection && (mods.isCommandDown() || alwaysFlipSelection)) { flipRowSelection (row); } else if (multipleSelection && mods.isShiftDown() && lastRowSelected >= 0) { selectRangeOfRows (lastRowSelected, row); } else if ((! mods.isPopupMenu()) || ! isRowSelected (row)) { selectRowInternal (row, false, ! (multipleSelection && (! isMouseUpEvent) && isRowSelected (row)), true); } } int ListBox::getNumSelectedRows() const { return selected.size(); } int ListBox::getSelectedRow (const int index) const { return (isPositiveAndBelow (index, selected.size())) ? selected [index] : -1; } bool ListBox::isRowSelected (const int row) const { return selected.contains (row); } int ListBox::getLastRowSelected() const { return isRowSelected (lastRowSelected) ? lastRowSelected : -1; } //============================================================================== int ListBox::getRowContainingPosition (const int x, const int y) const noexcept { if (isPositiveAndBelow (x, getWidth())) { const int row = (viewport->getViewPositionY() + y - viewport->getY()) / rowHeight; if (isPositiveAndBelow (row, totalItems)) return row; } return -1; } int ListBox::getInsertionIndexForPosition (const int x, const int y) const noexcept { if (isPositiveAndBelow (x, getWidth())) return jlimit (0, totalItems, (viewport->getViewPositionY() + y + rowHeight / 2 - viewport->getY()) / rowHeight); return -1; } Component* ListBox::getComponentForRowNumber (const int row) const noexcept { if (auto* listRowComp = viewport->getComponentForRowIfOnscreen (row)) return listRowComp->customComponent.get(); return nullptr; } int ListBox::getRowNumberOfComponent (Component* const rowComponent) const noexcept { return viewport->getRowNumberOfComponent (rowComponent); } Rectangle ListBox::getRowPosition (int rowNumber, bool relativeToComponentTopLeft) const noexcept { auto y = viewport->getY() + rowHeight * rowNumber; if (relativeToComponentTopLeft) y -= viewport->getViewPositionY(); return { viewport->getX(), y, viewport->getViewedComponent()->getWidth(), rowHeight }; } void ListBox::setVerticalPosition (const double proportion) { auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); viewport->setViewPosition (viewport->getViewPositionX(), jmax (0, roundToInt (proportion * offscreen))); } double ListBox::getVerticalPosition() const { auto offscreen = viewport->getViewedComponent()->getHeight() - viewport->getHeight(); return offscreen > 0 ? viewport->getViewPositionY() / (double) offscreen : 0; } int ListBox::getVisibleRowWidth() const noexcept { return viewport->getViewWidth(); } void ListBox::scrollToEnsureRowIsOnscreen (const int row) { viewport->scrollToEnsureRowIsOnscreen (row, getRowHeight()); } //============================================================================== bool ListBox::keyPressed (const KeyPress& key) { checkModelPtrIsValid(); const int numVisibleRows = viewport->getHeight() / getRowHeight(); const bool multiple = multipleSelection && lastRowSelected >= 0 && key.getModifiers().isShiftDown(); if (key.isKeyCode (KeyPress::upKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected - 1); else selectRow (jmax (0, lastRowSelected - 1)); } else if (key.isKeyCode (KeyPress::downKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected + 1); else selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected + 1))); } else if (key.isKeyCode (KeyPress::pageUpKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected - numVisibleRows); else selectRow (jmax (0, jmax (0, lastRowSelected) - numVisibleRows)); } else if (key.isKeyCode (KeyPress::pageDownKey)) { if (multiple) selectRangeOfRows (lastRowSelected, lastRowSelected + numVisibleRows); else selectRow (jmin (totalItems - 1, jmax (0, lastRowSelected) + numVisibleRows)); } else if (key.isKeyCode (KeyPress::homeKey)) { if (multiple) selectRangeOfRows (lastRowSelected, 0); else selectRow (0); } else if (key.isKeyCode (KeyPress::endKey)) { if (multiple) selectRangeOfRows (lastRowSelected, totalItems - 1); else selectRow (totalItems - 1); } else if (key.isKeyCode (KeyPress::returnKey) && isRowSelected (lastRowSelected)) { if (model != nullptr) model->returnKeyPressed (lastRowSelected); } else if ((key.isKeyCode (KeyPress::deleteKey) || key.isKeyCode (KeyPress::backspaceKey)) && isRowSelected (lastRowSelected)) { if (model != nullptr) model->deleteKeyPressed (lastRowSelected); } else if (multipleSelection && key == KeyPress ('a', ModifierKeys::commandModifier, 0)) { selectRangeOfRows (0, std::numeric_limits::max()); } else { return false; } return true; } bool ListBox::keyStateChanged (const bool isKeyDown) { return isKeyDown && (KeyPress::isKeyCurrentlyDown (KeyPress::upKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageUpKey) || KeyPress::isKeyCurrentlyDown (KeyPress::downKey) || KeyPress::isKeyCurrentlyDown (KeyPress::pageDownKey) || KeyPress::isKeyCurrentlyDown (KeyPress::homeKey) || KeyPress::isKeyCurrentlyDown (KeyPress::endKey) || KeyPress::isKeyCurrentlyDown (KeyPress::returnKey)); } void ListBox::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) { bool eventWasUsed = false; if (wheel.deltaX != 0.0f && getHorizontalScrollBar().isVisible()) { eventWasUsed = true; getHorizontalScrollBar().mouseWheelMove (e, wheel); } if (wheel.deltaY != 0.0f && getVerticalScrollBar().isVisible()) { eventWasUsed = true; getVerticalScrollBar().mouseWheelMove (e, wheel); } if (! eventWasUsed) Component::mouseWheelMove (e, wheel); } void ListBox::mouseUp (const MouseEvent& e) { checkModelPtrIsValid(); if (e.mouseWasClicked() && model != nullptr) model->backgroundClicked (e); } //============================================================================== void ListBox::setRowHeight (const int newHeight) { rowHeight = jmax (1, newHeight); viewport->setSingleStepSizes (20, rowHeight); updateContent(); } int ListBox::getNumRowsOnScreen() const noexcept { return viewport->getMaximumVisibleHeight() / rowHeight; } void ListBox::setMinimumContentWidth (const int newMinimumWidth) { minimumRowWidth = newMinimumWidth; updateContent(); } int ListBox::getVisibleContentWidth() const noexcept { return viewport->getMaximumVisibleWidth(); } ScrollBar& ListBox::getVerticalScrollBar() const noexcept { return viewport->getVerticalScrollBar(); } ScrollBar& ListBox::getHorizontalScrollBar() const noexcept { return viewport->getHorizontalScrollBar(); } void ListBox::colourChanged() { setOpaque (findColour (backgroundColourId).isOpaque()); viewport->setOpaque (isOpaque()); repaint(); } void ListBox::parentHierarchyChanged() { colourChanged(); } void ListBox::setOutlineThickness (int newThickness) { outlineThickness = newThickness; resized(); } void ListBox::setHeaderComponent (std::unique_ptr newHeaderComponent) { headerComponent = std::move (newHeaderComponent); addAndMakeVisible (headerComponent.get()); ListBox::resized(); invalidateAccessibilityHandler(); } bool ListBox::hasAccessibleHeaderComponent() const { return headerComponent != nullptr && headerComponent->getAccessibilityHandler() != nullptr; } void ListBox::repaintRow (const int rowNumber) noexcept { repaint (getRowPosition (rowNumber, true)); } ScaledImage ListBox::createSnapshotOfRows (const SparseSet& rows, int& imageX, int& imageY) { Rectangle imageArea; auto firstRow = getRowContainingPosition (0, viewport->getY()); for (int i = getNumRowsOnScreen() + 2; --i >= 0;) { if (rows.contains (firstRow + i)) { if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) { auto pos = getLocalPoint (rowComp, Point()); imageArea = imageArea.getUnion ({ pos.x, pos.y, rowComp->getWidth(), rowComp->getHeight() }); } } } imageArea = imageArea.getIntersection (getLocalBounds()); imageX = imageArea.getX(); imageY = imageArea.getY(); const auto additionalScale = 2.0f; const auto listScale = Component::getApproximateScaleFactorForComponent (this) * additionalScale; Image snapshot (Image::ARGB, roundToInt ((float) imageArea.getWidth() * listScale), roundToInt ((float) imageArea.getHeight() * listScale), true); for (int i = getNumRowsOnScreen() + 2; --i >= 0;) { if (rows.contains (firstRow + i)) { if (auto* rowComp = viewport->getComponentForRowIfOnscreen (firstRow + i)) { Graphics g (snapshot); g.setOrigin ((getLocalPoint (rowComp, Point()) - imageArea.getPosition()) * additionalScale); const auto rowScale = Component::getApproximateScaleFactorForComponent (rowComp) * additionalScale; if (g.reduceClipRegion (rowComp->getLocalBounds() * rowScale)) { g.beginTransparencyLayer (0.6f); g.addTransform (AffineTransform::scale (rowScale)); rowComp->paintEntireComponent (g, false); g.endTransparencyLayer(); } } } } return { snapshot, additionalScale }; } void ListBox::startDragAndDrop (const MouseEvent& e, const SparseSet& rowsToDrag, const var& dragDescription, bool allowDraggingToOtherWindows) { if (auto* dragContainer = DragAndDropContainer::findParentDragContainerFor (this)) { int x, y; auto dragImage = createSnapshotOfRows (rowsToDrag, x, y); auto p = Point (x, y) - e.getEventRelativeTo (this).position.toInt(); dragContainer->startDragging (dragDescription, this, dragImage, allowDraggingToOtherWindows, &p, &e.source); } else { // to be able to do a drag-and-drop operation, the listbox needs to // be inside a component which is also a DragAndDropContainer. jassertfalse; } } std::unique_ptr ListBox::createAccessibilityHandler() { class TableInterface : public AccessibilityTableInterface { public: explicit TableInterface (ListBox& listBoxToWrap) : listBox (listBoxToWrap) { } int getNumRows() const override { listBox.checkModelPtrIsValid(); if (listBox.model == nullptr) return 0; const auto numRows = listBox.model->getNumRows(); if (listBox.hasAccessibleHeaderComponent()) return numRows + 1; return numRows; } int getNumColumns() const override { return 1; } const AccessibilityHandler* getCellHandler (int row, int) const override { if (auto* headerHandler = getHeaderHandler()) { if (row == 0) return headerHandler; --row; } if (auto* rowComponent = listBox.viewport->getComponentForRow (row)) return rowComponent->getAccessibilityHandler(); return nullptr; } private: const AccessibilityHandler* getHeaderHandler() const { if (listBox.hasAccessibleHeaderComponent()) return listBox.headerComponent->getAccessibilityHandler(); return nullptr; } ListBox& listBox; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableInterface) }; return std::make_unique (*this, AccessibilityRole::list, AccessibilityActions{}, AccessibilityHandler::Interfaces { std::make_unique (*this) }); } //============================================================================== Component* ListBoxModel::refreshComponentForRow (int, bool, Component* existingComponentToUpdate) { ignoreUnused (existingComponentToUpdate); jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components return nullptr; } String ListBoxModel::getNameForRow (int rowNumber) { return "Row " + String (rowNumber + 1); } void ListBoxModel::listBoxItemClicked (int, const MouseEvent&) {} void ListBoxModel::listBoxItemDoubleClicked (int, const MouseEvent&) {} void ListBoxModel::backgroundClicked (const MouseEvent&) {} void ListBoxModel::selectedRowsChanged (int) {} void ListBoxModel::deleteKeyPressed (int) {} void ListBoxModel::returnKeyPressed (int) {} void ListBoxModel::listWasScrolled() {} var ListBoxModel::getDragSourceDescription (const SparseSet&) { return {}; } String ListBoxModel::getTooltipForRow (int) { return {}; } MouseCursor ListBoxModel::getMouseCursorForRow (int) { return MouseCursor::NormalCursor; } } // namespace juce