/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { 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 ScaledImage& 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 (transformOffsetCoordinates (sourceComponent, offset)), originalInputSourceIndex (draggingSource->getIndex()), originalInputSourceType (draggingSource->getType()) { updateSize(); if (mouseDragSource == nullptr) mouseDragSource = sourceComponent; mouseDragSource->addMouseListener (this, false); startTimer (200); setInterceptsMouseClicks (false, false); setWantsKeyboardFocus (true); 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.drawImage (image.getImage(), getLocalBounds().toFloat()); } 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 ScaledImage& 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) { const auto wasVisible = isVisible(); setVisible (false); if (wasVisible) 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: ScaledImage image; DragAndDropContainer& owner; WeakReference mouseDragSource, currentlyOverComp; const Point imageOffset; bool hasCheckedForExternalDrag = false; Time lastTimeOverTarget; int originalInputSourceIndex; MouseInputSource::InputSourceType originalInputSourceType; void updateSize() { const auto bounds = image.getScaledBounds().toNearestInt(); setSize (bounds.getWidth(), bounds.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; } Point transformOffsetCoordinates (const Component* const sourceComponent, Point offsetInSource) const { return getLocalPoint (sourceComponent, offsetInSource) - getLocalPoint (sourceComponent, Point()); } 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() = default; DragAndDropContainer::~DragAndDropContainer() = default; void DragAndDropContainer::startDragging (const var& sourceDescription, Component* sourceComponent, const ScaledImage& 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; } const auto lastMouseDown = draggingSource->getLastMouseDownPosition().roundToInt(); struct ImageAndOffset { ScaledImage image; Point offset; }; const auto imageToUse = [&]() -> ImageAndOffset { if (! dragImage.getImage().isNull()) return { dragImage, imageOffsetFromMouse != nullptr ? dragImage.getScaledBounds().getConstrainedPoint (-imageOffsetFromMouse->toDouble()) : dragImage.getScaledBounds().getCentre() }; const auto scaleFactor = 2.0; auto image = sourceComponent->createComponentSnapshot (sourceComponent->getLocalBounds(), true, (float) scaleFactor) .convertedToFormat (Image::ARGB); image.multiplyAllAlphas (0.6f); const auto relPos = sourceComponent->getLocalPoint (nullptr, lastMouseDown).toDouble(); const auto clipped = (image.getBounds().toDouble() / scaleFactor).getConstrainedPoint (relPos); Image fade (Image::SingleChannel, image.getWidth(), image.getHeight(), true); Graphics fadeContext (fade); ColourGradient gradient; gradient.isRadial = true; gradient.point1 = clipped.toFloat() * scaleFactor; gradient.point2 = gradient.point1 + Point (0.0f, scaleFactor * 400.0f); gradient.addColour (0.0, Colours::white); gradient.addColour (0.375, Colours::white); gradient.addColour (1.0, Colours::transparentWhite); fadeContext.setGradientFill (gradient); fadeContext.fillAll(); Image composite (Image::ARGB, image.getWidth(), image.getHeight(), true); Graphics compositeContext (composite); compositeContext.reduceClipRegion (fade, {}); compositeContext.drawImageAt (image, 0, 0); return { ScaledImage (composite, scaleFactor), clipped }; }(); auto* dragImageComponent = dragImageComponents.add (new DragImageComponent (imageToUse.image, sourceDescription, sourceComponent, draggingSource, *this, imageToUse.offset.roundToInt())); if (allowDraggingToExternalWindows) { if (! Desktop::canUseSemiTransparentWindows()) dragImageComponent->setOpaque (true); dragImageComponent->addToDesktop (ComponentPeer::windowIgnoresMouseClicks | ComponentPeer::windowIsTemporary); } 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); dragImageComponent->grabKeyboardFocus(); #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 ScaledImage& 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 ScaledImage& 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