/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. 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 { class DropShadower::ShadowWindow : public Component { public: ShadowWindow (Component* comp, const DropShadow& ds) : target (comp), shadow (ds) { setVisible (true); setAccessible (false); setInterceptsMouseClicks (false, false); if (comp->isOnDesktop()) { #if JUCE_WINDOWS const auto scope = [&]() -> std::unique_ptr { if (comp != nullptr) if (auto* handle = comp->getWindowHandle()) return std::make_unique (handle); return nullptr; }(); #endif setSize (1, 1); // to keep the OS happy by not having zero-size windows addToDesktop (ComponentPeer::windowIgnoresMouseClicks | ComponentPeer::windowIsTemporary | ComponentPeer::windowIgnoresKeyPresses); } else if (Component* const parent = comp->getParentComponent()) { parent->addChildComponent (this); } } void paint (Graphics& g) override { if (Component* c = target) shadow.drawForRectangle (g, getLocalArea (c, c->getLocalBounds())); } void resized() override { repaint(); // (needed for correct repainting) } float getDesktopScaleFactor() const override { if (target != nullptr) return target->getDesktopScaleFactor(); return Component::getDesktopScaleFactor(); } private: WeakReference target; DropShadow shadow; JUCE_DECLARE_NON_COPYABLE (ShadowWindow) }; class DropShadower::ParentVisibilityChangedListener : public ComponentListener, private Timer { public: ParentVisibilityChangedListener (Component& r, ComponentListener& l) : root (&r), listener (&l) { if (auto* firstParent = root->getParentComponent()) updateParentHierarchy (firstParent); if ((SystemStats::getOperatingSystemType() & SystemStats::Windows) != 0) { isOnVirtualDesktop = isWindowOnCurrentVirtualDesktop (root->getWindowHandle()); startTimerHz (5); } } ~ParentVisibilityChangedListener() override { for (auto& compEntry : observedComponents) if (auto* comp = compEntry.get()) comp->removeComponentListener (this); } void componentVisibilityChanged (Component&) override { listener->componentVisibilityChanged (*root); } void componentParentHierarchyChanged (Component& component) override { if (root == &component) if (auto* firstParent = root->getParentComponent()) updateParentHierarchy (firstParent); } bool isWindowOnVirtualDesktop() const noexcept { return isOnVirtualDesktop; } private: class ComponentWithWeakReference { public: explicit ComponentWithWeakReference (Component& c) : ptr (&c), ref (&c) {} Component* get() const { return ref.get(); } bool operator< (const ComponentWithWeakReference& other) const { return ptr < other.ptr; } private: Component* ptr; WeakReference ref; }; void updateParentHierarchy (Component* rootComponent) { const auto lastSeenComponents = std::exchange (observedComponents, [&] { std::set result; for (auto node = rootComponent; node != nullptr; node = node->getParentComponent()) result.emplace (*node); return result; }()); const auto withDifference = [] (const auto& rangeA, const auto& rangeB, auto&& callback) { std::vector result; std::set_difference (rangeA.begin(), rangeA.end(), rangeB.begin(), rangeB.end(), std::back_inserter (result)); for (const auto& item : result) if (auto* c = item.get()) callback (*c); }; withDifference (lastSeenComponents, observedComponents, [this] (auto& comp) { comp.removeComponentListener (this); }); withDifference (observedComponents, lastSeenComponents, [this] (auto& comp) { comp.addComponentListener (this); }); } void timerCallback() override { WeakReference deletionChecker { static_cast (listener) }; const auto wasOnVirtualDesktop = std::exchange (isOnVirtualDesktop, isWindowOnCurrentVirtualDesktop (root->getWindowHandle())); // on Windows, isWindowOnCurrentVirtualDesktop() may cause synchronous messages to be dispatched // to the HWND so we need to check if the shadower is still valid after calling if (deletionChecker == nullptr) return; if (isOnVirtualDesktop != wasOnVirtualDesktop) listener->componentVisibilityChanged (*root); } Component* root = nullptr; ComponentListener* listener = nullptr; std::set observedComponents; bool isOnVirtualDesktop = true; JUCE_DECLARE_NON_COPYABLE (ParentVisibilityChangedListener) JUCE_DECLARE_NON_MOVEABLE (ParentVisibilityChangedListener) }; //============================================================================== DropShadower::DropShadower (const DropShadow& ds) : shadow (ds) {} DropShadower::~DropShadower() { if (owner != nullptr) { owner->removeComponentListener (this); owner = nullptr; } updateParent(); const ScopedValueSetter setter (reentrant, true); shadowWindows.clear(); } void DropShadower::setOwner (Component* componentToFollow) { if (componentToFollow != owner) { if (owner != nullptr) owner->removeComponentListener (this); // (the component can't be null) jassert (componentToFollow != nullptr); owner = componentToFollow; jassert (owner != nullptr); updateParent(); owner->addComponentListener (this); // The visibility of `owner` is transitively affected by the visibility of its parents. Thus we need to trigger the // componentVisibilityChanged() event in case it changes for any of the parents. visibilityChangedListener = std::make_unique (*owner, static_cast (*this)); updateShadows(); } } void DropShadower::updateParent() { if (Component* p = lastParentComp) p->removeComponentListener (this); lastParentComp = owner != nullptr ? owner->getParentComponent() : nullptr; if (Component* p = lastParentComp) p->addComponentListener (this); } void DropShadower::componentMovedOrResized (Component& c, bool /*wasMoved*/, bool /*wasResized*/) { if (owner == &c) updateShadows(); } void DropShadower::componentBroughtToFront (Component& c) { if (owner == &c) updateShadows(); } void DropShadower::componentChildrenChanged (Component&) { updateShadows(); } void DropShadower::componentParentHierarchyChanged (Component& c) { if (owner == &c) { updateParent(); updateShadows(); } } void DropShadower::componentVisibilityChanged (Component& c) { if (owner == &c) updateShadows(); } void DropShadower::updateShadows() { if (reentrant) return; const ScopedValueSetter setter (reentrant, true); if (owner != nullptr && owner->isShowing() && owner->getWidth() > 0 && owner->getHeight() > 0 && (Desktop::canUseSemiTransparentWindows() || owner->getParentComponent() != nullptr) && (visibilityChangedListener != nullptr && visibilityChangedListener->isWindowOnVirtualDesktop())) { while (shadowWindows.size() < 4) shadowWindows.add (new ShadowWindow (owner, shadow)); const int shadowEdge = jmax (shadow.offset.x, shadow.offset.y) + shadow.radius; const int x = owner->getX(); const int y = owner->getY() - shadowEdge; const int w = owner->getWidth(); const int h = owner->getHeight() + shadowEdge + shadowEdge; for (int i = 4; --i >= 0;) { // there seem to be rare situations where the dropshadower may be deleted by // callbacks during this loop, so use a weak ref to watch out for this.. WeakReference sw (shadowWindows[i]); if (sw != nullptr) { sw->setAlwaysOnTop (owner->isAlwaysOnTop()); if (sw == nullptr) return; switch (i) { case 0: sw->setBounds (x - shadowEdge, y, shadowEdge, h); break; case 1: sw->setBounds (x + w, y, shadowEdge, h); break; case 2: sw->setBounds (x, y, w, shadowEdge); break; case 3: sw->setBounds (x, owner->getBottom(), w, shadowEdge); break; default: break; } if (sw == nullptr) return; sw->toBehind (i == 3 ? owner.get() : shadowWindows.getUnchecked (i + 1)); } } } else { shadowWindows.clear(); } } } // namespace juce