/* ============================================================================== This file is part of the JUCE library - "Jules' Utility Class Extensions" Copyright 2004-9 by Raw Material Software Ltd. ------------------------------------------------------------------------------ JUCE can be redistributed and/or modified under the terms of the GNU General Public License (Version 2), as published by the Free Software Foundation. A copy of the license is included in the JUCE distribution, or can be found online at www.gnu.org/licenses. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.rawmaterialsoftware.com/juce for more information. ============================================================================== */ #include "../../../core/juce_StandardHeader.h" BEGIN_JUCE_NAMESPACE #include "juce_TreeView.h" #include "../lookandfeel/juce_LookAndFeel.h" #include "../../../containers/juce_BitArray.h" #include "../mouse/juce_DragAndDropContainer.h" #include "../../graphics/imaging/juce_Image.h" //============================================================================== class TreeViewContentComponent : public Component, public TooltipClient { public: TreeViewContentComponent (TreeView* const owner_) : owner (owner_), buttonUnderMouse (0), isDragging (false) { } ~TreeViewContentComponent() { deleteAllChildren(); } void mouseDown (const MouseEvent& e) { updateButtonUnderMouse (e); isDragging = false; needSelectionOnMouseUp = false; Rectangle pos; TreeViewItem* const item = findItemAt (e.y, pos); if (item == 0) return; // (if the open/close buttons are hidden, we'll treat clicks to the left of the item // as selection clicks) if (e.x < pos.getX() && owner->openCloseButtonsVisible) { if (e.x >= pos.getX() - owner->getIndentSize()) item->setOpen (! item->isOpen()); // (clicks to the left of an open/close button are ignored) } else { // mouse-down inside the body of the item.. if (! owner->isMultiSelectEnabled()) item->setSelected (true, true); else if (item->isSelected()) needSelectionOnMouseUp = ! e.mods.isPopupMenu(); else selectBasedOnModifiers (item, e.mods); MouseEvent e2 (e); e2.x -= pos.getX(); e2.y -= pos.getY(); if (e2.x >= 0) item->itemClicked (e2); } } void mouseUp (const MouseEvent& e) { updateButtonUnderMouse (e); if (needSelectionOnMouseUp && e.mouseWasClicked()) { Rectangle pos; TreeViewItem* const item = findItemAt (e.y, pos); if (item != 0) selectBasedOnModifiers (item, e.mods); } } void mouseDoubleClick (const MouseEvent& e) { if (e.getNumberOfClicks() != 3) // ignore triple clicks { Rectangle pos; TreeViewItem* const item = findItemAt (e.y, pos); if (item != 0 && (e.x >= pos.getX() || ! owner->openCloseButtonsVisible)) { MouseEvent e2 (e); e2.x -= pos.getX(); e2.y -= pos.getY(); item->itemDoubleClicked (e2); } } } void mouseDrag (const MouseEvent& e) { if (isEnabled() && ! (isDragging || e.mouseWasClicked() || e.getDistanceFromDragStart() < 5 || e.mods.isPopupMenu())) { isDragging = true; Rectangle pos; TreeViewItem* const item = findItemAt (e.getMouseDownY(), pos); if (item != 0 && e.getMouseDownX() >= pos.getX()) { const String dragDescription (item->getDragSourceDescription()); if (dragDescription.isNotEmpty()) { DragAndDropContainer* const dragContainer = DragAndDropContainer::findParentDragContainerFor (this); if (dragContainer != 0) { pos.setSize (pos.getWidth(), item->itemHeight); Image* dragImage = Component::createComponentSnapshot (pos, true); dragImage->multiplyAllAlphas (0.6f); Point imageOffset ((float) (pos.getX() - e.x), (float) (pos.getY() - e.y)); dragContainer->startDragging (dragDescription, owner, dragImage, true, &imageOffset); } else { // to be able to do a drag-and-drop operation, the treeview needs to // be inside a component which is also a DragAndDropContainer. jassertfalse } } } } } void mouseMove (const MouseEvent& e) { updateButtonUnderMouse (e); } void mouseExit (const MouseEvent& e) { updateButtonUnderMouse (e); } void paint (Graphics& g); TreeViewItem* findItemAt (int y, Rectangle& itemPosition) const; void updateComponents() { const int visibleTop = -getY(); const int visibleBottom = visibleTop + getParentHeight(); BitArray itemsToKeep; TreeViewItem* item = owner->rootItem; int y = (item != 0 && !owner->rootItemVisible) ? -item->itemHeight : 0; while (item != 0 && y < visibleBottom) { y += item->itemHeight; if (y >= visibleTop) { const int index = rowComponentIds.indexOf (item->uid); if (index < 0) { Component* const comp = item->createItemComponent(); if (comp != 0) { addAndMakeVisible (comp); itemsToKeep.setBit (rowComponentItems.size()); rowComponentItems.add (item); rowComponentIds.add (item->uid); rowComponents.add (comp); } } else { itemsToKeep.setBit (index); } } item = item->getNextVisibleItem (true); } for (int i = rowComponentItems.size(); --i >= 0;) { Component* const comp = (Component*) rowComponents.getUnchecked(i); bool keep = false; if (isParentOf (comp)) { if (itemsToKeep[i]) { const TreeViewItem* const item = (TreeViewItem*) rowComponentItems.getUnchecked(i); Rectangle pos (item->getItemPosition (false)); pos.setSize (pos.getWidth(), item->itemHeight); if (pos.getBottom() >= visibleTop && pos.getY() < visibleBottom) { keep = true; comp->setBounds (pos); } } if ((! keep) && Component::isMouseButtonDownAnywhere() && (comp == Component::getComponentUnderMouse() || comp->isParentOf (Component::getComponentUnderMouse()))) { keep = true; comp->setSize (0, 0); } } if (! keep) { delete comp; rowComponents.remove (i); rowComponentIds.remove (i); rowComponentItems.remove (i); } } } void updateButtonUnderMouse (const MouseEvent& e) { TreeViewItem* newItem = 0; if (owner->openCloseButtonsVisible) { Rectangle pos; TreeViewItem* item = findItemAt (e.y, pos); if (item != 0 && e.x < pos.getX() && e.x >= pos.getX() - owner->getIndentSize()) { newItem = item; if (! newItem->mightContainSubItems()) newItem = 0; } } if (buttonUnderMouse != newItem) { if (buttonUnderMouse != 0 && containsItem (buttonUnderMouse)) { const Rectangle r (buttonUnderMouse->getItemPosition (false)); repaint (0, r.getY(), r.getX(), buttonUnderMouse->getItemHeight()); } buttonUnderMouse = newItem; if (buttonUnderMouse != 0) { const Rectangle r (buttonUnderMouse->getItemPosition (false)); repaint (0, r.getY(), r.getX(), buttonUnderMouse->getItemHeight()); } } } bool isMouseOverButton (TreeViewItem* item) const throw() { return item == buttonUnderMouse; } void resized() { owner->itemsChanged(); } const String getTooltip() { int x, y; getMouseXYRelative (x, y); Rectangle pos; TreeViewItem* const item = findItemAt (y, pos); if (item != 0) return item->getTooltip(); return owner->getTooltip(); } //============================================================================== juce_UseDebuggingNewOperator private: TreeView* const owner; VoidArray rowComponentItems; Array rowComponentIds; VoidArray rowComponents; TreeViewItem* buttonUnderMouse; bool isDragging, needSelectionOnMouseUp; TreeViewContentComponent (const TreeViewContentComponent&); const TreeViewContentComponent& operator= (const TreeViewContentComponent&); void selectBasedOnModifiers (TreeViewItem* const item, const ModifierKeys& modifiers) { TreeViewItem* firstSelected = 0; if (modifiers.isShiftDown() && ((firstSelected = owner->getSelectedItem (0)) != 0)) { TreeViewItem* const lastSelected = owner->getSelectedItem (owner->getNumSelectedItems() - 1); jassert (lastSelected != 0); int rowStart = firstSelected->getRowNumberInTree(); int rowEnd = lastSelected->getRowNumberInTree(); if (rowStart > rowEnd) swapVariables (rowStart, rowEnd); int ourRow = item->getRowNumberInTree(); int otherEnd = ourRow < rowEnd ? rowStart : rowEnd; if (ourRow > otherEnd) swapVariables (ourRow, otherEnd); for (int i = ourRow; i <= otherEnd; ++i) owner->getItemOnRow (i)->setSelected (true, false); } else { const bool cmd = modifiers.isCommandDown(); item->setSelected ((! cmd) || (! item->isSelected()), ! cmd); } } bool containsItem (TreeViewItem* const item) const { for (int i = rowComponentItems.size(); --i >= 0;) if ((TreeViewItem*) rowComponentItems.getUnchecked (i) == item) return true; return false; } }; //============================================================================== class TreeViewport : public Viewport { public: TreeViewport() throw() {} ~TreeViewport() throw() {} void updateComponents() { if (getViewedComponent() != 0) ((TreeViewContentComponent*) getViewedComponent())->updateComponents(); repaint(); } void visibleAreaChanged (int, int, int, int) { updateComponents(); } //============================================================================== juce_UseDebuggingNewOperator private: TreeViewport (const TreeViewport&); const TreeViewport& operator= (const TreeViewport&); }; //============================================================================== TreeView::TreeView (const String& componentName) : Component (componentName), rootItem (0), dragInsertPointHighlight (0), dragTargetGroupHighlight (0), indentSize (24), defaultOpenness (false), needsRecalculating (true), rootItemVisible (true), multiSelectEnabled (false), openCloseButtonsVisible (true) { addAndMakeVisible (viewport = new TreeViewport()); viewport->setViewedComponent (new TreeViewContentComponent (this)); viewport->setWantsKeyboardFocus (false); setWantsKeyboardFocus (true); } TreeView::~TreeView() { if (rootItem != 0) rootItem->setOwnerView (0); deleteAllChildren(); } void TreeView::setRootItem (TreeViewItem* const newRootItem) { if (rootItem != newRootItem) { if (newRootItem != 0) { jassert (newRootItem->ownerView == 0); // can't use a tree item in more than one tree at once.. if (newRootItem->ownerView != 0) newRootItem->ownerView->setRootItem (0); } if (rootItem != 0) rootItem->setOwnerView (0); rootItem = newRootItem; if (newRootItem != 0) newRootItem->setOwnerView (this); needsRecalculating = true; handleAsyncUpdate(); if (rootItem != 0 && (defaultOpenness || ! rootItemVisible)) { rootItem->setOpen (false); // force a re-open rootItem->setOpen (true); } } } void TreeView::deleteRootItem() { const ScopedPointer deleter (rootItem); setRootItem (0); } void TreeView::setRootItemVisible (const bool shouldBeVisible) { rootItemVisible = shouldBeVisible; if (rootItem != 0 && (defaultOpenness || ! rootItemVisible)) { rootItem->setOpen (false); // force a re-open rootItem->setOpen (true); } itemsChanged(); } void TreeView::colourChanged() { setOpaque (findColour (backgroundColourId).isOpaque()); repaint(); } void TreeView::setIndentSize (const int newIndentSize) { if (indentSize != newIndentSize) { indentSize = newIndentSize; resized(); } } void TreeView::setDefaultOpenness (const bool isOpenByDefault) { if (defaultOpenness != isOpenByDefault) { defaultOpenness = isOpenByDefault; itemsChanged(); } } void TreeView::setMultiSelectEnabled (const bool canMultiSelect) { multiSelectEnabled = canMultiSelect; } void TreeView::setOpenCloseButtonsVisible (const bool shouldBeVisible) { if (openCloseButtonsVisible != shouldBeVisible) { openCloseButtonsVisible = shouldBeVisible; itemsChanged(); } } //============================================================================== void TreeView::clearSelectedItems() { if (rootItem != 0) rootItem->deselectAllRecursively(); } int TreeView::getNumSelectedItems() const throw() { return (rootItem != 0) ? rootItem->countSelectedItemsRecursively() : 0; } TreeViewItem* TreeView::getSelectedItem (const int index) const throw() { return (rootItem != 0) ? rootItem->getSelectedItemWithIndex (index) : 0; } int TreeView::getNumRowsInTree() const { if (rootItem != 0) return rootItem->getNumRows() - (rootItemVisible ? 0 : 1); return 0; } TreeViewItem* TreeView::getItemOnRow (int index) const { if (! rootItemVisible) ++index; if (rootItem != 0 && index >= 0) return rootItem->getItemOnRow (index); return 0; } TreeViewItem* TreeView::getItemAt (int y) const throw() { TreeViewContentComponent* const tc = (TreeViewContentComponent*) viewport->getViewedComponent(); int x; relativePositionToOtherComponent (tc, x, y); Rectangle pos; return tc->findItemAt (y, pos); } TreeViewItem* TreeView::findItemFromIdentifierString (const String& identifierString) const { if (rootItem == 0) return 0; return rootItem->findItemFromIdentifierString (identifierString); } //============================================================================== XmlElement* TreeView::getOpennessState (const bool alsoIncludeScrollPosition) const { XmlElement* e = 0; if (rootItem != 0) { e = rootItem->getOpennessState(); if (e != 0 && alsoIncludeScrollPosition) e->setAttribute (T("scrollPos"), viewport->getViewPositionY()); } return e; } void TreeView::restoreOpennessState (const XmlElement& newState) { if (rootItem != 0) { rootItem->restoreOpennessState (newState); if (newState.hasAttribute (T("scrollPos"))) viewport->setViewPosition (viewport->getViewPositionX(), newState.getIntAttribute (T("scrollPos"))); } } //============================================================================== void TreeView::paint (Graphics& g) { g.fillAll (findColour (backgroundColourId)); } void TreeView::resized() { viewport->setBounds (0, 0, getWidth(), getHeight()); itemsChanged(); handleAsyncUpdate(); } void TreeView::enablementChanged() { repaint(); } void TreeView::moveSelectedRow (int delta) { if (delta == 0) return; int rowSelected = 0; TreeViewItem* const firstSelected = getSelectedItem (0); if (firstSelected != 0) rowSelected = firstSelected->getRowNumberInTree(); rowSelected = jlimit (0, getNumRowsInTree() - 1, rowSelected + delta); for (;;) { TreeViewItem* item = getItemOnRow (rowSelected); if (item != 0) { if (! item->canBeSelected()) { // if the row we want to highlight doesn't allow it, try skipping // to the next item.. const int nextRowToTry = jlimit (0, getNumRowsInTree() - 1, rowSelected + (delta < 0 ? -1 : 1)); if (rowSelected != nextRowToTry) { rowSelected = nextRowToTry; continue; } else { break; } } item->setSelected (true, true); scrollToKeepItemVisible (item); } break; } } void TreeView::scrollToKeepItemVisible (TreeViewItem* item) { if (item != 0 && item->ownerView == this) { handleAsyncUpdate(); item = item->getDeepestOpenParentItem(); int y = item->y; int viewTop = viewport->getViewPositionY(); if (y < viewTop) { viewport->setViewPosition (viewport->getViewPositionX(), y); } else if (y + item->itemHeight > viewTop + viewport->getViewHeight()) { viewport->setViewPosition (viewport->getViewPositionX(), (y + item->itemHeight) - viewport->getViewHeight()); } } } bool TreeView::keyPressed (const KeyPress& key) { if (key.isKeyCode (KeyPress::upKey)) { moveSelectedRow (-1); } else if (key.isKeyCode (KeyPress::downKey)) { moveSelectedRow (1); } else if (key.isKeyCode (KeyPress::pageDownKey) || key.isKeyCode (KeyPress::pageUpKey)) { if (rootItem != 0) { int rowsOnScreen = getHeight() / jmax (1, rootItem->itemHeight); if (key.isKeyCode (KeyPress::pageUpKey)) rowsOnScreen = -rowsOnScreen; moveSelectedRow (rowsOnScreen); } } else if (key.isKeyCode (KeyPress::homeKey)) { moveSelectedRow (-0x3fffffff); } else if (key.isKeyCode (KeyPress::endKey)) { moveSelectedRow (0x3fffffff); } else if (key.isKeyCode (KeyPress::returnKey)) { TreeViewItem* const firstSelected = getSelectedItem (0); if (firstSelected != 0) firstSelected->setOpen (! firstSelected->isOpen()); } else if (key.isKeyCode (KeyPress::leftKey)) { TreeViewItem* const firstSelected = getSelectedItem (0); if (firstSelected != 0) { if (firstSelected->isOpen()) { firstSelected->setOpen (false); } else { TreeViewItem* parent = firstSelected->parentItem; if ((! rootItemVisible) && parent == rootItem) parent = 0; if (parent != 0) { parent->setSelected (true, true); scrollToKeepItemVisible (parent); } } } } else if (key.isKeyCode (KeyPress::rightKey)) { TreeViewItem* const firstSelected = getSelectedItem (0); if (firstSelected != 0) { if (firstSelected->isOpen() || ! firstSelected->mightContainSubItems()) moveSelectedRow (1); else firstSelected->setOpen (true); } } else { return false; } return true; } void TreeView::itemsChanged() throw() { needsRecalculating = true; repaint(); triggerAsyncUpdate(); } void TreeView::handleAsyncUpdate() { if (needsRecalculating) { needsRecalculating = false; const ScopedLock sl (nodeAlterationLock); if (rootItem != 0) rootItem->updatePositions (rootItemVisible ? 0 : -rootItem->itemHeight); ((TreeViewport*) viewport)->updateComponents(); if (rootItem != 0) { viewport->getViewedComponent() ->setSize (jmax (viewport->getMaximumVisibleWidth(), rootItem->totalWidth), rootItem->totalHeight - (rootItemVisible ? 0 : rootItem->itemHeight)); } else { viewport->getViewedComponent()->setSize (0, 0); } } } //============================================================================== class TreeViewDragInsertPointHighlight : public Component { public: TreeViewDragInsertPointHighlight() : lastItem (0) { setSize (100, 12); setAlwaysOnTop (true); setInterceptsMouseClicks (false, false); } ~TreeViewDragInsertPointHighlight() {} void setTargetPosition (TreeViewItem* const item, int insertIndex, const int x, const int y, const int width) throw() { lastItem = item; lastIndex = insertIndex; const int offset = getHeight() / 2; setBounds (x - offset, y - offset, width - (x - offset), getHeight()); } void paint (Graphics& g) { Path p; const float h = (float) getHeight(); p.addEllipse (2.0f, 2.0f, h - 4.0f, h - 4.0f); p.startNewSubPath (h - 2.0f, h / 2.0f); p.lineTo ((float) getWidth(), h / 2.0f); g.setColour (findColour (TreeView::dragAndDropIndicatorColourId, true)); g.strokePath (p, PathStrokeType (2.0f)); } TreeViewItem* lastItem; int lastIndex; }; class TreeViewDragTargetGroupHighlight : public Component { public: TreeViewDragTargetGroupHighlight() { setAlwaysOnTop (true); setInterceptsMouseClicks (false, false); } ~TreeViewDragTargetGroupHighlight() {} void setTargetPosition (TreeViewItem* const item) throw() { Rectangle r (item->getItemPosition (true)); r.setHeight (item->getItemHeight()); setBounds (r); } void paint (Graphics& g) { g.setColour (findColour (TreeView::dragAndDropIndicatorColourId, true)); g.drawRoundedRectangle (1.0f, 1.0f, getWidth() - 2.0f, getHeight() - 2.0f, 3.0f, 2.0f); } }; //============================================================================== void TreeView::showDragHighlight (TreeViewItem* item, int insertIndex, int x, int y) throw() { beginDragAutoRepeat (1000 / 30); if (dragInsertPointHighlight == 0) { addAndMakeVisible (dragInsertPointHighlight = new TreeViewDragInsertPointHighlight()); addAndMakeVisible (dragTargetGroupHighlight = new TreeViewDragTargetGroupHighlight()); } ((TreeViewDragInsertPointHighlight*) dragInsertPointHighlight) ->setTargetPosition (item, insertIndex, x, y, viewport->getViewWidth()); ((TreeViewDragTargetGroupHighlight*) dragTargetGroupHighlight) ->setTargetPosition (item); } void TreeView::hideDragHighlight() throw() { deleteAndZero (dragInsertPointHighlight); deleteAndZero (dragTargetGroupHighlight); } TreeViewItem* TreeView::getInsertPosition (int& x, int& y, int& insertIndex, const StringArray& files, const String& sourceDescription, Component* sourceComponent) const throw() { insertIndex = 0; TreeViewItem* item = getItemAt (y); if (item == 0) return 0; Rectangle itemPos (item->getItemPosition (true)); insertIndex = item->getIndexInParent(); const int oldY = y; y = itemPos.getY(); if (item->getNumSubItems() == 0 || ! item->isOpen()) { if (files.size() > 0 ? item->isInterestedInFileDrag (files) : item->isInterestedInDragSource (sourceDescription, sourceComponent)) { // Check if we're trying to drag into an empty group item.. if (oldY > itemPos.getY() + itemPos.getHeight() / 4 && oldY < itemPos.getBottom() - itemPos.getHeight() / 4) { insertIndex = 0; x = itemPos.getX() + getIndentSize(); y = itemPos.getBottom(); return item; } } } if (oldY > itemPos.getCentreY()) { y += item->getItemHeight(); while (item->isLastOfSiblings() && item->parentItem != 0 && item->parentItem->parentItem != 0) { if (x > itemPos.getX()) break; item = item->parentItem; itemPos = item->getItemPosition (true); insertIndex = item->getIndexInParent(); } ++insertIndex; } x = itemPos.getX(); return item->parentItem; } void TreeView::handleDrag (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y) { const bool scrolled = viewport->autoScroll (x, y, 20, 10); int insertIndex; TreeViewItem* const item = getInsertPosition (x, y, insertIndex, files, sourceDescription, sourceComponent); if (item != 0) { if (scrolled || dragInsertPointHighlight == 0 || ((TreeViewDragInsertPointHighlight*) dragInsertPointHighlight)->lastItem != item || ((TreeViewDragInsertPointHighlight*) dragInsertPointHighlight)->lastIndex != insertIndex) { if (files.size() > 0 ? item->isInterestedInFileDrag (files) : item->isInterestedInDragSource (sourceDescription, sourceComponent)) showDragHighlight (item, insertIndex, x, y); else hideDragHighlight(); } } else { hideDragHighlight(); } } void TreeView::handleDrop (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y) { hideDragHighlight(); int insertIndex; TreeViewItem* const item = getInsertPosition (x, y, insertIndex, files, sourceDescription, sourceComponent); if (item != 0) { if (files.size() > 0) { if (item->isInterestedInFileDrag (files)) item->filesDropped (files, insertIndex); } else { if (item->isInterestedInDragSource (sourceDescription, sourceComponent)) item->itemDropped (sourceDescription, sourceComponent, insertIndex); } } } //============================================================================== bool TreeView::isInterestedInFileDrag (const StringArray& files) { return true; } void TreeView::fileDragEnter (const StringArray& files, int x, int y) { fileDragMove (files, x, y); } void TreeView::fileDragMove (const StringArray& files, int x, int y) { handleDrag (files, String::empty, 0, x, y); } void TreeView::fileDragExit (const StringArray& files) { hideDragHighlight(); } void TreeView::filesDropped (const StringArray& files, int x, int y) { handleDrop (files, String::empty, 0, x, y); } bool TreeView::isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent) { return true; } void TreeView::itemDragEnter (const String& sourceDescription, Component* sourceComponent, int x, int y) { itemDragMove (sourceDescription, sourceComponent, x, y); } void TreeView::itemDragMove (const String& sourceDescription, Component* sourceComponent, int x, int y) { handleDrag (StringArray(), sourceDescription, sourceComponent, x, y); } void TreeView::itemDragExit (const String& sourceDescription, Component* sourceComponent) { hideDragHighlight(); } void TreeView::itemDropped (const String& sourceDescription, Component* sourceComponent, int x, int y) { handleDrop (StringArray(), sourceDescription, sourceComponent, x, y); } //============================================================================== void TreeViewContentComponent::paint (Graphics& g) { if (owner->rootItem != 0) { owner->handleAsyncUpdate(); if (! owner->rootItemVisible) g.setOrigin (0, -owner->rootItem->itemHeight); owner->rootItem->paintRecursively (g, getWidth()); } } TreeViewItem* TreeViewContentComponent::findItemAt (int y, Rectangle& itemPosition) const { if (owner->rootItem != 0) { owner->handleAsyncUpdate(); if (! owner->rootItemVisible) y += owner->rootItem->itemHeight; TreeViewItem* const ti = owner->rootItem->findItemRecursively (y); if (ti != 0) itemPosition = ti->getItemPosition (false); return ti; } return 0; } //============================================================================== enum TreeViewOpenness { opennessDefault = 0, opennessClosed = 1, opennessOpen = 2 }; TreeViewItem::TreeViewItem() : ownerView (0), parentItem (0), subItems (8), y (0), itemHeight (0), totalHeight (0), selected (false), redrawNeeded (true), drawLinesInside (true), drawsInLeftMargin (false), openness (opennessDefault) { static int nextUID = 0; uid = nextUID++; } TreeViewItem::~TreeViewItem() { } const String TreeViewItem::getUniqueName() const { return String::empty; } void TreeViewItem::itemOpennessChanged (bool) { } int TreeViewItem::getNumSubItems() const throw() { return subItems.size(); } TreeViewItem* TreeViewItem::getSubItem (const int index) const throw() { return subItems [index]; } void TreeViewItem::clearSubItems() { if (subItems.size() > 0) { if (ownerView != 0) { const ScopedLock sl (ownerView->nodeAlterationLock); subItems.clear(); treeHasChanged(); } else { subItems.clear(); } } } void TreeViewItem::addSubItem (TreeViewItem* const newItem, const int insertPosition) { if (newItem != 0) { newItem->parentItem = this; newItem->setOwnerView (ownerView); newItem->y = 0; newItem->itemHeight = newItem->getItemHeight(); newItem->totalHeight = 0; newItem->itemWidth = newItem->getItemWidth(); newItem->totalWidth = 0; if (ownerView != 0) { const ScopedLock sl (ownerView->nodeAlterationLock); subItems.insert (insertPosition, newItem); treeHasChanged(); if (newItem->isOpen()) newItem->itemOpennessChanged (true); } else { subItems.insert (insertPosition, newItem); if (newItem->isOpen()) newItem->itemOpennessChanged (true); } } } void TreeViewItem::removeSubItem (const int index, const bool deleteItem) { if (ownerView != 0) ownerView->nodeAlterationLock.enter(); if (((unsigned int) index) < (unsigned int) subItems.size()) { subItems.remove (index, deleteItem); treeHasChanged(); } if (ownerView != 0) ownerView->nodeAlterationLock.exit(); } bool TreeViewItem::isOpen() const throw() { if (openness == opennessDefault) return ownerView != 0 && ownerView->defaultOpenness; else return openness == opennessOpen; } void TreeViewItem::setOpen (const bool shouldBeOpen) { if (isOpen() != shouldBeOpen) { openness = shouldBeOpen ? opennessOpen : opennessClosed; treeHasChanged(); itemOpennessChanged (isOpen()); } } bool TreeViewItem::isSelected() const throw() { return selected; } void TreeViewItem::deselectAllRecursively() { setSelected (false, false); for (int i = 0; i < subItems.size(); ++i) subItems.getUnchecked(i)->deselectAllRecursively(); } void TreeViewItem::setSelected (const bool shouldBeSelected, const bool deselectOtherItemsFirst) { if (shouldBeSelected && ! canBeSelected()) return; if (deselectOtherItemsFirst) getTopLevelItem()->deselectAllRecursively(); if (shouldBeSelected != selected) { selected = shouldBeSelected; if (ownerView != 0) ownerView->repaint(); itemSelectionChanged (shouldBeSelected); } } void TreeViewItem::paintItem (Graphics&, int, int) { } void TreeViewItem::paintOpenCloseButton (Graphics& g, int width, int height, bool isMouseOver) { ownerView->getLookAndFeel() .drawTreeviewPlusMinusBox (g, 0, 0, width, height, ! isOpen(), isMouseOver); } void TreeViewItem::itemClicked (const MouseEvent&) { } void TreeViewItem::itemDoubleClicked (const MouseEvent&) { if (mightContainSubItems()) setOpen (! isOpen()); } void TreeViewItem::itemSelectionChanged (bool) { } const String TreeViewItem::getTooltip() { return String::empty; } const String TreeViewItem::getDragSourceDescription() { return String::empty; } bool TreeViewItem::isInterestedInFileDrag (const StringArray& files) { return false; } void TreeViewItem::filesDropped (const StringArray& files, int insertIndex) { } bool TreeViewItem::isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent) { return false; } void TreeViewItem::itemDropped (const String& sourceDescription, Component* sourceComponent, int insertIndex) { } const Rectangle TreeViewItem::getItemPosition (const bool relativeToTreeViewTopLeft) const throw() { const int indentX = getIndentX(); int width = itemWidth; if (ownerView != 0 && width < 0) width = ownerView->viewport->getViewWidth() - indentX; Rectangle r (indentX, y, jmax (0, width), totalHeight); if (relativeToTreeViewTopLeft) r.setPosition (r.getX() - ownerView->viewport->getViewPositionX(), r.getY() - ownerView->viewport->getViewPositionY()); return r; } void TreeViewItem::treeHasChanged() const throw() { if (ownerView != 0) ownerView->itemsChanged(); } void TreeViewItem::repaintItem() const { if (ownerView != 0 && areAllParentsOpen()) { const Rectangle r (getItemPosition (true)); ownerView->viewport->repaint (0, r.getY(), r.getRight(), r.getHeight()); } } bool TreeViewItem::areAllParentsOpen() const throw() { return parentItem == 0 || (parentItem->isOpen() && parentItem->areAllParentsOpen()); } void TreeViewItem::updatePositions (int newY) { y = newY; itemHeight = getItemHeight(); totalHeight = itemHeight; itemWidth = getItemWidth(); totalWidth = jmax (itemWidth, 0) + getIndentX(); if (isOpen()) { newY += totalHeight; for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const ti = subItems.getUnchecked(i); ti->updatePositions (newY); newY += ti->totalHeight; totalHeight += ti->totalHeight; totalWidth = jmax (totalWidth, ti->totalWidth); } } } TreeViewItem* TreeViewItem::getDeepestOpenParentItem() throw() { TreeViewItem* result = this; TreeViewItem* item = this; while (item->parentItem != 0) { item = item->parentItem; if (! item->isOpen()) result = item; } return result; } void TreeViewItem::setOwnerView (TreeView* const newOwner) throw() { ownerView = newOwner; for (int i = subItems.size(); --i >= 0;) subItems.getUnchecked(i)->setOwnerView (newOwner); } int TreeViewItem::getIndentX() const throw() { const int indentWidth = ownerView->getIndentSize(); int x = ownerView->rootItemVisible ? indentWidth : 0; if (! ownerView->openCloseButtonsVisible) x -= indentWidth; TreeViewItem* p = parentItem; while (p != 0) { x += indentWidth; p = p->parentItem; } return x; } void TreeViewItem::setDrawsInLeftMargin (bool canDrawInLeftMargin) throw() { drawsInLeftMargin = canDrawInLeftMargin; } void TreeViewItem::paintRecursively (Graphics& g, int width) { jassert (ownerView != 0); if (ownerView == 0) return; const int indent = getIndentX(); const int itemW = itemWidth < 0 ? width - indent : itemWidth; g.setColour (ownerView->findColour (TreeView::linesColourId)); const float halfH = itemHeight * 0.5f; int depth = 0; TreeViewItem* p = parentItem; while (p != 0) { ++depth; p = p->parentItem; } if (! ownerView->rootItemVisible) --depth; const int indentWidth = ownerView->getIndentSize(); if (depth >= 0 && ownerView->openCloseButtonsVisible) { float x = (depth + 0.5f) * indentWidth; if (depth >= 0) { if (parentItem != 0 && parentItem->drawLinesInside) g.drawLine (x, 0, x, isLastOfSiblings() ? halfH : (float) itemHeight); if ((parentItem != 0 && parentItem->drawLinesInside) || (parentItem == 0 && drawLinesInside)) g.drawLine (x, halfH, x + indentWidth / 2, halfH); } p = parentItem; int d = depth; while (p != 0 && --d >= 0) { x -= (float) indentWidth; if ((p->parentItem == 0 || p->parentItem->drawLinesInside) && ! p->isLastOfSiblings()) { g.drawLine (x, 0, x, (float) itemHeight); } p = p->parentItem; } if (mightContainSubItems()) { g.saveState(); g.setOrigin (depth * indentWidth, 0); g.reduceClipRegion (0, 0, indentWidth, itemHeight); paintOpenCloseButton (g, indentWidth, itemHeight, ((TreeViewContentComponent*) ownerView->viewport->getViewedComponent()) ->isMouseOverButton (this)); g.restoreState(); } } { g.saveState(); g.setOrigin (indent, 0); if (g.reduceClipRegion (drawsInLeftMargin ? -indent : 0, 0, drawsInLeftMargin ? itemW + indent : itemW, itemHeight)) paintItem (g, itemW, itemHeight); g.restoreState(); } if (isOpen()) { const Rectangle clip (g.getClipBounds()); for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const ti = subItems.getUnchecked(i); const int relY = ti->y - y; if (relY >= clip.getBottom()) break; if (relY + ti->totalHeight >= clip.getY()) { g.saveState(); g.setOrigin (0, relY); if (g.reduceClipRegion (0, 0, width, ti->totalHeight)) ti->paintRecursively (g, width); g.restoreState(); } } } } bool TreeViewItem::isLastOfSiblings() const throw() { return parentItem == 0 || parentItem->subItems.getLast() == this; } int TreeViewItem::getIndexInParent() const throw() { if (parentItem == 0) return 0; return parentItem->subItems.indexOf (this); } TreeViewItem* TreeViewItem::getTopLevelItem() throw() { return (parentItem == 0) ? this : parentItem->getTopLevelItem(); } int TreeViewItem::getNumRows() const throw() { int num = 1; if (isOpen()) { for (int i = subItems.size(); --i >= 0;) num += subItems.getUnchecked(i)->getNumRows(); } return num; } TreeViewItem* TreeViewItem::getItemOnRow (int index) throw() { if (index == 0) return this; if (index > 0 && isOpen()) { --index; for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const item = subItems.getUnchecked(i); if (index == 0) return item; const int numRows = item->getNumRows(); if (numRows > index) return item->getItemOnRow (index); index -= numRows; } } return 0; } TreeViewItem* TreeViewItem::findItemRecursively (int targetY) throw() { if (((unsigned int) targetY) < (unsigned int) totalHeight) { const int h = itemHeight; if (targetY < h) return this; if (isOpen()) { targetY -= h; for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const ti = subItems.getUnchecked(i); if (targetY < ti->totalHeight) return ti->findItemRecursively (targetY); targetY -= ti->totalHeight; } } } return 0; } int TreeViewItem::countSelectedItemsRecursively() const throw() { int total = 0; if (isSelected()) ++total; for (int i = subItems.size(); --i >= 0;) total += subItems.getUnchecked(i)->countSelectedItemsRecursively(); return total; } TreeViewItem* TreeViewItem::getSelectedItemWithIndex (int index) throw() { if (isSelected()) { if (index == 0) return this; --index; } if (index >= 0) { for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const item = subItems.getUnchecked(i); TreeViewItem* const found = item->getSelectedItemWithIndex (index); if (found != 0) return found; index -= item->countSelectedItemsRecursively(); } } return 0; } int TreeViewItem::getRowNumberInTree() const throw() { if (parentItem != 0 && ownerView != 0) { int n = 1 + parentItem->getRowNumberInTree(); int ourIndex = parentItem->subItems.indexOf (this); jassert (ourIndex >= 0); while (--ourIndex >= 0) n += parentItem->subItems [ourIndex]->getNumRows(); if (parentItem->parentItem == 0 && ! ownerView->rootItemVisible) --n; return n; } else { return 0; } } void TreeViewItem::setLinesDrawnForSubItems (const bool drawLines) throw() { drawLinesInside = drawLines; } TreeViewItem* TreeViewItem::getNextVisibleItem (const bool recurse) const throw() { if (recurse && isOpen() && subItems.size() > 0) return subItems [0]; if (parentItem != 0) { const int nextIndex = parentItem->subItems.indexOf (this) + 1; if (nextIndex >= parentItem->subItems.size()) return parentItem->getNextVisibleItem (false); return parentItem->subItems [nextIndex]; } return 0; } const String TreeViewItem::getItemIdentifierString() const { String s; if (parentItem != 0) s = parentItem->getItemIdentifierString(); return s + T("/") + getUniqueName().replaceCharacter (T('/'), T('\\')); } TreeViewItem* TreeViewItem::findItemFromIdentifierString (const String& identifierString) { const String uid (getUniqueName()); if (uid == identifierString) return this; if (identifierString.startsWith (uid + T("/"))) { const String remainingPath (identifierString.substring (uid.length() + 1)); bool wasOpen = isOpen(); setOpen (true); for (int i = subItems.size(); --i >= 0;) { TreeViewItem* item = subItems.getUnchecked(i)->findItemFromIdentifierString (remainingPath); if (item != 0) return item; } setOpen (wasOpen); } return 0; } void TreeViewItem::restoreOpennessState (const XmlElement& e) throw() { if (e.hasTagName (T("CLOSED"))) { setOpen (false); } else if (e.hasTagName (T("OPEN"))) { setOpen (true); forEachXmlChildElement (e, n) { const String id (n->getStringAttribute (T("id"))); for (int i = 0; i < subItems.size(); ++i) { TreeViewItem* const ti = subItems.getUnchecked(i); if (ti->getUniqueName() == id) { ti->restoreOpennessState (*n); break; } } } } } XmlElement* TreeViewItem::getOpennessState() const throw() { const String name (getUniqueName()); if (name.isNotEmpty()) { XmlElement* e; if (isOpen()) { e = new XmlElement (T("OPEN")); for (int i = 0; i < subItems.size(); ++i) e->addChildElement (subItems.getUnchecked(i)->getOpennessState()); } else { e = new XmlElement (T("CLOSED")); } e->setAttribute (T("id"), name); return e; } else { // trying to save the openness for an element that has no name - this won't // work because it needs the names to identify what to open. jassertfalse } return 0; } END_JUCE_NAMESPACE