/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { bool juce_performDragDropFiles (const StringArray&, const bool copyFiles, bool& shouldStop); bool juce_performDragDropText (const String&, bool& shouldStop); //============================================================================== class DragAndDropContainer::DragImageComponent : public Component, private Timer { public: DragImageComponent (const Image& im, const var& desc, Component* const sourceComponent, const MouseInputSource* draggingSource, DragAndDropContainer& ddc, Point offset) : sourceDetails (desc, sourceComponent, Point()), image (im), owner (ddc), mouseDragSource (draggingSource->getComponentUnderMouse()), imageOffset (offset), originalInputSourceIndex (draggingSource->getIndex()), originalInputSourceType (draggingSource->getType()) { updateSize(); if (mouseDragSource == nullptr) mouseDragSource = sourceComponent; mouseDragSource->addMouseListener (this, false); startTimer (200); setInterceptsMouseClicks (false, false); setAlwaysOnTop (true); } ~DragImageComponent() override { owner.dragImageComponents.remove (owner.dragImageComponents.indexOf (this), false); if (mouseDragSource != nullptr) { mouseDragSource->removeMouseListener (this); if (auto* current = getCurrentlyOver()) if (current->isInterestedInDragSource (sourceDetails)) current->itemDragExit (sourceDetails); } owner.dragOperationEnded (sourceDetails); } void paint (Graphics& g) override { if (isOpaque()) g.fillAll (Colours::white); g.setOpacity (1.0f); g.drawImageAt (image, 0, 0); } void mouseUp (const MouseEvent& e) override { if (e.originalComponent != this && isOriginalInputSource (e.source)) { if (mouseDragSource != nullptr) mouseDragSource->removeMouseListener (this); // (note: use a local copy of this in case the callback runs // a modal loop and deletes this object before the method completes) auto details = sourceDetails; DragAndDropTarget* finalTarget = nullptr; auto wasVisible = isVisible(); setVisible (false); Component* unused; finalTarget = findTarget (e.getScreenPosition(), details.localPosition, unused); if (wasVisible) // fade the component and remove it - it'll be deleted later by the timer callback dismissWithAnimation (finalTarget == nullptr); if (auto* parent = getParentComponent()) parent->removeChildComponent (this); if (finalTarget != nullptr) { currentlyOverComp = nullptr; finalTarget->itemDropped (details); } // careful - this object could now be deleted.. } } void mouseDrag (const MouseEvent& e) override { if (e.originalComponent != this && isOriginalInputSource (e.source)) updateLocation (true, e.getScreenPosition()); } void updateLocation (const bool canDoExternalDrag, Point screenPos) { auto details = sourceDetails; setNewScreenPos (screenPos); Component* newTargetComp; auto* newTarget = findTarget (screenPos, details.localPosition, newTargetComp); setVisible (newTarget == nullptr || newTarget->shouldDrawDragImageWhenOver()); if (newTargetComp != currentlyOverComp) { if (auto* lastTarget = getCurrentlyOver()) if (details.sourceComponent != nullptr && lastTarget->isInterestedInDragSource (details)) lastTarget->itemDragExit (details); currentlyOverComp = newTargetComp; if (newTarget != nullptr && newTarget->isInterestedInDragSource (details)) newTarget->itemDragEnter (details); } sendDragMove (details); if (canDoExternalDrag) { auto now = Time::getCurrentTime(); if (getCurrentlyOver() != nullptr) lastTimeOverTarget = now; else if (now > lastTimeOverTarget + RelativeTime::milliseconds (700)) checkForExternalDrag (details, screenPos); } forceMouseCursorUpdate(); } void updateImage (const Image& newImage) { image = newImage; updateSize(); repaint(); } void timerCallback() override { forceMouseCursorUpdate(); if (sourceDetails.sourceComponent == nullptr) { deleteSelf(); } else { for (auto& s : Desktop::getInstance().getMouseSources()) { if (isOriginalInputSource (s) && ! s.isDragging()) { if (mouseDragSource != nullptr) mouseDragSource->removeMouseListener (this); deleteSelf(); break; } } } } bool keyPressed (const KeyPress& key) override { if (key == KeyPress::escapeKey) { dismissWithAnimation (true); deleteSelf(); return true; } return false; } bool canModalEventBeSentToComponent (const Component* targetComponent) override { return targetComponent == mouseDragSource; } // (overridden to avoid beeps when dragging) void inputAttemptWhenModal() override {} DragAndDropTarget::SourceDetails sourceDetails; private: Image image; DragAndDropContainer& owner; WeakReference mouseDragSource, currentlyOverComp; const Point imageOffset; bool hasCheckedForExternalDrag = false; Time lastTimeOverTarget; int originalInputSourceIndex; MouseInputSource::InputSourceType originalInputSourceType; void updateSize() { setSize (image.getWidth(), image.getHeight()); } void forceMouseCursorUpdate() { Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate(); } DragAndDropTarget* getCurrentlyOver() const noexcept { return dynamic_cast (currentlyOverComp.get()); } static Component* findDesktopComponentBelow (Point screenPos) { auto& desktop = Desktop::getInstance(); for (auto i = desktop.getNumComponents(); --i >= 0;) { auto* desktopComponent = desktop.getComponent (i); auto dPoint = desktopComponent->getLocalPoint (nullptr, screenPos); if (auto* c = desktopComponent->getComponentAt (dPoint)) { auto cPoint = c->getLocalPoint (desktopComponent, dPoint); if (c->hitTest (cPoint.getX(), cPoint.getY())) return c; } } return nullptr; } DragAndDropTarget* findTarget (Point screenPos, Point& relativePos, Component*& resultComponent) const { auto* hit = getParentComponent(); if (hit == nullptr) hit = findDesktopComponentBelow (screenPos); else hit = hit->getComponentAt (hit->getLocalPoint (nullptr, screenPos)); // (note: use a local copy of this in case the callback runs // a modal loop and deletes this object before the method completes) auto details = sourceDetails; while (hit != nullptr) { if (auto* ddt = dynamic_cast (hit)) { if (ddt->isInterestedInDragSource (details)) { relativePos = hit->getLocalPoint (nullptr, screenPos); resultComponent = hit; return ddt; } } hit = hit->getParentComponent(); } resultComponent = nullptr; return nullptr; } void setNewScreenPos (Point screenPos) { auto newPos = screenPos - imageOffset; if (auto* p = getParentComponent()) newPos = p->getLocalPoint (nullptr, newPos); setTopLeftPosition (newPos); } void sendDragMove (DragAndDropTarget::SourceDetails& details) const { if (auto* target = getCurrentlyOver()) if (target->isInterestedInDragSource (details)) target->itemDragMove (details); } void checkForExternalDrag (DragAndDropTarget::SourceDetails& details, Point screenPos) { if (! hasCheckedForExternalDrag) { if (Desktop::getInstance().findComponentAt (screenPos) == nullptr) { hasCheckedForExternalDrag = true; if (ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown()) { StringArray files; auto canMoveFiles = false; if (owner.shouldDropFilesWhenDraggedExternally (details, files, canMoveFiles) && ! files.isEmpty()) { MessageManager::callAsync ([=] { DragAndDropContainer::performExternalDragDropOfFiles (files, canMoveFiles); }); deleteSelf(); return; } String text; if (owner.shouldDropTextWhenDraggedExternally (details, text) && text.isNotEmpty()) { MessageManager::callAsync ([=] { DragAndDropContainer::performExternalDragDropOfText (text); }); deleteSelf(); return; } } } } } void deleteSelf() { delete this; } void dismissWithAnimation (const bool shouldSnapBack) { setVisible (true); auto& animator = Desktop::getInstance().getAnimator(); if (shouldSnapBack && sourceDetails.sourceComponent != nullptr) { auto target = sourceDetails.sourceComponent->localPointToGlobal (sourceDetails.sourceComponent->getLocalBounds().getCentre()); auto ourCentre = localPointToGlobal (getLocalBounds().getCentre()); animator.animateComponent (this, getBounds() + (target - ourCentre), 0.0f, 120, true, 1.0, 1.0); } else { animator.fadeOut (this, 120); } } bool isOriginalInputSource (const MouseInputSource& sourceToCheck) { return (sourceToCheck.getType() == originalInputSourceType && sourceToCheck.getIndex() == originalInputSourceIndex); } JUCE_DECLARE_NON_COPYABLE (DragImageComponent) }; //============================================================================== DragAndDropContainer::DragAndDropContainer() { } DragAndDropContainer::~DragAndDropContainer() { } void DragAndDropContainer::startDragging (const var& sourceDescription, Component* sourceComponent, Image dragImage, const bool allowDraggingToExternalWindows, const Point* imageOffsetFromMouse, const MouseInputSource* inputSourceCausingDrag) { if (isAlreadyDragging (sourceComponent)) return; auto* draggingSource = getMouseInputSourceForDrag (sourceComponent, inputSourceCausingDrag); if (draggingSource == nullptr || ! draggingSource->isDragging()) { jassertfalse; // You must call startDragging() from within a mouseDown or mouseDrag callback! return; } auto lastMouseDown = draggingSource->getLastMouseDownPosition().roundToInt(); Point imageOffset; if (dragImage.isNull()) { dragImage = sourceComponent->createComponentSnapshot (sourceComponent->getLocalBounds()) .convertedToFormat (Image::ARGB); dragImage.multiplyAllAlphas (0.6f); auto lo = 150; auto hi = 400; auto relPos = sourceComponent->getLocalPoint (nullptr, lastMouseDown); auto clipped = dragImage.getBounds().getConstrainedPoint (relPos); Random random; for (auto y = dragImage.getHeight(); --y >= 0;) { auto dy = (y - clipped.getY()) * (y - clipped.getY()); for (auto x = dragImage.getWidth(); --x >= 0;) { auto dx = x - clipped.getX(); auto distance = roundToInt (std::sqrt (dx * dx + dy)); if (distance > lo) { auto alpha = (distance > hi) ? 0 : (hi - distance) / (float) (hi - lo) + random.nextFloat() * 0.008f; dragImage.multiplyAlphaAt (x, y, alpha); } } } imageOffset = clipped; } else { if (imageOffsetFromMouse == nullptr) imageOffset = dragImage.getBounds().getCentre(); else imageOffset = dragImage.getBounds().getConstrainedPoint (-*imageOffsetFromMouse); } auto* dragImageComponent = dragImageComponents.add (new DragImageComponent (dragImage, sourceDescription, sourceComponent, draggingSource, *this, imageOffset)); if (allowDraggingToExternalWindows) { if (! Desktop::canUseSemiTransparentWindows()) dragImageComponent->setOpaque (true); dragImageComponent->addToDesktop (ComponentPeer::windowIgnoresMouseClicks | ComponentPeer::windowIsTemporary | ComponentPeer::windowIgnoresKeyPresses); } else { if (auto* thisComp = dynamic_cast (this)) { thisComp->addChildComponent (dragImageComponent); } else { jassertfalse; // Your DragAndDropContainer needs to be a Component! return; } } dragImageComponent->sourceDetails.localPosition = sourceComponent->getLocalPoint (nullptr, lastMouseDown); dragImageComponent->updateLocation (false, lastMouseDown); #if JUCE_WINDOWS // Under heavy load, the layered window's paint callback can often be lost by the OS, // so forcing a repaint at least once makes sure that the window becomes visible.. if (auto* peer = dragImageComponent->getPeer()) peer->performAnyPendingRepaintsNow(); #endif dragOperationStarted (dragImageComponent->sourceDetails); } bool DragAndDropContainer::isDragAndDropActive() const { return dragImageComponents.size() > 0; } int DragAndDropContainer::getNumCurrentDrags() const { return dragImageComponents.size(); } var DragAndDropContainer::getCurrentDragDescription() const { // If you are performing drag and drop in a multi-touch environment then // you should use the getDragDescriptionForIndex() method instead! jassert (dragImageComponents.size() < 2); return dragImageComponents.size() != 0 ? dragImageComponents[0]->sourceDetails.description : var(); } var DragAndDropContainer::getDragDescriptionForIndex (int index) const { if (! isPositiveAndBelow (index, dragImageComponents.size())) return {}; return dragImageComponents.getUnchecked (index)->sourceDetails.description; } void DragAndDropContainer::setCurrentDragImage (const Image& newImage) { // If you are performing drag and drop in a multi-touch environment then // you should use the setDragImageForIndex() method instead! jassert (dragImageComponents.size() < 2); dragImageComponents[0]->updateImage (newImage); } void DragAndDropContainer::setDragImageForIndex (int index, const Image& newImage) { if (isPositiveAndBelow (index, dragImageComponents.size())) dragImageComponents.getUnchecked (index)->updateImage (newImage); } DragAndDropContainer* DragAndDropContainer::findParentDragContainerFor (Component* c) { return c != nullptr ? c->findParentComponentOfClass() : nullptr; } bool DragAndDropContainer::shouldDropFilesWhenDraggedExternally (const DragAndDropTarget::SourceDetails&, StringArray&, bool&) { return false; } bool DragAndDropContainer::shouldDropTextWhenDraggedExternally (const DragAndDropTarget::SourceDetails&, String&) { return false; } void DragAndDropContainer::dragOperationStarted (const DragAndDropTarget::SourceDetails&) {} void DragAndDropContainer::dragOperationEnded (const DragAndDropTarget::SourceDetails&) {} const MouseInputSource* DragAndDropContainer::getMouseInputSourceForDrag (Component* sourceComponent, const MouseInputSource* inputSourceCausingDrag) { if (inputSourceCausingDrag == nullptr) { auto minDistance = std::numeric_limits::max(); auto& desktop = Desktop::getInstance(); auto centrePoint = sourceComponent ? sourceComponent->getScreenBounds().getCentre().toFloat() : Point(); auto numDragging = desktop.getNumDraggingMouseSources(); for (auto i = 0; i < numDragging; ++i) { if (auto* ms = desktop.getDraggingMouseSource (i)) { auto distance = ms->getScreenPosition().getDistanceSquaredFrom (centrePoint); if (distance < minDistance) { minDistance = distance; inputSourceCausingDrag = ms; } } } } // You must call startDragging() from within a mouseDown or mouseDrag callback! jassert (inputSourceCausingDrag != nullptr && inputSourceCausingDrag->isDragging()); return inputSourceCausingDrag; } bool DragAndDropContainer::isAlreadyDragging (Component* component) const noexcept { for (auto* dragImageComp : dragImageComponents) { if (dragImageComp->sourceDetails.sourceComponent == component) return true; } return false; } //============================================================================== DragAndDropTarget::SourceDetails::SourceDetails (const var& desc, Component* comp, Point pos) noexcept : description (desc), sourceComponent (comp), localPosition (pos) { } void DragAndDropTarget::itemDragEnter (const SourceDetails&) {} void DragAndDropTarget::itemDragMove (const SourceDetails&) {} void DragAndDropTarget::itemDragExit (const SourceDetails&) {} bool DragAndDropTarget::shouldDrawDragImageWhenOver() { return true; } //============================================================================== void FileDragAndDropTarget::fileDragEnter (const StringArray&, int, int) {} void FileDragAndDropTarget::fileDragMove (const StringArray&, int, int) {} void FileDragAndDropTarget::fileDragExit (const StringArray&) {} void TextDragAndDropTarget::textDragEnter (const String&, int, int) {} void TextDragAndDropTarget::textDragMove (const String&, int, int) {} void TextDragAndDropTarget::textDragExit (const String&) {} } // namespace juce