From be5ecce1fe8a93c070f13056953171bba558c096 Mon Sep 17 00:00:00 2001 From: Julian Storer Date: Wed, 9 Dec 2009 13:08:38 +0000 Subject: [PATCH] Added drag-and-drop target support to TreeViews. The TreeViewItem class now has methods that can be overridden to accept external and internal drags, and the TreeView provides some nice graphics to indicate the target insertion point. Also added a handy method Viewport::autoScroll, and options for positioning the dragged image supplied to DragAndDropContainer::startDrag --- juce_amalgamated.cpp | 424 +++++++++++++++--- juce_amalgamated.h | 312 +++++++++---- src/gui/components/controls/juce_TreeView.cpp | 332 ++++++++++++-- src/gui/components/controls/juce_TreeView.h | 131 +++++- src/gui/components/layout/juce_Viewport.cpp | 38 ++ src/gui/components/layout/juce_Viewport.h | 17 + .../lookandfeel/juce_LookAndFeel.cpp | 1 + .../mouse/juce_DragAndDropContainer.cpp | 55 ++- .../mouse/juce_DragAndDropContainer.h | 7 +- 9 files changed, 1117 insertions(+), 200 deletions(-) diff --git a/juce_amalgamated.cpp b/juce_amalgamated.cpp index 3670537dec..8e5fac562f 100644 --- a/juce_amalgamated.cpp +++ b/juce_amalgamated.cpp @@ -54764,7 +54764,8 @@ public: Image* dragImage = Component::createComponentSnapshot (pos, true); dragImage->multiplyAllAlphas (0.6f); - dragContainer->startDragging (dragDescription, owner, dragImage, true); + Point imageOffset ((float) (pos.getX() - e.x), (float) (pos.getY() - e.y)); + dragContainer->startDragging (dragDescription, owner, dragImage, true, &imageOffset); } else { @@ -55015,6 +55016,8 @@ private: TreeView::TreeView (const String& componentName) : Component (componentName), rootItem (0), + dragInsertPointHighlight (0), + dragTargetGroupHighlight (0), indentSize (24), defaultOpenness (false), needsRecalculating (true), @@ -55160,13 +55163,22 @@ TreeViewItem* TreeView::getItemOnRow (int index) const 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); +} + XmlElement* TreeView::getOpennessState (const bool alsoIncludeScrollPosition) const { XmlElement* e = 0; if (rootItem != 0) { - e = rootItem->createXmlOpenness(); + e = rootItem->getOpennessState(); if (e != 0 && alsoIncludeScrollPosition) e->setAttribute (T("scrollPos"), viewport->getViewPositionY()); @@ -55179,7 +55191,7 @@ void TreeView::restoreOpennessState (const XmlElement& newState) { if (rootItem != 0) { - rootItem->restoreFromXml (newState); + rootItem->restoreOpennessState (newState); if (newState.hasAttribute (T("scrollPos"))) viewport->setViewPosition (viewport->getViewPositionX(), @@ -55388,6 +55400,243 @@ void TreeView::handleAsyncUpdate() } } +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 || item->parentItem == 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) @@ -55615,6 +55864,24 @@ 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(); @@ -55840,6 +56107,14 @@ bool TreeViewItem::isLastOfSiblings() const throw() || 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 @@ -55904,7 +56179,7 @@ TreeViewItem* TreeViewItem::findItemRecursively (int y) throw() { TreeViewItem* const ti = subItems.getUnchecked(i); - if (ti->totalHeight >= y) + if (y < ti->totalHeight) return ti->findItemRecursively (y); y -= ti->totalHeight; @@ -56003,7 +56278,7 @@ TreeViewItem* TreeViewItem::getNextVisibleItem (const bool recurse) const throw( return 0; } -void TreeViewItem::restoreFromXml (const XmlElement& e) +void TreeViewItem::restoreOpennessState (const XmlElement& e) throw() { if (e.hasTagName (T("CLOSED"))) { @@ -56023,7 +56298,7 @@ void TreeViewItem::restoreFromXml (const XmlElement& e) if (ti->getUniqueName() == id) { - ti->restoreFromXml (*n); + ti->restoreOpennessState (*n); break; } } @@ -56031,38 +56306,35 @@ void TreeViewItem::restoreFromXml (const XmlElement& e) } } -XmlElement* TreeViewItem::createXmlOpenness() const +XmlElement* TreeViewItem::getOpennessState() const throw() { - if (openness != opennessDefault) + const String name (getUniqueName()); + + if (name.isNotEmpty()) { - const String name (getUniqueName()); + XmlElement* e; - if (name.isNotEmpty()) + if (isOpen()) { - XmlElement* e; + e = new XmlElement (T("OPEN")); - if (isOpen()) - { - e = new XmlElement (T("OPEN")); - - for (int i = 0; i < subItems.size(); ++i) - e->addChildElement (subItems.getUnchecked(i)->createXmlOpenness()); - } - else - { - e = new XmlElement (T("CLOSED")); - } - - e->setAttribute (T("id"), name); - - return e; + for (int i = 0; i < subItems.size(); ++i) + e->addChildElement (subItems.getUnchecked(i)->getOpennessState()); } 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 + 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; @@ -63034,6 +63306,44 @@ void Viewport::setViewPositionProportionately (const double x, jmax (0, roundDoubleToInt (y * (contentComp->getHeight() - getHeight())))); } +bool Viewport::autoScroll (int mouseX, int mouseY, int activeBorderThickness, int maximumSpeed) +{ + if (contentComp != 0) + { + int dx = 0, dy = 0; + + if (mouseX < activeBorderThickness) + dx = activeBorderThickness - mouseX; + else if (mouseX >= contentHolder->getWidth() - activeBorderThickness) + dx = (contentHolder->getWidth() - activeBorderThickness) - mouseX; + + if (dx < 0) + dx = jmax (dx, -maximumSpeed, contentHolder->getWidth() - contentComp->getRight()); + else + dx = jmin (dx, maximumSpeed, -contentComp->getX()); + + if (mouseY < activeBorderThickness) + dy = activeBorderThickness - mouseY; + else if (mouseY >= contentHolder->getHeight() - activeBorderThickness) + dy = (contentHolder->getHeight() - activeBorderThickness) - mouseY; + + if (dy < 0) + dy = jmax (dy, -maximumSpeed, contentHolder->getHeight() - contentComp->getBottom()); + else + dy = jmin (dy, maximumSpeed, -contentComp->getY()); + + if (dx != 0 || dy != 0) + { + contentComp->setTopLeftPosition (contentComp->getX() + dx, + contentComp->getY() + dy); + + return true; + } + } + + return false; +} + void Viewport::componentMovedOrResized (Component&, bool, bool) { updateVisibleRegion(); @@ -63297,6 +63607,7 @@ LookAndFeel::LookAndFeel() TreeView::linesColourId, 0x4c000000, TreeView::backgroundColourId, 0x00000000, + TreeView::dragAndDropIndicatorColourId, 0x80ff0000, PopupMenu::backgroundColourId, 0xffffffff, PopupMenu::textColourId, 0xff000000, @@ -69156,7 +69467,7 @@ private: ComponentDeletionWatcher* currentlyOverWatcher; String dragDesc; - int xOff, yOff; + const int imageX, imageY; bool hasCheckedForExternalDrag, drawImage; DragImageComponent (const DragImageComponent&); @@ -69166,13 +69477,16 @@ public: DragImageComponent (Image* const im, const String& desc, Component* const s, - DragAndDropContainer* const o) + DragAndDropContainer* const o, + const int imageX_, const int imageY_) : image (im), source (s), owner (o), currentlyOver (0), currentlyOverWatcher (0), dragDesc (desc), + imageX (imageX_), + imageY (imageY_), hasCheckedForExternalDrag (false), drawImage (true) { @@ -69188,13 +69502,6 @@ public: mouseDragSourceWatcher = new ComponentDeletionWatcher (mouseDragSource); mouseDragSource->addMouseListener (this, false); - int mx, my; - Desktop::getLastMouseDownPosition (mx, my); - source->globalPositionToRelative (mx, my); - - xOff = jlimit (0, im->getWidth(), mx); - yOff = jlimit (0, im->getHeight(), my); - startTimer (200); setInterceptsMouseClicks (false, false); @@ -69341,13 +69648,13 @@ public: // a modal loop and deletes this object before it returns) const String dragDescLocal (dragDesc); - int newX = x - xOff; - int newY = y - yOff; + int newX = x + imageX; + int newY = y + imageY; if (getParentComponent() != 0) getParentComponent()->globalPositionToRelative (newX, newY); - if (newX != getX() || newY != getY()) + //if (newX != getX() || newY != getY()) { setTopLeftPosition (newX, newY); @@ -69450,19 +69757,18 @@ DragAndDropContainer::DragAndDropContainer() DragAndDropContainer::~DragAndDropContainer() { - if (dragImageComponent != 0) - delete dragImageComponent; + delete dragImageComponent; } void DragAndDropContainer::startDragging (const String& sourceDescription, Component* sourceComponent, Image* im, - const bool allowDraggingToExternalWindows) + const bool allowDraggingToExternalWindows, + const Point* imageOffsetFromMouse) { if (dragImageComponent != 0) { - if (im != 0) - delete im; + delete im; } else { @@ -69472,6 +69778,7 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, { int mx, my; Desktop::getLastMouseDownPosition (mx, my); + int imageX = 0, imageY = 0; if (im == 0) { @@ -69516,13 +69823,27 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, } } } + + imageX = -cx; + imageY = -cy; + } + else + { + if (imageOffsetFromMouse == 0) + { + imageX = im->getWidth() / -2; + imageY = im->getHeight() / -2; + } + else + { + imageX = (int) imageOffsetFromMouse->getX(); + imageY = (int) imageOffsetFromMouse->getY(); + } } DragImageComponent* const dic - = new DragImageComponent (im, - sourceDescription, - sourceComponent, - this); + = new DragImageComponent (im, sourceDescription, sourceComponent, + this, imageX, imageY); dragImageComponent = dic; currentDragDesc = sourceDescription; @@ -69548,8 +69869,7 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, // is also a Component. jassertfalse - if (im != 0) - delete im; + delete im; } } } diff --git a/juce_amalgamated.h b/juce_amalgamated.h index 2f56d4a3db..34d881fc62 100644 --- a/juce_amalgamated.h +++ b/juce_amalgamated.h @@ -31567,6 +31567,23 @@ public: void setViewPositionProportionately (const double proportionX, const double proportionY); + /** If the specified position is at the edges of the viewport, this method scrolls + the viewport to bring that position nearer to the centre. + + Call this if you're dragging an object inside a viewport and want to make it scroll + when the user approaches an edge. You might also find Component::beginDragAutoRepeat() + useful when auto-scrolling. + + @param mouseX the x position, relative to the Viewport's top-left + @param mouseY the y position, relative to the Viewport's top-left + @param distanceFromEdge specifies how close to an edge the position needs to be + before the viewport should scroll in that direction + @param maximumSpeed the maximum number of pixels that the viewport is allowed + to scroll by. + @returns true if the viewport was scrolled + */ + bool autoScroll (int mouseX, int mouseY, int distanceFromEdge, int maximumSpeed); + /** Returns the position within the child component of the top-left of its visible area. @see getViewWidth, setViewPosition */ @@ -42491,11 +42508,16 @@ public: @param allowDraggingToOtherJuceWindows if true, the dragged component will appear as a desktop window, and can be dragged to DragAndDropTargets that are the children of components other than this one. + @param imageOffsetFromMouse if an image has been passed-in, this specifies the offset + at which the image should be drawn from the mouse. If it isn't + specified, then the image will be centred around the mouse. If + an image hasn't been passed-in, this will be ignored. */ void startDragging (const String& sourceDescription, Component* sourceComponent, Image* dragImage = 0, - const bool allowDraggingToOtherJuceWindows = false); + const bool allowDraggingToOtherJuceWindows = false, + const Point* imageOffsetFromMouse = 0); /** Returns true if something is currently being dragged. */ bool isDragAndDropActive() const; @@ -43549,6 +43571,88 @@ private: #ifndef __JUCE_TREEVIEW_JUCEHEADER__ #define __JUCE_TREEVIEW_JUCEHEADER__ +/********* Start of inlined file: juce_FileDragAndDropTarget.h *********/ +#ifndef __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ +#define __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ + +/** + Components derived from this class can have files dropped onto them by an external application. + + @see DragAndDropContainer +*/ +class JUCE_API FileDragAndDropTarget +{ +public: + /** Destructor. */ + virtual ~FileDragAndDropTarget() {} + + /** Callback to check whether this target is interested in the set of files being offered. + + Note that this will be called repeatedly when the user is dragging the mouse around over your + component, so don't do anything time-consuming in here, like opening the files to have a look + inside them! + + @param files the set of (absolute) pathnames of the files that the user is dragging + @returns true if this component wants to receive the other callbacks regarging this + type of object; if it returns false, no other callbacks will be made. + */ + virtual bool isInterestedInFileDrag (const StringArray& files) = 0; + + /** Callback to indicate that some files are being dragged over this component. + + This gets called when the user moves the mouse into this component while dragging. + + Use this callback as a trigger to make your component repaint itself to give the + user feedback about whether the files can be dropped here or not. + + @param files the set of (absolute) pathnames of the files that the user is dragging + @param x the mouse x position, relative to this component + @param y the mouse y position, relative to this component + */ + virtual void fileDragEnter (const StringArray& files, int x, int y); + + /** Callback to indicate that the user is dragging some files over this component. + + This gets called when the user moves the mouse over this component while dragging. + Normally overriding itemDragEnter() and itemDragExit() are enough, but + this lets you know what happens in-between. + + @param files the set of (absolute) pathnames of the files that the user is dragging + @param x the mouse x position, relative to this component + @param y the mouse y position, relative to this component + */ + virtual void fileDragMove (const StringArray& files, int x, int y); + + /** Callback to indicate that the mouse has moved away from this component. + + This gets called when the user moves the mouse out of this component while dragging + the files. + + If you've used fileDragEnter() to repaint your component and give feedback, use this + as a signal to repaint it in its normal state. + + @param files the set of (absolute) pathnames of the files that the user is dragging + */ + virtual void fileDragExit (const StringArray& files); + + /** Callback to indicate that the user has dropped the files onto this component. + + When the user drops the files, this get called, and you can use the files in whatever + way is appropriate. + + Note that after this is called, the fileDragExit method may not be called, so you should + clean up in here if there's anything you need to do when the drag finishes. + + @param files the set of (absolute) pathnames of the files that the user is dragging + @param x the mouse x position, relative to this component + @param y the mouse y position, relative to this component + */ + virtual void filesDropped (const StringArray& files, int x, int y) = 0; +}; + +#endif // __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ +/********* End of inlined file: juce_FileDragAndDropTarget.h *********/ + class TreeView; /** @@ -43860,10 +43964,52 @@ public: If you need more complex drag-and-drop behaviour, you can use custom components for the items, and use those to trigger the drag. + To accept drag-and-drop in your tree, see isInterestedInDragSource(), + isInterestedInFileDrag(), etc. + @see DragAndDropContainer::startDragging */ virtual const String getDragSourceDescription(); + /** If you want your item to be able to have files drag-and-dropped onto it, implement this + method and return true. + + If you return true and allow some files to be dropped, you'll also need to implement the + filesDropped() method to do something with them. + + Note that this will be called often, so make your implementation very quick! There's + certainly no time to try opening the files and having a think about what's inside them! + + For responding to internal drag-and-drop of other types of object, see isInterestedInDragSource(). + @see FileDragAndDropTarget::isInterestedInFileDrag, isInterestedInDragSource + */ + virtual bool isInterestedInFileDrag (const StringArray& files); + + /** When files are dropped into this item, this callback is invoked. + + For this to work, you'll need to have also implemented isInterestedInFileDrag(). + The insertIndex value indicates where in the list of sub-items the files were dropped. + @see FileDragAndDropTarget::filesDropped, isInterestedInFileDrag + */ + virtual void filesDropped (const StringArray& files, int insertIndex); + + /** If you want your item to act as a DragAndDropTarget, implement this method and return true. + + If you implement this method, you'll also need to implement itemDropped() in order to handle + the items when they are dropped. + To respond to drag-and-drop of files from external applications, see isInterestedInFileDrag(). + @see DragAndDropTarget::isInterestedInDragSource, itemDropped + */ + virtual bool isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent); + + /** When a things are dropped into this item, this callback is invoked. + + For this to work, you need to have also implemented isInterestedInDragSource(). + The insertIndex value indicates where in the list of sub-items the new items should be placed. + @see isInterestedInDragSource, DragAndDropTarget::itemDropped + */ + virtual void itemDropped (const String& sourceDescription, Component* sourceComponent, int insertIndex); + /** Sets a flag to indicate that the item wants to be allowed to draw all the way across to the left edge of the treeview. @@ -43879,6 +44025,42 @@ public: */ void setDrawsInLeftMargin (bool canDrawInLeftMargin) throw(); + /** Saves the current state of open/closed nodes so it can be restored later. + + This takes a snapshot of which sub-nodes have been explicitly opened or closed, + and records it as XML. To identify node objects it uses the + TreeViewItem::getUniqueName() method to create named paths. This + means that the same state of open/closed nodes can be restored to a + completely different instance of the tree, as long as it contains nodes + whose unique names are the same. + + You'd normally want to use TreeView::getOpennessState() rather than call it + for a specific item, but this can be handy if you need to briefly save the state + for a section of the tree. + + The caller is responsible for deleting the object that is returned. + @see TreeView::getOpennessState, restoreOpennessState + */ + XmlElement* getOpennessState() const throw(); + + /** Restores the openness of this item and all its sub-items from a saved state. + + See TreeView::restoreOpennessState for more details. + + You'd normally want to use TreeView::restoreOpennessState() rather than call it + for a specific item, but this can be handy if you need to briefly save the state + for a section of the tree. + + @see TreeView::restoreOpennessState, getOpennessState + */ + void restoreOpennessState (const XmlElement& xml) throw(); + + /** Returns the index of this item in its parent's sub-items. */ + int getIndexInParent() const throw(); + + /** Returns true if this item is the last of its parent's sub-itens. */ + bool isLastOfSiblings() const throw(); + juce_UseDebuggingNewOperator private: @@ -43900,12 +44082,9 @@ private: int getIndentX() const throw(); void setOwnerView (TreeView* const newOwner) throw(); void paintRecursively (Graphics& g, int width); + TreeViewItem* getTopLevelItem() throw(); TreeViewItem* findItemRecursively (int y) throw(); TreeViewItem* getDeepestOpenParentItem() throw(); - void restoreFromXml (const XmlElement& e); - XmlElement* createXmlOpenness() const; - bool isLastOfSiblings() const throw(); - TreeViewItem* getTopLevelItem() throw(); int getNumRows() const throw(); TreeViewItem* getItemOnRow (int index) throw(); void deselectAllRecursively(); @@ -43925,6 +44104,8 @@ private: */ class JUCE_API TreeView : public Component, public SettableTooltipClient, + public FileDragAndDropTarget, + public DragAndDropTarget, private AsyncUpdater { public: @@ -44057,6 +44238,11 @@ public: */ TreeViewItem* getItemOnRow (int index) const; + /** Returns the item that contains a given y position. + The y is relative to the top of the TreeView component. + */ + TreeViewItem* getItemAt (int yPosition) const throw(); + /** Tries to scroll the tree so that this item is on-screen somewhere. */ void scrollToKeepItemVisible (TreeViewItem* item); @@ -44111,8 +44297,9 @@ public: */ enum ColourIds { - backgroundColourId = 0x1000500, /**< A background colour to fill the component with. */ - linesColourId = 0x1000501 /**< The colour to draw the lines with.*/ + backgroundColourId = 0x1000500, /**< A background colour to fill the component with. */ + linesColourId = 0x1000501, /**< The colour to draw the lines with.*/ + dragAndDropIndicatorColourId = 0x1000502 /**< The colour to use for the drag-and-drop target position indicator. */ }; /** @internal */ @@ -44125,6 +44312,26 @@ public: void colourChanged(); /** @internal */ void enablementChanged(); + /** @internal */ + bool isInterestedInFileDrag (const StringArray& files); + /** @internal */ + void fileDragEnter (const StringArray& files, int x, int y); + /** @internal */ + void fileDragMove (const StringArray& files, int x, int y); + /** @internal */ + void fileDragExit (const StringArray& files); + /** @internal */ + void filesDropped (const StringArray& files, int x, int y); + /** @internal */ + bool isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent); + /** @internal */ + void itemDragEnter (const String& sourceDescription, Component* sourceComponent, int x, int y); + /** @internal */ + void itemDragMove (const String& sourceDescription, Component* sourceComponent, int x, int y); + /** @internal */ + void itemDragExit (const String& sourceDescription, Component* sourceComponent); + /** @internal */ + void itemDropped (const String& sourceDescription, Component* sourceComponent, int x, int y); juce_UseDebuggingNewOperator @@ -44134,6 +44341,8 @@ private: Viewport* viewport; CriticalSection nodeAlterationLock; TreeViewItem* rootItem; + Component* dragInsertPointHighlight; + Component* dragTargetGroupHighlight; int indentSize; bool defaultOpenness : 1; bool needsRecalculating : 1; @@ -44145,6 +44354,13 @@ private: void handleAsyncUpdate(); void moveSelectedRow (int delta); void updateButtonUnderMouse (const MouseEvent& e); + void showDragHighlight (TreeViewItem* item, int insertIndex, int x, int y) throw(); + void hideDragHighlight() throw(); + void handleDrag (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y); + void handleDrop (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y); + TreeViewItem* getInsertPosition (int& x, int& y, int& insertIndex, + const StringArray& files, const String& sourceDescription, + Component* sourceComponent) const throw(); TreeView (const TreeView&); const TreeView& operator= (const TreeView&); @@ -45490,88 +45706,6 @@ private: #endif #ifndef __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ -/********* Start of inlined file: juce_FileDragAndDropTarget.h *********/ -#ifndef __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ -#define __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ - -/** - Components derived from this class can have files dropped onto them by an external application. - - @see DragAndDropContainer -*/ -class JUCE_API FileDragAndDropTarget -{ -public: - /** Destructor. */ - virtual ~FileDragAndDropTarget() {} - - /** Callback to check whether this target is interested in the set of files being offered. - - Note that this will be called repeatedly when the user is dragging the mouse around over your - component, so don't do anything time-consuming in here, like opening the files to have a look - inside them! - - @param files the set of (absolute) pathnames of the files that the user is dragging - @returns true if this component wants to receive the other callbacks regarging this - type of object; if it returns false, no other callbacks will be made. - */ - virtual bool isInterestedInFileDrag (const StringArray& files) = 0; - - /** Callback to indicate that some files are being dragged over this component. - - This gets called when the user moves the mouse into this component while dragging. - - Use this callback as a trigger to make your component repaint itself to give the - user feedback about whether the files can be dropped here or not. - - @param files the set of (absolute) pathnames of the files that the user is dragging - @param x the mouse x position, relative to this component - @param y the mouse y position, relative to this component - */ - virtual void fileDragEnter (const StringArray& files, int x, int y); - - /** Callback to indicate that the user is dragging some files over this component. - - This gets called when the user moves the mouse over this component while dragging. - Normally overriding itemDragEnter() and itemDragExit() are enough, but - this lets you know what happens in-between. - - @param files the set of (absolute) pathnames of the files that the user is dragging - @param x the mouse x position, relative to this component - @param y the mouse y position, relative to this component - */ - virtual void fileDragMove (const StringArray& files, int x, int y); - - /** Callback to indicate that the mouse has moved away from this component. - - This gets called when the user moves the mouse out of this component while dragging - the files. - - If you've used fileDragEnter() to repaint your component and give feedback, use this - as a signal to repaint it in its normal state. - - @param files the set of (absolute) pathnames of the files that the user is dragging - */ - virtual void fileDragExit (const StringArray& files); - - /** Callback to indicate that the user has dropped the files onto this component. - - When the user drops the files, this get called, and you can use the files in whatever - way is appropriate. - - Note that after this is called, the fileDragExit method may not be called, so you should - clean up in here if there's anything you need to do when the drag finishes. - - @param files the set of (absolute) pathnames of the files that the user is dragging - @param x the mouse x position, relative to this component - @param y the mouse y position, relative to this component - */ - virtual void filesDropped (const StringArray& files, int x, int y) = 0; -}; - -#endif // __JUCE_FILEDRAGANDDROPTARGET_JUCEHEADER__ -/********* End of inlined file: juce_FileDragAndDropTarget.h *********/ - #endif #ifndef __JUCE_LASSOCOMPONENT_JUCEHEADER__ diff --git a/src/gui/components/controls/juce_TreeView.cpp b/src/gui/components/controls/juce_TreeView.cpp index f2f215dc24..59322f0584 100644 --- a/src/gui/components/controls/juce_TreeView.cpp +++ b/src/gui/components/controls/juce_TreeView.cpp @@ -147,7 +147,8 @@ public: Image* dragImage = Component::createComponentSnapshot (pos, true); dragImage->multiplyAllAlphas (0.6f); - dragContainer->startDragging (dragDescription, owner, dragImage, true); + Point imageOffset ((float) (pos.getX() - e.x), (float) (pos.getY() - e.y)); + dragContainer->startDragging (dragDescription, owner, dragImage, true, &imageOffset); } else { @@ -403,6 +404,8 @@ private: TreeView::TreeView (const String& componentName) : Component (componentName), rootItem (0), + dragInsertPointHighlight (0), + dragTargetGroupHighlight (0), indentSize (24), defaultOpenness (false), needsRecalculating (true), @@ -550,6 +553,15 @@ TreeViewItem* TreeView::getItemOnRow (int index) const 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); +} + //============================================================================== XmlElement* TreeView::getOpennessState (const bool alsoIncludeScrollPosition) const { @@ -557,7 +569,7 @@ XmlElement* TreeView::getOpennessState (const bool alsoIncludeScrollPosition) co if (rootItem != 0) { - e = rootItem->createXmlOpenness(); + e = rootItem->getOpennessState(); if (e != 0 && alsoIncludeScrollPosition) e->setAttribute (T("scrollPos"), viewport->getViewPositionY()); @@ -570,7 +582,7 @@ void TreeView::restoreOpennessState (const XmlElement& newState) { if (rootItem != 0) { - rootItem->restoreFromXml (newState); + rootItem->restoreOpennessState (newState); if (newState.hasAttribute (T("scrollPos"))) viewport->setViewPosition (viewport->getViewPositionX(), @@ -780,6 +792,245 @@ void TreeView::handleAsyncUpdate() } } +//============================================================================== +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 || item->parentItem == 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) @@ -1010,6 +1261,24 @@ 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(); @@ -1236,6 +1505,14 @@ bool TreeViewItem::isLastOfSiblings() const throw() || 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 @@ -1300,7 +1577,7 @@ TreeViewItem* TreeViewItem::findItemRecursively (int y) throw() { TreeViewItem* const ti = subItems.getUnchecked(i); - if (ti->totalHeight >= y) + if (y < ti->totalHeight) return ti->findItemRecursively (y); y -= ti->totalHeight; @@ -1399,7 +1676,7 @@ TreeViewItem* TreeViewItem::getNextVisibleItem (const bool recurse) const throw( return 0; } -void TreeViewItem::restoreFromXml (const XmlElement& e) +void TreeViewItem::restoreOpennessState (const XmlElement& e) throw() { if (e.hasTagName (T("CLOSED"))) { @@ -1419,7 +1696,7 @@ void TreeViewItem::restoreFromXml (const XmlElement& e) if (ti->getUniqueName() == id) { - ti->restoreFromXml (*n); + ti->restoreOpennessState (*n); break; } } @@ -1427,38 +1704,35 @@ void TreeViewItem::restoreFromXml (const XmlElement& e) } } -XmlElement* TreeViewItem::createXmlOpenness() const +XmlElement* TreeViewItem::getOpennessState() const throw() { - if (openness != opennessDefault) + const String name (getUniqueName()); + + if (name.isNotEmpty()) { - const String name (getUniqueName()); + XmlElement* e; - if (name.isNotEmpty()) + if (isOpen()) { - XmlElement* e; + e = new XmlElement (T("OPEN")); - if (isOpen()) - { - e = new XmlElement (T("OPEN")); - - for (int i = 0; i < subItems.size(); ++i) - e->addChildElement (subItems.getUnchecked(i)->createXmlOpenness()); - } - else - { - e = new XmlElement (T("CLOSED")); - } - - e->setAttribute (T("id"), name); - - return e; + for (int i = 0; i < subItems.size(); ++i) + e->addChildElement (subItems.getUnchecked(i)->getOpennessState()); } 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 + 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; diff --git a/src/gui/components/controls/juce_TreeView.h b/src/gui/components/controls/juce_TreeView.h index 01ec12579c..03fa4eb6f5 100644 --- a/src/gui/components/controls/juce_TreeView.h +++ b/src/gui/components/controls/juce_TreeView.h @@ -29,8 +29,11 @@ #include "../layout/juce_Viewport.h" #include "../../../text/juce_XmlElement.h" #include "../../../events/juce_AsyncUpdater.h" +#include "../mouse/juce_FileDragAndDropTarget.h" +#include "../mouse/juce_DragAndDropTarget.h" class TreeView; + //============================================================================== /** An item in a treeview. @@ -336,6 +339,7 @@ public: */ virtual const String getTooltip(); + //============================================================================== /** To allow items from your treeview to be dragged-and-dropped, implement this method. If this returns a non-empty name then when the user drags an item, the treeview will @@ -346,10 +350,53 @@ public: If you need more complex drag-and-drop behaviour, you can use custom components for the items, and use those to trigger the drag. + To accept drag-and-drop in your tree, see isInterestedInDragSource(), + isInterestedInFileDrag(), etc. + @see DragAndDropContainer::startDragging */ virtual const String getDragSourceDescription(); + /** If you want your item to be able to have files drag-and-dropped onto it, implement this + method and return true. + + If you return true and allow some files to be dropped, you'll also need to implement the + filesDropped() method to do something with them. + + Note that this will be called often, so make your implementation very quick! There's + certainly no time to try opening the files and having a think about what's inside them! + + For responding to internal drag-and-drop of other types of object, see isInterestedInDragSource(). + @see FileDragAndDropTarget::isInterestedInFileDrag, isInterestedInDragSource + */ + virtual bool isInterestedInFileDrag (const StringArray& files); + + /** When files are dropped into this item, this callback is invoked. + + For this to work, you'll need to have also implemented isInterestedInFileDrag(). + The insertIndex value indicates where in the list of sub-items the files were dropped. + @see FileDragAndDropTarget::filesDropped, isInterestedInFileDrag + */ + virtual void filesDropped (const StringArray& files, int insertIndex); + + /** If you want your item to act as a DragAndDropTarget, implement this method and return true. + + If you implement this method, you'll also need to implement itemDropped() in order to handle + the items when they are dropped. + To respond to drag-and-drop of files from external applications, see isInterestedInFileDrag(). + @see DragAndDropTarget::isInterestedInDragSource, itemDropped + */ + virtual bool isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent); + + /** When a things are dropped into this item, this callback is invoked. + + For this to work, you need to have also implemented isInterestedInDragSource(). + The insertIndex value indicates where in the list of sub-items the new items should be placed. + @see isInterestedInDragSource, DragAndDropTarget::itemDropped + */ + virtual void itemDropped (const String& sourceDescription, Component* sourceComponent, int insertIndex); + + //============================================================================== /** Sets a flag to indicate that the item wants to be allowed to draw all the way across to the left edge of the treeview. @@ -365,6 +412,44 @@ public: */ void setDrawsInLeftMargin (bool canDrawInLeftMargin) throw(); + //============================================================================== + /** Saves the current state of open/closed nodes so it can be restored later. + + This takes a snapshot of which sub-nodes have been explicitly opened or closed, + and records it as XML. To identify node objects it uses the + TreeViewItem::getUniqueName() method to create named paths. This + means that the same state of open/closed nodes can be restored to a + completely different instance of the tree, as long as it contains nodes + whose unique names are the same. + + You'd normally want to use TreeView::getOpennessState() rather than call it + for a specific item, but this can be handy if you need to briefly save the state + for a section of the tree. + + The caller is responsible for deleting the object that is returned. + @see TreeView::getOpennessState, restoreOpennessState + */ + XmlElement* getOpennessState() const throw(); + + /** Restores the openness of this item and all its sub-items from a saved state. + + See TreeView::restoreOpennessState for more details. + + You'd normally want to use TreeView::restoreOpennessState() rather than call it + for a specific item, but this can be handy if you need to briefly save the state + for a section of the tree. + + @see TreeView::restoreOpennessState, getOpennessState + */ + void restoreOpennessState (const XmlElement& xml) throw(); + + //============================================================================== + /** Returns the index of this item in its parent's sub-items. */ + int getIndexInParent() const throw(); + + /** Returns true if this item is the last of its parent's sub-itens. */ + bool isLastOfSiblings() const throw(); + //============================================================================== juce_UseDebuggingNewOperator @@ -387,12 +472,9 @@ private: int getIndentX() const throw(); void setOwnerView (TreeView* const newOwner) throw(); void paintRecursively (Graphics& g, int width); + TreeViewItem* getTopLevelItem() throw(); TreeViewItem* findItemRecursively (int y) throw(); TreeViewItem* getDeepestOpenParentItem() throw(); - void restoreFromXml (const XmlElement& e); - XmlElement* createXmlOpenness() const; - bool isLastOfSiblings() const throw(); - TreeViewItem* getTopLevelItem() throw(); int getNumRows() const throw(); TreeViewItem* getItemOnRow (int index) throw(); void deselectAllRecursively(); @@ -414,6 +496,8 @@ private: */ class JUCE_API TreeView : public Component, public SettableTooltipClient, + public FileDragAndDropTarget, + public DragAndDropTarget, private AsyncUpdater { public: @@ -549,6 +633,11 @@ public: */ TreeViewItem* getItemOnRow (int index) const; + /** Returns the item that contains a given y position. + The y is relative to the top of the TreeView component. + */ + TreeViewItem* getItemAt (int yPosition) const throw(); + /** Tries to scroll the tree so that this item is on-screen somewhere. */ void scrollToKeepItemVisible (TreeViewItem* item); @@ -605,8 +694,9 @@ public: */ enum ColourIds { - backgroundColourId = 0x1000500, /**< A background colour to fill the component with. */ - linesColourId = 0x1000501 /**< The colour to draw the lines with.*/ + backgroundColourId = 0x1000500, /**< A background colour to fill the component with. */ + linesColourId = 0x1000501, /**< The colour to draw the lines with.*/ + dragAndDropIndicatorColourId = 0x1000502 /**< The colour to use for the drag-and-drop target position indicator. */ }; //============================================================================== @@ -620,6 +710,26 @@ public: void colourChanged(); /** @internal */ void enablementChanged(); + /** @internal */ + bool isInterestedInFileDrag (const StringArray& files); + /** @internal */ + void fileDragEnter (const StringArray& files, int x, int y); + /** @internal */ + void fileDragMove (const StringArray& files, int x, int y); + /** @internal */ + void fileDragExit (const StringArray& files); + /** @internal */ + void filesDropped (const StringArray& files, int x, int y); + /** @internal */ + bool isInterestedInDragSource (const String& sourceDescription, Component* sourceComponent); + /** @internal */ + void itemDragEnter (const String& sourceDescription, Component* sourceComponent, int x, int y); + /** @internal */ + void itemDragMove (const String& sourceDescription, Component* sourceComponent, int x, int y); + /** @internal */ + void itemDragExit (const String& sourceDescription, Component* sourceComponent); + /** @internal */ + void itemDropped (const String& sourceDescription, Component* sourceComponent, int x, int y); juce_UseDebuggingNewOperator @@ -629,6 +739,8 @@ private: Viewport* viewport; CriticalSection nodeAlterationLock; TreeViewItem* rootItem; + Component* dragInsertPointHighlight; + Component* dragTargetGroupHighlight; int indentSize; bool defaultOpenness : 1; bool needsRecalculating : 1; @@ -640,6 +752,13 @@ private: void handleAsyncUpdate(); void moveSelectedRow (int delta); void updateButtonUnderMouse (const MouseEvent& e); + void showDragHighlight (TreeViewItem* item, int insertIndex, int x, int y) throw(); + void hideDragHighlight() throw(); + void handleDrag (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y); + void handleDrop (const StringArray& files, const String& sourceDescription, Component* sourceComponent, int x, int y); + TreeViewItem* getInsertPosition (int& x, int& y, int& insertIndex, + const StringArray& files, const String& sourceDescription, + Component* sourceComponent) const throw(); TreeView (const TreeView&); const TreeView& operator= (const TreeView&); diff --git a/src/gui/components/layout/juce_Viewport.cpp b/src/gui/components/layout/juce_Viewport.cpp index 9670d1734c..8830eb04f3 100644 --- a/src/gui/components/layout/juce_Viewport.cpp +++ b/src/gui/components/layout/juce_Viewport.cpp @@ -124,6 +124,44 @@ void Viewport::setViewPositionProportionately (const double x, jmax (0, roundDoubleToInt (y * (contentComp->getHeight() - getHeight())))); } +bool Viewport::autoScroll (int mouseX, int mouseY, int activeBorderThickness, int maximumSpeed) +{ + if (contentComp != 0) + { + int dx = 0, dy = 0; + + if (mouseX < activeBorderThickness) + dx = activeBorderThickness - mouseX; + else if (mouseX >= contentHolder->getWidth() - activeBorderThickness) + dx = (contentHolder->getWidth() - activeBorderThickness) - mouseX; + + if (dx < 0) + dx = jmax (dx, -maximumSpeed, contentHolder->getWidth() - contentComp->getRight()); + else + dx = jmin (dx, maximumSpeed, -contentComp->getX()); + + if (mouseY < activeBorderThickness) + dy = activeBorderThickness - mouseY; + else if (mouseY >= contentHolder->getHeight() - activeBorderThickness) + dy = (contentHolder->getHeight() - activeBorderThickness) - mouseY; + + if (dy < 0) + dy = jmax (dy, -maximumSpeed, contentHolder->getHeight() - contentComp->getBottom()); + else + dy = jmin (dy, maximumSpeed, -contentComp->getY()); + + if (dx != 0 || dy != 0) + { + contentComp->setTopLeftPosition (contentComp->getX() + dx, + contentComp->getY() + dy); + + return true; + } + } + + return false; +} + void Viewport::componentMovedOrResized (Component&, bool, bool) { updateVisibleRegion(); diff --git a/src/gui/components/layout/juce_Viewport.h b/src/gui/components/layout/juce_Viewport.h index ef24ce6329..01fe2fae70 100644 --- a/src/gui/components/layout/juce_Viewport.h +++ b/src/gui/components/layout/juce_Viewport.h @@ -104,6 +104,23 @@ public: void setViewPositionProportionately (const double proportionX, const double proportionY); + /** If the specified position is at the edges of the viewport, this method scrolls + the viewport to bring that position nearer to the centre. + + Call this if you're dragging an object inside a viewport and want to make it scroll + when the user approaches an edge. You might also find Component::beginDragAutoRepeat() + useful when auto-scrolling. + + @param mouseX the x position, relative to the Viewport's top-left + @param mouseY the y position, relative to the Viewport's top-left + @param distanceFromEdge specifies how close to an edge the position needs to be + before the viewport should scroll in that direction + @param maximumSpeed the maximum number of pixels that the viewport is allowed + to scroll by. + @returns true if the viewport was scrolled + */ + bool autoScroll (int mouseX, int mouseY, int distanceFromEdge, int maximumSpeed); + /** Returns the position within the child component of the top-left of its visible area. @see getViewWidth, setViewPosition */ diff --git a/src/gui/components/lookandfeel/juce_LookAndFeel.cpp b/src/gui/components/lookandfeel/juce_LookAndFeel.cpp index 647075f525..badb73e5ef 100644 --- a/src/gui/components/lookandfeel/juce_LookAndFeel.cpp +++ b/src/gui/components/lookandfeel/juce_LookAndFeel.cpp @@ -140,6 +140,7 @@ LookAndFeel::LookAndFeel() TreeView::linesColourId, 0x4c000000, TreeView::backgroundColourId, 0x00000000, + TreeView::dragAndDropIndicatorColourId, 0x80ff0000, PopupMenu::backgroundColourId, 0xffffffff, PopupMenu::textColourId, 0xff000000, diff --git a/src/gui/components/mouse/juce_DragAndDropContainer.cpp b/src/gui/components/mouse/juce_DragAndDropContainer.cpp index a4ee0dff3d..4a29a08e2e 100644 --- a/src/gui/components/mouse/juce_DragAndDropContainer.cpp +++ b/src/gui/components/mouse/juce_DragAndDropContainer.cpp @@ -55,7 +55,7 @@ private: ComponentDeletionWatcher* currentlyOverWatcher; String dragDesc; - int xOff, yOff; + const int imageX, imageY; bool hasCheckedForExternalDrag, drawImage; DragImageComponent (const DragImageComponent&); @@ -65,13 +65,16 @@ public: DragImageComponent (Image* const im, const String& desc, Component* const s, - DragAndDropContainer* const o) + DragAndDropContainer* const o, + const int imageX_, const int imageY_) : image (im), source (s), owner (o), currentlyOver (0), currentlyOverWatcher (0), dragDesc (desc), + imageX (imageX_), + imageY (imageY_), hasCheckedForExternalDrag (false), drawImage (true) { @@ -87,13 +90,6 @@ public: mouseDragSourceWatcher = new ComponentDeletionWatcher (mouseDragSource); mouseDragSource->addMouseListener (this, false); - int mx, my; - Desktop::getLastMouseDownPosition (mx, my); - source->globalPositionToRelative (mx, my); - - xOff = jlimit (0, im->getWidth(), mx); - yOff = jlimit (0, im->getHeight(), my); - startTimer (200); setInterceptsMouseClicks (false, false); @@ -240,13 +236,13 @@ public: // a modal loop and deletes this object before it returns) const String dragDescLocal (dragDesc); - int newX = x - xOff; - int newY = y - yOff; + int newX = x + imageX; + int newY = y + imageY; if (getParentComponent() != 0) getParentComponent()->globalPositionToRelative (newX, newY); - if (newX != getX() || newY != getY()) + //if (newX != getX() || newY != getY()) { setTopLeftPosition (newX, newY); @@ -351,19 +347,18 @@ DragAndDropContainer::DragAndDropContainer() DragAndDropContainer::~DragAndDropContainer() { - if (dragImageComponent != 0) - delete dragImageComponent; + delete dragImageComponent; } void DragAndDropContainer::startDragging (const String& sourceDescription, Component* sourceComponent, Image* im, - const bool allowDraggingToExternalWindows) + const bool allowDraggingToExternalWindows, + const Point* imageOffsetFromMouse) { if (dragImageComponent != 0) { - if (im != 0) - delete im; + delete im; } else { @@ -373,6 +368,7 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, { int mx, my; Desktop::getLastMouseDownPosition (mx, my); + int imageX = 0, imageY = 0; if (im == 0) { @@ -417,13 +413,27 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, } } } + + imageX = -cx; + imageY = -cy; + } + else + { + if (imageOffsetFromMouse == 0) + { + imageX = im->getWidth() / -2; + imageY = im->getHeight() / -2; + } + else + { + imageX = (int) imageOffsetFromMouse->getX(); + imageY = (int) imageOffsetFromMouse->getY(); + } } DragImageComponent* const dic - = new DragImageComponent (im, - sourceDescription, - sourceComponent, - this); + = new DragImageComponent (im, sourceDescription, sourceComponent, + this, imageX, imageY); dragImageComponent = dic; currentDragDesc = sourceDescription; @@ -449,8 +459,7 @@ void DragAndDropContainer::startDragging (const String& sourceDescription, // is also a Component. jassertfalse - if (im != 0) - delete im; + delete im; } } } diff --git a/src/gui/components/mouse/juce_DragAndDropContainer.h b/src/gui/components/mouse/juce_DragAndDropContainer.h index e0c0a2d38e..647dfb0ae5 100644 --- a/src/gui/components/mouse/juce_DragAndDropContainer.h +++ b/src/gui/components/mouse/juce_DragAndDropContainer.h @@ -85,11 +85,16 @@ public: @param allowDraggingToOtherJuceWindows if true, the dragged component will appear as a desktop window, and can be dragged to DragAndDropTargets that are the children of components other than this one. + @param imageOffsetFromMouse if an image has been passed-in, this specifies the offset + at which the image should be drawn from the mouse. If it isn't + specified, then the image will be centred around the mouse. If + an image hasn't been passed-in, this will be ignored. */ void startDragging (const String& sourceDescription, Component* sourceComponent, Image* dragImage = 0, - const bool allowDraggingToOtherJuceWindows = false); + const bool allowDraggingToOtherJuceWindows = false, + const Point* imageOffsetFromMouse = 0); /** Returns true if something is currently being dragged. */ bool isDragAndDropActive() const;