/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). 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 { static const Identifier tableColumnProperty { "_tableColumnId" }; static const Identifier tableAccessiblePlaceholderProperty { "_accessiblePlaceholder" }; class TableListBox::RowComp final : public TooltipClient, public ComponentWithListRowMouseBehaviours { public: explicit RowComp (TableListBox& tlb) : owner (tlb) { setFocusContainerType (FocusContainerType::focusContainer); } void paint (Graphics& g) override { if (auto* tableModel = owner.getTableListBoxModel()) { tableModel->paintRowBackground (g, getRow(), getWidth(), getHeight(), isSelected()); auto& headerComp = owner.getHeader(); const auto numColumns = jmin ((int) columnComponents.size(), headerComp.getNumColumns (true)); const auto clipBounds = g.getClipBounds(); for (int i = 0; i < numColumns; ++i) { if (columnComponents[(size_t) i]->getProperties().contains (tableAccessiblePlaceholderProperty)) { auto columnRect = headerComp.getColumnPosition (i).withHeight (getHeight()); if (columnRect.getX() >= clipBounds.getRight()) break; if (columnRect.getRight() > clipBounds.getX()) { Graphics::ScopedSaveState ss (g); if (g.reduceClipRegion (columnRect)) { g.setOrigin (columnRect.getX(), 0); tableModel->paintCell (g, getRow(), headerComp.getColumnIdOfIndex (i, true), columnRect.getWidth(), columnRect.getHeight(), isSelected()); } } } } } } void update (int newRow, bool isNowSelected) { jassert (newRow >= 0); updateRowAndSelection (newRow, isNowSelected); auto* tableModel = owner.getTableListBoxModel(); if (tableModel != nullptr && getRow() < owner.getNumRows()) { const ComponentDeleter deleter { columnForComponent }; const auto numColumns = owner.getHeader().getNumColumns (true); while (numColumns < (int) columnComponents.size()) columnComponents.pop_back(); while ((int) columnComponents.size() < numColumns) columnComponents.emplace_back (nullptr, deleter); for (int i = 0; i < numColumns; ++i) { auto columnId = owner.getHeader().getColumnIdOfIndex (i, true); auto originalComp = std::move (columnComponents[(size_t) i]); auto oldCustomComp = originalComp != nullptr && ! originalComp->getProperties().contains (tableAccessiblePlaceholderProperty) ? std::move (originalComp) : std::unique_ptr { nullptr, deleter }; auto compToRefresh = oldCustomComp != nullptr && columnId == static_cast (oldCustomComp->getProperties()[tableColumnProperty]) ? std::move (oldCustomComp) : std::unique_ptr { nullptr, deleter }; columnForComponent.erase (compToRefresh.get()); std::unique_ptr newCustomComp { tableModel->refreshComponentForCell (getRow(), columnId, isSelected(), compToRefresh.release()), deleter }; auto columnComp = [&] { // We got a result from refreshComponentForCell, so use that if (newCustomComp != nullptr) return std::move (newCustomComp); // There was already a placeholder component for this column if (originalComp != nullptr) return std::move (originalComp); // Create a new placeholder component to use std::unique_ptr comp { new Component, deleter }; comp->setInterceptsMouseClicks (false, false); comp->getProperties().set (tableAccessiblePlaceholderProperty, true); return comp; }(); columnForComponent.emplace (columnComp.get(), i); // In order for navigation to work correctly on macOS, the number of child // accessibility elements on each row must match the number of header accessibility // elements. columnComp->setFocusContainerType (FocusContainerType::focusContainer); columnComp->getProperties().set (tableColumnProperty, columnId); addAndMakeVisible (*columnComp); columnComponents[(size_t) i] = std::move (columnComp); resizeCustomComp (i); } } else { columnComponents.clear(); } } void resized() override { for (auto i = (int) columnComponents.size(); --i >= 0;) resizeCustomComp (i); } void resizeCustomComp (int index) { if (auto& c = columnComponents[(size_t) index]) { c->setBounds (owner.getHeader() .getColumnPosition (index) .withY (0) .withHeight (getHeight())); } } void performSelection (const MouseEvent& e, bool isMouseUp) { owner.selectRowsBasedOnModifierKeys (getRow(), e.mods, isMouseUp); auto columnId = owner.getHeader().getColumnIdAtX (e.x); if (columnId != 0) if (auto* m = owner.getTableListBoxModel()) m->cellClicked (getRow(), columnId, e); } void mouseDoubleClick (const MouseEvent& e) override { if (! isEnabled()) return; const auto columnId = owner.getHeader().getColumnIdAtX (e.x); if (columnId != 0) if (auto* m = owner.getTableListBoxModel()) m->cellDoubleClicked (getRow(), columnId, e); } String getTooltip() override { auto columnId = owner.getHeader().getColumnIdAtX (getMouseXYRelative().getX()); if (columnId != 0) if (auto* m = owner.getTableListBoxModel()) return m->getCellTooltip (getRow(), columnId); return {}; } Component* findChildComponentForColumn (int columnId) const { const auto index = (size_t) owner.getHeader().getIndexOfColumnId (columnId, true); if (isPositiveAndBelow (index, columnComponents.size())) return columnComponents[index].get(); return nullptr; } int getColumnNumberOfComponent (const Component* comp) const { const auto iter = columnForComponent.find (comp); return iter != columnForComponent.cend() ? iter->second : -1; } std::unique_ptr createAccessibilityHandler() override { return std::make_unique (*this); } TableListBox& getOwner() const { return owner; } private: //============================================================================== class RowAccessibilityHandler final : public AccessibilityHandler { public: RowAccessibilityHandler (RowComp& rowComp) : AccessibilityHandler (rowComp, AccessibilityRole::row, getListRowAccessibilityActions (rowComp), { std::make_unique (*this) }), rowComponent (rowComp) { } String getTitle() const override { if (auto* m = rowComponent.owner.ListBox::model) return m->getNameForRow (rowComponent.getRow()); return {}; } String getHelp() const override { return rowComponent.getTooltip(); } AccessibleState getCurrentState() const override { if (auto* m = rowComponent.owner.getTableListBoxModel()) if (rowComponent.getRow() >= m->getNumRows()) return AccessibleState().withIgnored(); auto state = AccessibilityHandler::getCurrentState(); if (rowComponent.owner.multipleSelection) state = state.withMultiSelectable(); else state = state.withSelectable(); if (rowComponent.isSelected()) return state.withSelected(); return state; } private: class RowComponentCellInterface final : public AccessibilityCellInterface { public: RowComponentCellInterface (RowAccessibilityHandler& handler) : owner (handler) { } int getDisclosureLevel() const override { return 0; } const AccessibilityHandler* getTableHandler() const override { return owner.rowComponent.owner.getAccessibilityHandler(); } private: RowAccessibilityHandler& owner; }; private: RowComp& rowComponent; }; //============================================================================== class ComponentDeleter { public: explicit ComponentDeleter (std::map& locations) : columnForComponent (&locations) {} void operator() (Component* comp) const { columnForComponent->erase (comp); if (comp != nullptr) delete comp; } private: std::map* columnForComponent; }; TableListBox& owner; std::map columnForComponent; std::vector> columnComponents; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RowComp) }; //============================================================================== class TableListBox::Header final : public TableHeaderComponent { public: Header (TableListBox& tlb) : owner (tlb) {} void addMenuItems (PopupMenu& menu, int columnIdClicked) { if (owner.isAutoSizeMenuOptionShown()) { menu.addItem (autoSizeColumnId, TRANS ("Auto-size this column"), columnIdClicked != 0); menu.addItem (autoSizeAllId, TRANS ("Auto-size all columns"), owner.getHeader().getNumColumns (true) > 0); menu.addSeparator(); } TableHeaderComponent::addMenuItems (menu, columnIdClicked); } void reactToMenuItem (int menuReturnId, int columnIdClicked) { switch (menuReturnId) { case autoSizeColumnId: owner.autoSizeColumn (columnIdClicked); break; case autoSizeAllId: owner.autoSizeAllColumns(); break; default: TableHeaderComponent::reactToMenuItem (menuReturnId, columnIdClicked); break; } } private: TableListBox& owner; enum { autoSizeColumnId = 0xf836743, autoSizeAllId = 0xf836744 }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Header) }; //============================================================================== TableListBox::TableListBox (const String& name, TableListBoxModel* const m) : ListBox (name, nullptr), model (m) { ListBox::assignModelPtr (this); setHeader (std::make_unique
(*this)); } TableListBox::~TableListBox() { } void TableListBox::setModel (TableListBoxModel* newModel) { if (model != newModel) { model = newModel; updateContent(); } } void TableListBox::setHeader (std::unique_ptr newHeader) { if (newHeader == nullptr) { jassertfalse; // you need to supply a real header for a table! return; } Rectangle newBounds (100, 28); if (header != nullptr) newBounds = header->getBounds(); header = newHeader.get(); header->setBounds (newBounds); setHeaderComponent (std::move (newHeader)); header->addListener (this); } int TableListBox::getHeaderHeight() const noexcept { return header->getHeight(); } void TableListBox::setHeaderHeight (int newHeight) { header->setSize (header->getWidth(), newHeight); resized(); } void TableListBox::autoSizeColumn (int columnId) { auto width = model != nullptr ? model->getColumnAutoSizeWidth (columnId) : 0; if (width > 0) header->setColumnWidth (columnId, width); } void TableListBox::autoSizeAllColumns() { for (int i = 0; i < header->getNumColumns (true); ++i) autoSizeColumn (header->getColumnIdOfIndex (i, true)); } void TableListBox::setAutoSizeMenuOptionShown (bool shouldBeShown) noexcept { autoSizeOptionsShown = shouldBeShown; } Rectangle TableListBox::getCellPosition (int columnId, int rowNumber, bool relativeToComponentTopLeft) const { auto headerCell = header->getColumnPosition (header->getIndexOfColumnId (columnId, true)); if (relativeToComponentTopLeft) headerCell.translate (header->getX(), 0); return getRowPosition (rowNumber, relativeToComponentTopLeft) .withX (headerCell.getX()) .withWidth (headerCell.getWidth()); } Component* TableListBox::getCellComponent (int columnId, int rowNumber) const { if (auto* rowComp = dynamic_cast (getComponentForRowNumber (rowNumber))) return rowComp->findChildComponentForColumn (columnId); return nullptr; } void TableListBox::scrollToEnsureColumnIsOnscreen (int columnId) { auto& scrollbar = getHorizontalScrollBar(); auto pos = header->getColumnPosition (header->getIndexOfColumnId (columnId, true)); auto x = scrollbar.getCurrentRangeStart(); auto w = scrollbar.getCurrentRangeSize(); if (pos.getX() < x) x = pos.getX(); else if (pos.getRight() > x + w) x += jmax (0.0, pos.getRight() - (x + w)); scrollbar.setCurrentRangeStart (x); } int TableListBox::getNumRows() { return model != nullptr ? model->getNumRows() : 0; } void TableListBox::paintListBoxItem (int, Graphics&, int, int, bool) { } Component* TableListBox::refreshComponentForRow (int rowNumber, bool rowSelected, Component* existingComponentToUpdate) { if (existingComponentToUpdate == nullptr) existingComponentToUpdate = new RowComp (*this); static_cast (existingComponentToUpdate)->update (rowNumber, rowSelected); return existingComponentToUpdate; } void TableListBox::selectedRowsChanged (int row) { if (model != nullptr) model->selectedRowsChanged (row); } void TableListBox::deleteKeyPressed (int row) { if (model != nullptr) model->deleteKeyPressed (row); } void TableListBox::returnKeyPressed (int row) { if (model != nullptr) model->returnKeyPressed (row); } void TableListBox::backgroundClicked (const MouseEvent& e) { if (model != nullptr) model->backgroundClicked (e); } void TableListBox::listWasScrolled() { if (model != nullptr) model->listWasScrolled(); } void TableListBox::tableColumnsChanged (TableHeaderComponent*) { setMinimumContentWidth (header->getTotalWidth()); repaint(); updateColumnComponents(); } void TableListBox::tableColumnsResized (TableHeaderComponent*) { setMinimumContentWidth (header->getTotalWidth()); repaint(); updateColumnComponents(); } void TableListBox::tableSortOrderChanged (TableHeaderComponent*) { if (model != nullptr) model->sortOrderChanged (header->getSortColumnId(), header->isSortedForwards()); } void TableListBox::tableColumnDraggingChanged (TableHeaderComponent*, int columnIdNowBeingDragged_) { columnIdNowBeingDragged = columnIdNowBeingDragged_; repaint(); } void TableListBox::resized() { ListBox::resized(); header->resizeAllColumnsToFit (getVisibleContentWidth()); setMinimumContentWidth (header->getTotalWidth()); } void TableListBox::updateColumnComponents() const { auto firstRow = getRowContainingPosition (0, 0); for (int i = firstRow + getNumRowsOnScreen() + 2; --i >= firstRow;) if (auto* rowComp = dynamic_cast (getComponentForRowNumber (i))) rowComp->resized(); } template Optional findRecursively (const AccessibilityHandler& handler, Component* outermost, FindIndex&& findIndexOfComponent) { for (auto* comp = &handler.getComponent(); comp != outermost; comp = comp->getParentComponent()) { const auto result = findIndexOfComponent (comp); if (result != -1) return AccessibilityTableInterface::Span { result, 1 }; } return nullopt; } std::unique_ptr TableListBox::createAccessibilityHandler() { class TableInterface final : public AccessibilityTableInterface { public: explicit TableInterface (TableListBox& tableListBoxToWrap) : tableListBox (tableListBoxToWrap) { } int getNumRows() const override { if (auto* tableModel = tableListBox.getTableListBoxModel()) return tableModel->getNumRows(); return 0; } int getNumColumns() const override { return tableListBox.getHeader().getNumColumns (true); } const AccessibilityHandler* getRowHandler (int row) const override { if (isPositiveAndBelow (row, getNumRows())) if (auto* rowComp = tableListBox.getComponentForRowNumber (row)) return rowComp->getAccessibilityHandler(); return nullptr; } const AccessibilityHandler* getCellHandler (int row, int column) const override { if (isPositiveAndBelow (row, getNumRows()) && isPositiveAndBelow (column, getNumColumns())) if (auto* cellComponent = tableListBox.getCellComponent (tableListBox.getHeader().getColumnIdOfIndex (column, true), row)) return cellComponent->getAccessibilityHandler(); return nullptr; } const AccessibilityHandler* getHeaderHandler() const override { if (tableListBox.hasAccessibleHeaderComponent()) return tableListBox.headerComponent->getAccessibilityHandler(); return nullptr; } Optional getRowSpan (const AccessibilityHandler& handler) const override { if (tableListBox.isParentOf (&handler.getComponent())) return findRecursively (handler, &tableListBox, [&] (auto* c) { return tableListBox.getRowNumberOfComponent (c); }); return nullopt; } Optional getColumnSpan (const AccessibilityHandler& handler) const override { if (const auto rowSpan = getRowSpan (handler)) if (auto* rowComponent = dynamic_cast (tableListBox.getComponentForRowNumber (rowSpan->begin))) return findRecursively (handler, &tableListBox, [&] (auto* c) { return rowComponent->getColumnNumberOfComponent (c); }); return nullopt; } void showCell (const AccessibilityHandler& handler) const override { const auto row = getRowSpan (handler); const auto col = getColumnSpan (handler); if (row.hasValue() && col.hasValue()) { tableListBox.scrollToEnsureRowIsOnscreen (row->begin); tableListBox.scrollToEnsureColumnIsOnscreen (col->begin); } } private: TableListBox& tableListBox; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableInterface) }; return std::make_unique (*this, AccessibilityRole::table, AccessibilityActions{}, AccessibilityHandler::Interfaces { std::make_unique (*this) }); } //============================================================================== void TableListBoxModel::cellClicked (int, int, const MouseEvent&) {} void TableListBoxModel::cellDoubleClicked (int, int, const MouseEvent&) {} void TableListBoxModel::backgroundClicked (const MouseEvent&) {} void TableListBoxModel::sortOrderChanged (int, bool) {} int TableListBoxModel::getColumnAutoSizeWidth (int) { return 0; } void TableListBoxModel::selectedRowsChanged (int) {} void TableListBoxModel::deleteKeyPressed (int) {} void TableListBoxModel::returnKeyPressed (int) {} void TableListBoxModel::listWasScrolled() {} String TableListBoxModel::getCellTooltip (int /*rowNumber*/, int /*columnId*/) { return {}; } var TableListBoxModel::getDragSourceDescription (const SparseSet&) { return {}; } Component* TableListBoxModel::refreshComponentForCell (int, int, bool, [[maybe_unused]] Component* existingComponentToUpdate) { jassert (existingComponentToUpdate == nullptr); // indicates a failure in the code that recycles the components return nullptr; } } // namespace juce