build(deps): vendor JUCE 7.0.12

This commit is contained in:
2026-09-08 14:55:21 +02:00
parent 5b99f51bd1
commit d6705ab29f
3591 changed files with 1218267 additions and 0 deletions
@@ -0,0 +1,37 @@
/*
==============================================================================
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
{
#if ! JUCE_NATIVE_ACCESSIBILITY_INCLUDED
class AccessibilityHandler::AccessibilityNativeImpl
{
public:
AccessibilityNativeImpl (AccessibilityHandler&) {}
};
#endif
} // namespace juce
@@ -0,0 +1,658 @@
/*
==============================================================================
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
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
int AccessibilityNativeHandle::idCounter = 0;
//==============================================================================
class UIAScrollProvider final : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IScrollProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
JUCE_COMCALL Scroll (ComTypes::ScrollAmount, ComTypes::ScrollAmount) override { return E_FAIL; }
JUCE_COMCALL SetScrollPercent (double, double) override { return E_FAIL; }
JUCE_COMCALL get_HorizontalScrollPercent (double*) override { return E_FAIL; }
JUCE_COMCALL get_VerticalScrollPercent (double*) override { return E_FAIL; }
JUCE_COMCALL get_HorizontalViewSize (double*) override { return E_FAIL; }
JUCE_COMCALL get_VerticalViewSize (double*) override { return E_FAIL; }
JUCE_COMCALL get_HorizontallyScrollable (BOOL*) override { return E_FAIL; }
JUCE_COMCALL get_VerticallyScrollable (BOOL*) override { return E_FAIL; }
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAScrollProvider)
};
class UIAScrollItemProvider final : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IScrollItemProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
JUCE_COMCALL ScrollIntoView() override
{
if (auto* handler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface))
{
if (auto* tableInterface = handler->getTableInterface())
{
tableInterface->showCell (getHandler());
return S_OK;
}
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAScrollItemProvider)
};
//==============================================================================
static String getAutomationId (const AccessibilityHandler& handler)
{
auto result = handler.getTitle();
auto* parentComponent = handler.getComponent().getParentComponent();
while (parentComponent != nullptr)
{
if (auto* parentHandler = parentComponent->getAccessibilityHandler())
{
auto parentTitle = parentHandler->getTitle();
result << "." << (parentTitle.isNotEmpty() ? parentTitle : "<empty>");
}
parentComponent = parentComponent->getParentComponent();
}
return result;
}
static auto roleToControlTypeId (AccessibilityRole roleType)
{
using namespace ComTypes::Constants;
switch (roleType)
{
case AccessibilityRole::popupMenu:
case AccessibilityRole::dialogWindow:
case AccessibilityRole::splashScreen:
case AccessibilityRole::window: return UIA_WindowControlTypeId;
case AccessibilityRole::label:
case AccessibilityRole::staticText: return UIA_TextControlTypeId;
case AccessibilityRole::column:
case AccessibilityRole::row: return UIA_ListItemControlTypeId;
case AccessibilityRole::button: return UIA_ButtonControlTypeId;
case AccessibilityRole::toggleButton: return UIA_CheckBoxControlTypeId;
case AccessibilityRole::radioButton: return UIA_RadioButtonControlTypeId;
case AccessibilityRole::comboBox: return UIA_ComboBoxControlTypeId;
case AccessibilityRole::image: return UIA_ImageControlTypeId;
case AccessibilityRole::slider: return UIA_SliderControlTypeId;
case AccessibilityRole::editableText: return UIA_EditControlTypeId;
case AccessibilityRole::menuItem: return UIA_MenuItemControlTypeId;
case AccessibilityRole::menuBar: return UIA_MenuBarControlTypeId;
case AccessibilityRole::table: return UIA_TableControlTypeId;
case AccessibilityRole::tableHeader: return UIA_HeaderControlTypeId;
case AccessibilityRole::cell: return UIA_DataItemControlTypeId;
case AccessibilityRole::hyperlink: return UIA_HyperlinkControlTypeId;
case AccessibilityRole::list: return UIA_ListControlTypeId;
case AccessibilityRole::listItem: return UIA_ListItemControlTypeId;
case AccessibilityRole::tree: return UIA_TreeControlTypeId;
case AccessibilityRole::treeItem: return UIA_TreeItemControlTypeId;
case AccessibilityRole::progressBar: return UIA_ProgressBarControlTypeId;
case AccessibilityRole::group: return UIA_GroupControlTypeId;
case AccessibilityRole::scrollBar: return UIA_ScrollBarControlTypeId;
case AccessibilityRole::tooltip: return UIA_ToolTipControlTypeId;
case AccessibilityRole::ignored:
case AccessibilityRole::unspecified: break;
};
return UIA_CustomControlTypeId;
}
//==============================================================================
AccessibilityNativeHandle::AccessibilityNativeHandle (AccessibilityHandler& handler)
: ComBaseClassHelper (0),
accessibilityHandler (handler)
{
}
//==============================================================================
JUCE_COMRESULT AccessibilityNativeHandle::QueryInterface (REFIID refId, void** result)
{
*result = nullptr;
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if ((refId == __uuidof (ComTypes::IRawElementProviderFragmentRoot) && ! isFragmentRoot()))
return E_NOINTERFACE;
return ComBaseClassHelper::QueryInterface (refId, result);
}
//==============================================================================
JUCE_COMRESULT AccessibilityNativeHandle::get_HostRawElementProvider (IRawElementProviderSimple** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
if (auto* wrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
{
if (isFragmentRoot())
return wrapper->hostProviderFromHwnd ((HWND) accessibilityHandler.getComponent().getWindowHandle(), pRetVal);
if (auto* embeddedWindow = static_cast<HWND> (AccessibilityHandler::getNativeChildForComponent (accessibilityHandler.getComponent())))
return wrapper->hostProviderFromHwnd (embeddedWindow, pRetVal);
}
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::get_ProviderOptions (ProviderOptions* options)
{
if (options == nullptr)
return E_INVALIDARG;
*options = (ProviderOptions) (ProviderOptions_ServerSideProvider | ProviderOptions_UseComThreading);
if (AccessibilityHandler::getNativeChildForComponent (accessibilityHandler.getComponent()) != nullptr)
*options = (ProviderOptions) (*options | ProviderOptions_OverrideProvider);
return S_OK;
}
JUCE_COMRESULT AccessibilityNativeHandle::GetPatternProvider (PATTERNID pId, IUnknown** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = [&]() -> IUnknown*
{
const auto role = accessibilityHandler.getRole();
const auto fragmentRoot = isFragmentRoot();
const auto isListOrTableCell = [] (auto& handler)
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&handler, &AccessibilityHandler::getTableInterface))
{
if (auto* tableInterface = tableHandler->getTableInterface())
{
const auto row = tableInterface->getRowSpan (handler);
const auto column = tableInterface->getColumnSpan (handler);
return row.hasValue() && column.hasValue();
}
}
return false;
};
using namespace ComTypes::Constants;
switch (pId)
{
case UIA_WindowPatternId:
{
if (fragmentRoot)
return new UIAWindowProvider (this);
break;
}
case UIA_TransformPatternId:
{
if (fragmentRoot)
return new UIATransformProvider (this);
break;
}
case UIA_TextPatternId:
case UIA_TextPattern2Id:
{
if (accessibilityHandler.getTextInterface() != nullptr)
return new UIATextProvider (this);
break;
}
case UIA_ValuePatternId:
{
if (accessibilityHandler.getValueInterface() != nullptr)
return new UIAValueProvider (this);
break;
}
case UIA_RangeValuePatternId:
{
if (accessibilityHandler.getValueInterface() != nullptr
&& accessibilityHandler.getValueInterface()->getRange().isValid())
{
return new UIARangeValueProvider (this);
}
break;
}
case UIA_TogglePatternId:
{
if (accessibilityHandler.getCurrentState().isCheckable()
&& (accessibilityHandler.getActions().contains (AccessibilityActionType::toggle)
|| accessibilityHandler.getActions().contains (AccessibilityActionType::press)))
{
return new UIAToggleProvider (this);
}
break;
}
case UIA_SelectionPatternId:
{
if (role == AccessibilityRole::list
|| role == AccessibilityRole::popupMenu
|| role == AccessibilityRole::tree)
{
return new UIASelectionProvider (this);
}
break;
}
case UIA_SelectionItemPatternId:
{
auto state = accessibilityHandler.getCurrentState();
if (state.isSelectable() || state.isMultiSelectable() || role == AccessibilityRole::radioButton)
{
return new UIASelectionItemProvider (this);
}
break;
}
case UIA_TablePatternId:
case UIA_GridPatternId:
{
if (accessibilityHandler.getTableInterface() != nullptr
&& (pId == UIA_GridPatternId || accessibilityHandler.getRole() == AccessibilityRole::table))
return static_cast<ComTypes::IGridProvider*> (new UIAGridProvider (this));
break;
}
case UIA_TableItemPatternId:
case UIA_GridItemPatternId:
{
if (isListOrTableCell (accessibilityHandler))
return static_cast<ComTypes::IGridItemProvider*> (new UIAGridItemProvider (this));
break;
}
case UIA_InvokePatternId:
{
if (accessibilityHandler.getActions().contains (AccessibilityActionType::press))
return new UIAInvokeProvider (this);
break;
}
case UIA_ExpandCollapsePatternId:
{
if (accessibilityHandler.getActions().contains (AccessibilityActionType::showMenu)
&& accessibilityHandler.getCurrentState().isExpandable())
return new UIAExpandCollapseProvider (this);
break;
}
case UIA_ScrollPatternId:
{
if (accessibilityHandler.getTableInterface() != nullptr)
return new UIAScrollProvider (this);
break;
}
case UIA_ScrollItemPatternId:
{
if (isListOrTableCell (accessibilityHandler))
return new UIAScrollItemProvider (this);
break;
}
}
return nullptr;
}();
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
VariantHelpers::clear (pRetVal);
const auto role = accessibilityHandler.getRole();
const auto state = accessibilityHandler.getCurrentState();
const auto ignored = accessibilityHandler.isIgnored();
using namespace ComTypes::Constants;
switch (propertyId)
{
case UIA_AutomationIdPropertyId:
VariantHelpers::setString (getAutomationId (accessibilityHandler), pRetVal);
break;
case UIA_ControlTypePropertyId:
VariantHelpers::setInt (roleToControlTypeId (role), pRetVal);
break;
case UIA_FrameworkIdPropertyId:
VariantHelpers::setString ("JUCE", pRetVal);
break;
case UIA_FullDescriptionPropertyId:
VariantHelpers::setString (accessibilityHandler.getDescription(), pRetVal);
break;
case UIA_HelpTextPropertyId:
VariantHelpers::setString (accessibilityHandler.getHelp(), pRetVal);
break;
case UIA_IsContentElementPropertyId:
VariantHelpers::setBool (! ignored && accessibilityHandler.isVisibleWithinParent(),
pRetVal);
break;
case UIA_IsControlElementPropertyId:
VariantHelpers::setBool (true, pRetVal);
break;
case UIA_IsDialogPropertyId:
VariantHelpers::setBool (role == AccessibilityRole::dialogWindow, pRetVal);
break;
case UIA_IsEnabledPropertyId:
VariantHelpers::setBool (accessibilityHandler.getComponent().isEnabled(), pRetVal);
break;
case UIA_IsKeyboardFocusablePropertyId:
VariantHelpers::setBool (state.isFocusable(), pRetVal);
break;
case UIA_HasKeyboardFocusPropertyId:
VariantHelpers::setBool (accessibilityHandler.hasFocus (true), pRetVal);
break;
case UIA_IsOffscreenPropertyId:
VariantHelpers::setBool (! accessibilityHandler.isVisibleWithinParent(), pRetVal);
break;
case UIA_IsPasswordPropertyId:
if (auto* textInterface = accessibilityHandler.getTextInterface())
VariantHelpers::setBool (textInterface->isDisplayingProtectedText(), pRetVal);
break;
case UIA_IsPeripheralPropertyId:
VariantHelpers::setBool (role == AccessibilityRole::tooltip
|| role == AccessibilityRole::popupMenu
|| role == AccessibilityRole::splashScreen,
pRetVal);
break;
case UIA_NamePropertyId:
if (! ignored)
VariantHelpers::setString (getElementName(), pRetVal);
break;
case UIA_ProcessIdPropertyId:
VariantHelpers::setInt ((int) GetCurrentProcessId(), pRetVal);
break;
case UIA_NativeWindowHandlePropertyId:
if (isFragmentRoot())
VariantHelpers::setInt ((int) (pointer_sized_int) accessibilityHandler.getComponent().getWindowHandle(), pRetVal);
break;
}
return S_OK;
});
}
//==============================================================================
JUCE_COMRESULT AccessibilityNativeHandle::Navigate (ComTypes::NavigateDirection direction, ComTypes::IRawElementProviderFragment** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto* handler = [&]() -> AccessibilityHandler*
{
if (direction == ComTypes::NavigateDirection_Parent)
return accessibilityHandler.getParent();
if (direction == ComTypes::NavigateDirection_FirstChild
|| direction == ComTypes::NavigateDirection_LastChild)
{
auto children = accessibilityHandler.getChildren();
return children.empty() ? nullptr
: (direction == ComTypes::NavigateDirection_FirstChild ? children.front()
: children.back());
}
if (direction == ComTypes::NavigateDirection_NextSibling
|| direction == ComTypes::NavigateDirection_PreviousSibling)
{
if (auto* parent = accessibilityHandler.getParent())
{
const auto siblings = parent->getChildren();
const auto iter = std::find (siblings.cbegin(), siblings.cend(), &accessibilityHandler);
if (iter == siblings.end())
return nullptr;
if (direction == ComTypes::NavigateDirection_NextSibling && iter != std::prev (siblings.cend()))
return *std::next (iter);
if (direction == ComTypes::NavigateDirection_PreviousSibling && iter != siblings.cbegin())
return *std::prev (iter);
}
}
return nullptr;
}();
if (handler != nullptr)
if (auto* provider = handler->getNativeImplementation())
if (provider->isElementValid())
provider->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::GetRuntimeId (SAFEARRAY** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
if (! isFragmentRoot())
{
*pRetVal = SafeArrayCreateVector (VT_I4, 0, 2);
if (*pRetVal == nullptr)
return E_OUTOFMEMORY;
for (LONG i = 0; i < 2; ++i)
{
auto hr = SafeArrayPutElement (*pRetVal, &i, &rtid[(size_t) i]);
if (FAILED (hr))
return E_FAIL;
}
}
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::get_BoundingRectangle (ComTypes::UiaRect* pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto bounds = Desktop::getInstance().getDisplays()
.logicalToPhysical (accessibilityHandler.getComponent().getScreenBounds());
pRetVal->left = bounds.getX();
pRetVal->top = bounds.getY();
pRetVal->width = bounds.getWidth();
pRetVal->height = bounds.getHeight();
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, []
{
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::SetFocus()
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const WeakReference<Component> safeComponent (&accessibilityHandler.getComponent());
accessibilityHandler.getActions().invoke (AccessibilityActionType::focus);
if (safeComponent != nullptr)
accessibilityHandler.grabFocus();
return S_OK;
}
JUCE_COMRESULT AccessibilityNativeHandle::get_FragmentRoot (ComTypes::IRawElementProviderFragmentRoot** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
auto* handler = [&]() -> AccessibilityHandler*
{
if (isFragmentRoot())
return &accessibilityHandler;
if (auto* peer = accessibilityHandler.getComponent().getPeer())
return peer->getComponent().getAccessibilityHandler();
return nullptr;
}();
if (handler != nullptr)
{
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
}
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
});
}
//==============================================================================
JUCE_COMRESULT AccessibilityNativeHandle::ElementProviderFromPoint (double x, double y, ComTypes::IRawElementProviderFragment** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto* handler = [&]
{
auto logicalScreenPoint = Desktop::getInstance().getDisplays()
.physicalToLogical (Point<int> (roundToInt (x),
roundToInt (y)));
if (auto* child = accessibilityHandler.getChildAt (logicalScreenPoint))
return child;
return &accessibilityHandler;
}();
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::GetFocus (ComTypes::IRawElementProviderFragment** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
const auto getFocusHandler = [this]() -> AccessibilityHandler*
{
if (auto* modal = Component::getCurrentlyModalComponent())
{
const auto& component = accessibilityHandler.getComponent();
if (! component.isParentOf (modal)
&& component.isCurrentlyBlockedByAnotherModalComponent())
{
if (auto* modalHandler = modal->getAccessibilityHandler())
{
if (auto* focusChild = modalHandler->getChildFocus())
return focusChild;
return modalHandler;
}
}
}
if (auto* focusChild = accessibilityHandler.getChildFocus())
return focusChild;
return nullptr;
};
if (auto* focusHandler = getFocusHandler())
focusHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT AccessibilityNativeHandle::GetOverrideProviderForHwnd (HWND hwnd, IRawElementProviderSimple** pRetVal)
{
return withCheckedComArgs (pRetVal, *this, [&]
{
if (auto* component = AccessibilityHandler::getComponentForNativeChild (hwnd))
if (auto* handler = component->getAccessibilityHandler())
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
//==============================================================================
String AccessibilityNativeHandle::getElementName() const
{
if (accessibilityHandler.getRole() == AccessibilityRole::tooltip)
return accessibilityHandler.getDescription();
auto name = accessibilityHandler.getTitle();
if (name.isEmpty() && isFragmentRoot())
return detail::AccessibilityHelpers::getApplicationOrPluginName();
return name;
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
} // namespace juce
@@ -0,0 +1,80 @@
/*
==============================================================================
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
{
class AccessibilityNativeHandle : public ComBaseClassHelper<IRawElementProviderSimple,
ComTypes::IRawElementProviderFragment,
ComTypes::IRawElementProviderFragmentRoot,
ComTypes::IRawElementProviderHwndOverride>
{
public:
explicit AccessibilityNativeHandle (AccessibilityHandler& handler);
//==============================================================================
void invalidateElement() noexcept { valid = false; }
bool isElementValid() const noexcept { return valid; }
const AccessibilityHandler& getHandler() { return accessibilityHandler; }
//==============================================================================
JUCE_COMRESULT QueryInterface (REFIID refId, void** result) override;
//==============================================================================
JUCE_COMRESULT get_HostRawElementProvider (IRawElementProviderSimple** provider) override;
JUCE_COMRESULT get_ProviderOptions (ProviderOptions* options) override;
JUCE_COMRESULT GetPatternProvider (PATTERNID pId, IUnknown** provider) override;
JUCE_COMRESULT GetPropertyValue (PROPERTYID propertyId, VARIANT* pRetVal) override;
JUCE_COMRESULT Navigate (ComTypes::NavigateDirection direction, ComTypes::IRawElementProviderFragment** pRetVal) override;
JUCE_COMRESULT GetRuntimeId (SAFEARRAY** pRetVal) override;
JUCE_COMRESULT get_BoundingRectangle (ComTypes::UiaRect* pRetVal) override;
JUCE_COMRESULT GetEmbeddedFragmentRoots (SAFEARRAY** pRetVal) override;
JUCE_COMRESULT SetFocus() override;
JUCE_COMRESULT get_FragmentRoot (ComTypes::IRawElementProviderFragmentRoot** pRetVal) override;
JUCE_COMRESULT ElementProviderFromPoint (double x, double y, ComTypes::IRawElementProviderFragment** pRetVal) override;
JUCE_COMRESULT GetFocus (ComTypes::IRawElementProviderFragment** pRetVal) override;
JUCE_COMRESULT GetOverrideProviderForHwnd (HWND hwnd, IRawElementProviderSimple** pRetVal) override;
private:
//==============================================================================
String getElementName() const;
bool isFragmentRoot() const { return accessibilityHandler.getComponent().isOnDesktop(); }
//==============================================================================
AccessibilityHandler& accessibilityHandler;
static int idCounter;
std::array<int, 2> rtid { UiaAppendRuntimeId, ++idCounter };
bool valid = true;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeHandle)
};
}
@@ -0,0 +1,274 @@
/*
==============================================================================
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
{
//==============================================================================
struct AccessibleObjCClassDeleter
{
template <typename ElementType>
void operator() (ElementType* element) const
{
juceFreeAccessibilityPlatformSpecificData (element);
object_setInstanceVariable (element, "handler", nullptr);
[element release];
}
};
template <typename Base>
class AccessibleObjCClass : public ObjCClass<Base>
{
public:
using Holder = std::unique_ptr<Base, AccessibleObjCClassDeleter>;
protected:
AccessibleObjCClass() : AccessibleObjCClass ("JUCEAccessibilityElement_") {}
explicit AccessibleObjCClass (const char* name) : ObjCClass<Base> (name)
{
ObjCClass<Base>::template addIvar<AccessibilityHandler*> ("handler");
}
//==============================================================================
static AccessibilityHandler* getHandler (id self)
{
return getIvar<AccessibilityHandler*> (self, "handler");
}
template <typename MemberFn>
static auto getInterface (id self, MemberFn fn) noexcept -> decltype ((std::declval<AccessibilityHandler>().*fn)())
{
if (auto* handler = getHandler (self))
return (handler->*fn)();
return nullptr;
}
static AccessibilityTextInterface* getTextInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTextInterface); }
static AccessibilityValueInterface* getValueInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getValueInterface); }
static AccessibilityTableInterface* getTableInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getTableInterface); }
static AccessibilityCellInterface* getCellInterface (id self) noexcept { return getInterface (self, &AccessibilityHandler::getCellInterface); }
static bool hasEditableText (AccessibilityHandler& handler) noexcept
{
return handler.getRole() == AccessibilityRole::editableText
&& handler.getTextInterface() != nullptr
&& ! handler.getTextInterface()->isReadOnly();
}
static id getAccessibilityValueFromInterfaces (const AccessibilityHandler& handler)
{
if (auto* textInterface = handler.getTextInterface())
return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }));
if (auto* valueInterface = handler.getValueInterface())
return juceStringToNS (valueInterface->getCurrentValueAsString());
return nil;
}
//==============================================================================
static BOOL getIsAccessibilityElement (id self, SEL)
{
if (auto* handler = getHandler (self))
return ! handler->isIgnored() && handler->getRole() != AccessibilityRole::window;
return NO;
}
static void setAccessibilityValue (id self, SEL, NSString* value)
{
if (auto* handler = getHandler (self))
{
if (hasEditableText (*handler))
{
handler->getTextInterface()->setText (nsStringToJuce (value));
return;
}
if (auto* valueInterface = handler->getValueInterface())
if (! valueInterface->isReadOnly())
valueInterface->setValueAsString (nsStringToJuce (value));
}
}
static BOOL performActionIfSupported (id self, AccessibilityActionType actionType)
{
if (auto* handler = getHandler (self))
if (handler->getActions().invoke (actionType))
return YES;
return NO;
}
static BOOL accessibilityPerformPress (id self, SEL)
{
if (auto* handler = getHandler (self))
if (handler->getCurrentState().isCheckable() && handler->getActions().invoke (AccessibilityActionType::toggle))
return YES;
return performActionIfSupported (self, AccessibilityActionType::press);
}
static BOOL accessibilityPerformIncrement (id self, SEL)
{
if (auto* valueInterface = getValueInterface (self))
{
if (! valueInterface->isReadOnly())
{
auto range = valueInterface->getRange();
if (range.isValid())
{
valueInterface->setValue (jlimit (range.getMinimumValue(),
range.getMaximumValue(),
valueInterface->getCurrentValue() + range.getInterval()));
return YES;
}
}
}
return NO;
}
static BOOL accessibilityPerformDecrement (id self, SEL)
{
if (auto* valueInterface = getValueInterface (self))
{
if (! valueInterface->isReadOnly())
{
auto range = valueInterface->getRange();
if (range.isValid())
{
valueInterface->setValue (jlimit (range.getMinimumValue(),
range.getMaximumValue(),
valueInterface->getCurrentValue() - range.getInterval()));
return YES;
}
}
}
return NO;
}
static NSString* getAccessibilityTitle (id self, SEL)
{
if (auto* handler = getHandler (self))
{
auto title = handler->getTitle();
if (title.isEmpty() && handler->getComponent().isOnDesktop())
title = detail::AccessibilityHelpers::getApplicationOrPluginName();
NSString* nsString = juceStringToNS (title);
if (nsString != nil && [[self accessibilityValue] isEqual: nsString])
return @"";
return nsString;
}
return nil;
}
static NSString* getAccessibilityHelp (id self, SEL)
{
if (auto* handler = getHandler (self))
return juceStringToNS (handler->getHelp());
return nil;
}
static BOOL getIsAccessibilityModal (id self, SEL)
{
if (auto* handler = getHandler (self))
return handler->getComponent().isCurrentlyModal();
return NO;
}
static NSInteger getAccessibilityRowCount (id self, SEL)
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface))
if (auto* tableInterface = tableHandler->getTableInterface())
return tableInterface->getNumRows();
return 0;
}
static NSInteger getAccessibilityColumnCount (id self, SEL)
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface))
if (auto* tableInterface = tableHandler->getTableInterface())
return tableInterface->getNumColumns();
return 0;
}
template <typename Getter>
static NSRange getCellDimensions (id self, Getter getter)
{
const auto notFound = NSMakeRange (NSNotFound, 0);
auto* handler = getHandler (self);
if (handler == nullptr)
return notFound;
auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface);
if (tableHandler == nullptr)
return notFound;
auto* tableInterface = tableHandler->getTableInterface();
if (tableInterface == nullptr)
return notFound;
const auto result = (tableInterface->*getter) (*handler);
if (! result.hasValue())
return notFound;
return NSMakeRange ((NSUInteger) result->begin, (NSUInteger) result->num);
}
static NSRange getAccessibilityRowIndexRange (id self, SEL)
{
return getCellDimensions (self, &AccessibilityTableInterface::getRowSpan);
}
static NSRange getAccessibilityColumnIndexRange (id self, SEL)
{
return getCellDimensions (self, &AccessibilityTableInterface::getColumnSpan);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibleObjCClass)
};
} // namespace juce
@@ -0,0 +1,302 @@
/*
==============================================================================
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
{
struct AccessibilityTextHelpers
{
/* Wraps a CharPtr into a stdlib-compatible iterator.
MSVC's std::reverse_iterator requires the wrapped iterator to be default constructible
when building in C++20 mode, but I don't really want to add public default constructors to
the CharPtr types. Instead, we add a very basic default constructor here which sets the
wrapped CharPtr to nullptr.
*/
template <typename CharPtr>
class CharPtrIteratorAdapter
{
public:
using difference_type = int;
using value_type = decltype (*std::declval<CharPtr>());
using pointer = value_type*;
using reference = value_type;
using iterator_category = std::bidirectional_iterator_tag;
CharPtrIteratorAdapter() = default;
constexpr explicit CharPtrIteratorAdapter (CharPtr arg) : ptr (arg) {}
constexpr auto operator*() const { return *ptr; }
constexpr CharPtrIteratorAdapter& operator++()
{
++ptr;
return *this;
}
constexpr CharPtrIteratorAdapter& operator--()
{
--ptr;
return *this;
}
constexpr bool operator== (const CharPtrIteratorAdapter& other) const { return ptr == other.ptr; }
constexpr bool operator!= (const CharPtrIteratorAdapter& other) const { return ptr != other.ptr; }
constexpr auto operator+ (difference_type offset) const { return CharPtrIteratorAdapter { ptr + offset }; }
constexpr auto operator- (difference_type offset) const { return CharPtrIteratorAdapter { ptr - offset }; }
private:
CharPtr ptr { {} };
};
template <typename CharPtr>
static auto makeCharPtrIteratorAdapter (CharPtr ptr)
{
return CharPtrIteratorAdapter<CharPtr> { ptr };
}
enum class BoundaryType
{
character,
word,
line,
document
};
enum class Direction
{
forwards,
backwards
};
enum class ExtendSelection
{
no,
yes
};
/* Indicates whether a function may return the current text position, in the case that the
position already falls on a text unit boundary.
*/
enum class IncludeThisBoundary
{
no, //< Always search for the following boundary, even if the current position falls on a boundary
yes //< Return the current position if it falls on a boundary
};
/* Indicates whether a word boundary should include any whitespaces that follow the
non-whitespace characters.
*/
enum class IncludeWhitespaceAfterWords
{
no, //< The word ends on the first whitespace character
yes //< The word ends after the last whitespace character
};
/* Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't
require the iterators to have any member type aliases.
*/
template <typename Iter>
static int countDifference (Iter from, Iter to)
{
int distance = 0;
while (from != to)
{
++from;
++distance;
}
return distance;
}
/* Returns the number of characters between ptr and the next word end in a specific
direction.
If ptr is inside a word, the result will be the distance to the end of the same
word.
*/
template <typename CharPtr>
static int findNextWordEndOffset (CharPtr beginIn,
CharPtr endIn,
CharPtr ptrIn,
Direction direction,
IncludeThisBoundary includeBoundary,
IncludeWhitespaceAfterWords includeWhitespace)
{
const auto begin = makeCharPtrIteratorAdapter (beginIn);
const auto end = makeCharPtrIteratorAdapter (endIn);
const auto ptr = makeCharPtrIteratorAdapter (ptrIn);
const auto move = [&] (auto b, auto e, auto iter)
{
const auto isSpace = [] (juce_wchar c) { return CharacterFunctions::isWhitespace (c); };
const auto start = [&]
{
if (iter == b && includeBoundary == IncludeThisBoundary::yes)
return b;
const auto nudged = iter - (iter != b && includeBoundary == IncludeThisBoundary::yes ? 1 : 0);
return includeWhitespace == IncludeWhitespaceAfterWords::yes
? std::find_if (nudged, e, isSpace)
: std::find_if_not (nudged, e, isSpace);
}();
const auto found = includeWhitespace == IncludeWhitespaceAfterWords::yes
? std::find_if_not (start, e, isSpace)
: std::find_if (start, e, isSpace);
return countDifference (iter, found);
};
return direction == Direction::forwards ? move (begin, end, ptr)
: -move (std::make_reverse_iterator (end),
std::make_reverse_iterator (begin),
std::make_reverse_iterator (ptr));
}
/* Returns the number of characters between ptr and the beginning of the next line in a
specific direction.
*/
template <typename CharPtr>
static int findNextLineOffset (CharPtr beginIn,
CharPtr endIn,
CharPtr ptrIn,
Direction direction,
IncludeThisBoundary includeBoundary)
{
const auto begin = makeCharPtrIteratorAdapter (beginIn);
const auto end = makeCharPtrIteratorAdapter (endIn);
const auto ptr = makeCharPtrIteratorAdapter (ptrIn);
const auto findNewline = [] (auto from, auto to) { return std::find (from, to, juce_wchar { '\n' }); };
if (direction == Direction::forwards)
{
if (ptr != begin && includeBoundary == IncludeThisBoundary::yes && *(ptr - 1) == '\n')
return 0;
const auto newline = findNewline (ptr, end);
return countDifference (ptr, newline) + (newline == end ? 0 : 1);
}
const auto rbegin = std::make_reverse_iterator (ptr);
const auto rend = std::make_reverse_iterator (begin);
return -countDifference (rbegin, findNewline (rbegin + (rbegin == rend || includeBoundary == IncludeThisBoundary::yes ? 0 : 1), rend));
}
/* Unfortunately, the method of computing end-points of text units depends on context, and on
the current platform.
Some examples of different behaviour:
- On Android, updating the cursor/selection always searches for the next text unit boundary;
but on Windows, ExpandToEnclosingUnit() should not move the starting point of the
selection if it already at a unit boundary. This means that we need both inclusive and
exclusive methods for finding the next text boundary.
- On Android, moving the cursor by 'words' should move to the first space following a
non-space character in the requested direction. On Windows, a 'word' includes trailing
whitespace, but not preceding whitespace. This means that we need a way of specifying
whether whitespace should be included when navigating by words.
*/
static int findTextBoundary (const AccessibilityTextInterface& textInterface,
int currentPosition,
BoundaryType boundary,
Direction direction,
IncludeThisBoundary includeBoundary,
IncludeWhitespaceAfterWords includeWhitespace)
{
const auto numCharacters = textInterface.getTotalNumCharacters();
const auto isForwards = (direction == Direction::forwards);
const auto currentClamped = jlimit (0, numCharacters, currentPosition);
switch (boundary)
{
case BoundaryType::character:
{
const auto offset = includeBoundary == IncludeThisBoundary::yes ? 0
: (isForwards ? 1 : -1);
return jlimit (0, numCharacters, currentPosition + offset);
}
case BoundaryType::word:
{
const auto str = textInterface.getText ({ 0, numCharacters });
return currentClamped + findNextWordEndOffset (str.begin(),
str.end(),
str.begin() + currentClamped,
direction,
includeBoundary,
includeWhitespace);
}
case BoundaryType::line:
{
const auto str = textInterface.getText ({ 0, numCharacters });
return currentClamped + findNextLineOffset (str.begin(),
str.end(),
str.begin() + currentClamped,
direction,
includeBoundary);
}
case BoundaryType::document:
return isForwards ? numCharacters : 0;
}
jassertfalse;
return -1;
}
/* Adjusts the current text selection range, using an algorithm appropriate for cursor movement
on Android.
*/
static Range<int> findNewSelectionRangeAndroid (const AccessibilityTextInterface& textInterface,
BoundaryType boundaryType,
ExtendSelection extend,
Direction direction)
{
const auto oldPos = textInterface.getTextInsertionOffset();
const auto cursorPos = findTextBoundary (textInterface,
oldPos,
boundaryType,
direction,
IncludeThisBoundary::no,
IncludeWhitespaceAfterWords::no);
if (extend == ExtendSelection::no)
return { cursorPos, cursorPos };
const auto currentSelection = textInterface.getSelection();
const auto start = currentSelection.getStart();
const auto end = currentSelection.getEnd();
return Range<int>::between (cursorPos, oldPos == start ? end : start);
}
};
} // namespace juce
@@ -0,0 +1,155 @@
/*
==============================================================================
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
{
struct AccessibilityTextHelpersTest final : public UnitTest
{
AccessibilityTextHelpersTest()
: UnitTest ("AccessibilityTextHelpers", UnitTestCategories::gui) {}
void runTest() override
{
using ATH = AccessibilityTextHelpers;
beginTest ("Android find word end");
{
const auto testMultiple = [this] (String str,
int start,
const std::vector<int>& collection)
{
auto it = collection.begin();
for (const auto direction : { ATH::Direction::forwards, ATH::Direction::backwards })
{
for (const auto includeBoundary : { ATH::IncludeThisBoundary::no, ATH::IncludeThisBoundary::yes })
{
for (const auto includeWhitespace : { ATH::IncludeWhitespaceAfterWords::no, ATH::IncludeWhitespaceAfterWords::yes })
{
const auto actual = ATH::findNextWordEndOffset (str.begin(), str.end(), str.begin() + start, direction, includeBoundary, includeWhitespace);
const auto expected = *it++;
expect (expected == actual);
}
}
}
};
// Character Indices 0 3 56 13 50 51
// | | || | | |
const auto string = String ("hello world \r\n with some spaces in this sentence ") + String (CharPointer_UTF8 ("\xe2\x88\xae E\xe2\x8b\x85""da = Q"));
// Direction forwards forwards forwards forwards backwards backwards backwards backwards
// IncludeBoundary no no yes yes no no yes yes
// IncludeWhitespace no yes no yes no yes no yes
testMultiple (string, 0, { 5, 6, 5, 0, 0, 0, 0, 0 });
testMultiple (string, 3, { 2, 3, 2, 3, -3, -3, -3, -3 });
testMultiple (string, 5, { 6, 1, 0, 1, -5, -5, -5, 0 });
testMultiple (string, 6, { 5, 9, 5, 0, -6, -1, 0, -1 });
testMultiple (string, 13, { 6, 2, 6, 2, -7, -2, -7, -2 });
testMultiple (string, 50, { 1, 2, 1, 0, -9, -1, 0, -1 });
testMultiple (string, 51, { 5, 1, 0, 1, -1, -2, -1, 0 });
testMultiple (" a b ", 0, { 3, 2, 0, 2, 0, 0, 0, 0 });
testMultiple (" a b ", 1, { 2, 1, 2, 1, -1, -1, -1, -1 });
}
beginTest ("Android text range adjustment");
{
const auto testMultiple = [this] (String str,
Range<int> initial,
auto boundary,
const std::vector<Range<int>>& collection)
{
auto it = collection.begin();
for (auto extend : { ATH::ExtendSelection::no, ATH::ExtendSelection::yes })
{
for (auto direction : { ATH::Direction::forwards, ATH::Direction::backwards })
{
for (auto insert : { CursorPosition::begin, CursorPosition::end })
{
const MockAccessibilityTextInterface mock { str, initial, insert };
const auto actual = ATH::findNewSelectionRangeAndroid (mock, boundary, extend, direction);
const auto expected = *it++;
expect (expected == actual);
}
}
}
};
// Extend no no no no yes yes yes yes
// Direction forwards forwards backwards backwards forwards forwards backwards backwards
// Insert begin end begin end begin end begin end
testMultiple ("hello world", { 5, 5 }, ATH::BoundaryType::character, { { 6, 6 }, { 6, 6 }, { 4, 4 }, { 4, 4 }, { 5, 6 }, { 5, 6 }, { 4, 5 }, { 4, 5 } });
testMultiple ("hello world", { 0, 0 }, ATH::BoundaryType::character, { { 1, 1 }, { 1, 1 }, { 0, 0 }, { 0, 0 }, { 0, 1 }, { 0, 1 }, { 0, 0 }, { 0, 0 } });
testMultiple ("hello world", { 11, 11 }, ATH::BoundaryType::character, { { 11, 11 }, { 11, 11 }, { 10, 10 }, { 10, 10 }, { 11, 11 }, { 11, 11 }, { 10, 11 }, { 10, 11 } });
testMultiple ("hello world", { 4, 5 }, ATH::BoundaryType::character, { { 5, 5 }, { 6, 6 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 4, 6 }, { 3, 5 }, { 4, 4 } });
testMultiple ("hello world", { 0, 1 }, ATH::BoundaryType::character, { { 1, 1 }, { 2, 2 }, { 0, 0 }, { 0, 0 }, { 1, 1 }, { 0, 2 }, { 0, 1 }, { 0, 0 } });
testMultiple ("hello world", { 10, 11 }, ATH::BoundaryType::character, { { 11, 11 }, { 11, 11 }, { 9, 9 }, { 10, 10 }, { 11, 11 }, { 10, 11 }, { 9, 11 }, { 10, 10 } });
testMultiple ("foo bar baz", { 0, 0 }, ATH::BoundaryType::word, { { 3, 3 }, { 3, 3 }, { 0, 0 }, { 0, 0 }, { 0, 3 }, { 0, 3 }, { 0, 0 }, { 0, 0 } });
testMultiple ("foo bar baz", { 1, 6 }, ATH::BoundaryType::word, { { 3, 3 }, { 8, 8 }, { 0, 0 }, { 5, 5 }, { 3, 6 }, { 1, 8 }, { 0, 6 }, { 1, 5 } });
testMultiple ("foo bar baz", { 3, 3 }, ATH::BoundaryType::word, { { 8, 8 }, { 8, 8 }, { 0, 0 }, { 0, 0 }, { 3, 8 }, { 3, 8 }, { 0, 3 }, { 0, 3 } });
testMultiple ("foo bar baz", { 3, 5 }, ATH::BoundaryType::word, { { 8, 8 }, { 8, 8 }, { 0, 0 }, { 0, 0 }, { 5, 8 }, { 3, 8 }, { 0, 5 }, { 0, 3 } });
testMultiple ("foo bar\n\n\na b\nc d e", { 0, 0 }, ATH::BoundaryType::line, { { 8, 8 }, { 8, 8 }, { 0, 0 }, { 0, 0 }, { 0, 8 }, { 0, 8 }, { 0, 0 }, { 0, 0 } });
testMultiple ("foo bar\n\n\na b\nc d e", { 7, 7 }, ATH::BoundaryType::line, { { 8, 8 }, { 8, 8 }, { 0, 0 }, { 0, 0 }, { 7, 8 }, { 7, 8 }, { 0, 7 }, { 0, 7 } });
testMultiple ("foo bar\n\n\na b\nc d e", { 8, 8 }, ATH::BoundaryType::line, { { 9, 9 }, { 9, 9 }, { 0, 0 }, { 0, 0 }, { 8, 9 }, { 8, 9 }, { 0, 8 }, { 0, 8 } });
testMultiple ("foo bar\r\na b\r\nxyz", { 0, 0 }, ATH::BoundaryType::line, { { 9, 9 }, { 9, 9 }, { 0, 0 }, { 0, 0 }, { 0, 9 }, { 0, 9 }, { 0, 0 }, { 0, 0 } });
testMultiple ("foo bar\r\na b\r\nxyz", { 10, 10 }, ATH::BoundaryType::line, { { 14, 14 }, { 14, 14 }, { 9, 9 }, { 9, 9 }, { 10, 14 }, { 10, 14 }, { 9, 10 }, { 9, 10 } });
}
}
enum class CursorPosition { begin, end };
class MockAccessibilityTextInterface final : public AccessibilityTextInterface
{
public:
MockAccessibilityTextInterface (String stringIn, Range<int> selectionIn, CursorPosition insertIn)
: string (stringIn), selection (selectionIn), insert (insertIn) {}
bool isDisplayingProtectedText() const override { return false; }
bool isReadOnly() const override { return false; }
int getTotalNumCharacters() const override { return string.length(); }
Range<int> getSelection() const override { return selection; }
int getTextInsertionOffset() const override { return insert == CursorPosition::begin ? selection.getStart() : selection.getEnd(); }
String getText (Range<int> range) const override { return string.substring (range.getStart(), range.getEnd()); }
RectangleList<int> getTextBounds (Range<int>) const override { return {}; }
int getOffsetAtPoint (Point<int>) const override { return 0; }
void setSelection (Range<int> newRange) override { selection = newRange; }
void setText (const String& newText) override { string = newText; }
private:
String string;
Range<int> selection;
CursorPosition insert;
};
};
static AccessibilityTextHelpersTest accessibilityTextHelpersTest;
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,677 @@
/*
==============================================================================
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.
==============================================================================
*/
static void juceFreeAccessibilityPlatformSpecificData (UIAccessibilityElement* element)
{
if (auto* container = juce::getIvar<UIAccessibilityElement*> (element, "container"))
{
object_setInstanceVariable (element, "container", nullptr);
object_setInstanceVariable (container, "handler", nullptr);
[container release];
}
}
namespace juce
{
#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
template <typename> struct Signature {};
template <typename Result, typename... Args>
struct Signature<Result (Args...)> {};
// @selector isn't constexpr, so the 'sel' members are functions rather than static constexpr data members
struct SignatureHasText final : public Signature<BOOL()> { static auto sel() { return @selector (hasText); } };
struct SignatureSetSelectedTextRange final : public Signature<void (UITextRange*)> { static auto sel() { return @selector (setSelectedTextRange:); } };
struct SignatureSelectedTextRange final : public Signature<UITextRange*()> { static auto sel() { return @selector (selectedTextRange); } };
struct SignatureMarkedTextRange final : public Signature<UITextRange*()> { static auto sel() { return @selector (markedTextRange); } };
struct SignatureSetMarkedTextSelectedRange final : public Signature<void (NSString*, NSRange)> { static auto sel() { return @selector (setMarkedText:selectedRange:); } };
struct SignatureUnmarkText final : public Signature<void()> { static auto sel() { return @selector (unmarkText); } };
struct SignatureMarkedTextStyle final : public Signature<NSDictionary<NSAttributedStringKey, id>*()> { static auto sel() { return @selector (markedTextStyle); } };
struct SignatureSetMarkedTextStyle final : public Signature<void (NSDictionary<NSAttributedStringKey, id>*)> { static auto sel() { return @selector (setMarkedTextStyle:); } };
struct SignatureBeginningOfDocument final : public Signature<UITextPosition*()> { static auto sel() { return @selector (beginningOfDocument); } };
struct SignatureEndOfDocument final : public Signature<UITextPosition*()> { static auto sel() { return @selector (endOfDocument); } };
struct SignatureTokenizer final : public Signature<id<UITextInputTokenizer>()> { static auto sel() { return @selector (tokenizer); } };
struct SignatureBaseWritingDirection final : public Signature<NSWritingDirection (UITextPosition*, UITextStorageDirection)> { static auto sel() { return @selector (baseWritingDirectionForPosition:inDirection:); } };
struct SignatureCaretRectForPosition final : public Signature<CGRect (UITextPosition*)> { static auto sel() { return @selector (caretRectForPosition:); } };
struct SignatureCharacterRangeByExtending final : public Signature<UITextRange* (UITextPosition*, UITextLayoutDirection)> { static auto sel() { return @selector (characterRangeByExtendingPosition:inDirection:); } };
struct SignatureCharacterRangeAtPoint final : public Signature<UITextRange* (CGPoint)> { static auto sel() { return @selector (characterRangeAtPoint:); } };
struct SignatureClosestPositionToPoint final : public Signature<UITextPosition* (CGPoint)> { static auto sel() { return @selector (closestPositionToPoint:); } };
struct SignatureClosestPositionToPointInRange final : public Signature<UITextPosition* (CGPoint, UITextRange*)> { static auto sel() { return @selector (closestPositionToPoint:withinRange:); } };
struct SignatureComparePositionToPosition final : public Signature<NSComparisonResult (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (comparePosition:toPosition:); } };
struct SignatureOffsetFromPositionToPosition final : public Signature<NSInteger (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (offsetFromPosition:toPosition:); } };
struct SignaturePositionFromPositionInDirection final : public Signature<UITextPosition* (UITextPosition*, UITextLayoutDirection, NSInteger)> { static auto sel() { return @selector (positionFromPosition:inDirection:offset:); } };
struct SignaturePositionFromPositionOffset final : public Signature<UITextPosition* (UITextPosition*, NSInteger)> { static auto sel() { return @selector (positionFromPosition:offset:); } };
struct SignatureFirstRectForRange final : public Signature<CGRect (UITextRange*)> { static auto sel() { return @selector (firstRectForRange:); } };
struct SignatureSelectionRectsForRange final : public Signature<NSArray<UITextSelectionRect*>* (UITextRange*)> { static auto sel() { return @selector (selectionRectsForRange:); } };
struct SignaturePositionWithinRange final : public Signature<UITextPosition* (UITextRange*, UITextLayoutDirection)> { static auto sel() { return @selector (positionWithinRange:farthestInDirection:); } };
struct SignatureReplaceRangeWithText final : public Signature<void (UITextRange*, NSString*)> { static auto sel() { return @selector (replaceRange:withText:); } };
struct SignatureSetBaseWritingDirection final : public Signature<void (NSWritingDirection, UITextRange*)> { static auto sel() { return @selector (setBaseWritingDirection:forRange:); } };
struct SignatureTextInRange final : public Signature<NSString* (UITextRange*)> { static auto sel() { return @selector (textInRange:); } };
struct SignatureTextRangeFromPosition final : public Signature<UITextRange* (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (textRangeFromPosition:toPosition:); } };
struct SignatureSetInputDelegate final : public Signature<void (id)> { static auto sel() { return @selector (setInputDelegate:); } };
struct SignatureInputDelegate final : public Signature<id()> { static auto sel() { return @selector (inputDelegate); } };
struct SignatureKeyboardType final : public Signature<UIKeyboardType()> { static auto sel() { return @selector (keyboardType); } };
struct SignatureAutocapitalizationType final : public Signature<UITextAutocapitalizationType()> { static auto sel() { return @selector (autocapitalizationType); } };
struct SignatureAutocorrectionType final : public Signature<UITextAutocorrectionType()> { static auto sel() { return @selector (autocorrectionType); } };
//==============================================================================
class AccessibilityHandler::AccessibilityNativeImpl
{
public:
explicit AccessibilityNativeImpl (AccessibilityHandler& handler)
: accessibilityElement (AccessibilityElement::create (handler))
{
}
UIAccessibilityElement* getAccessibilityElement() const noexcept
{
return accessibilityElement.get();
}
private:
//==============================================================================
class AccessibilityContainer final : public AccessibleObjCClass<NSObject>
{
public:
AccessibilityContainer()
: AccessibleObjCClass ("JUCEUIAccessibilityContainer_")
{
addMethod (@selector (isAccessibilityElement), [] (id, SEL) { return false; });
addMethod (@selector (accessibilityFrame), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
return convertToCGRect (handler->getComponent().getScreenBounds());
return CGRectZero;
});
addMethod (@selector (accessibilityElements), [] (id self, SEL) -> NSArray*
{
if (auto* handler = getHandler (self))
return getContainerAccessibilityElements (*handler);
return nil;
});
if (@available (iOS 11.0, *))
{
addMethod (@selector (accessibilityDataTableCellElementForRow:column:), [] (id self, SEL, NSUInteger row, NSUInteger column) -> id
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface))
if (auto* tableInterface = tableHandler->getTableInterface())
if (auto* cellHandler = tableInterface->getCellHandler ((int) row, (int) column))
if (auto* parent = getAccessibleParent (cellHandler))
return static_cast<id> (parent->getNativeImplementation());
return nil;
});
addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount);
addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount);
addMethod (@selector (accessibilityHeaderElementsForColumn:), [] (id self, SEL, NSUInteger column) -> NSArray*
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (getHandler (self), &AccessibilityHandler::getTableInterface))
{
if (auto* tableInterface = tableHandler->getTableInterface())
{
if (auto* header = tableInterface->getHeaderHandler())
{
if (isPositiveAndBelow (column, header->getChildren().size()))
{
auto* result = [NSMutableArray new];
[result addObject: static_cast<id> (header->getChildren()[(size_t) column]->getNativeImplementation())];
return result;
}
}
}
}
return nullptr;
});
addProtocol (@protocol (UIAccessibilityContainerDataTable));
addMethod (@selector (accessibilityContainerType), [] (id self, SEL) -> NSInteger
{
if (auto* handler = getHandler (self))
{
if (handler->getTableInterface() != nullptr)
{
if (@available (iOS 11.0, *))
return UIAccessibilityContainerTypeDataTable;
return 1; // UIAccessibilityContainerTypeDataTable
}
const auto handlerRole = handler->getRole();
if (handlerRole == AccessibilityRole::popupMenu
|| handlerRole == AccessibilityRole::list
|| handlerRole == AccessibilityRole::tree)
{
if (@available (iOS 11.0, *))
return UIAccessibilityContainerTypeList;
return 2; // UIAccessibilityContainerTypeList
}
}
if (@available (iOS 11.0, *))
return UIAccessibilityContainerTypeNone;
return 0; // UIAccessibilityContainerTypeNone
});
}
registerClass();
}
private:
static const AccessibilityHandler* getAccessibleParent (const AccessibilityHandler* h)
{
if (h == nullptr)
return nullptr;
if ([static_cast<id> (h->getNativeImplementation()) isAccessibilityElement])
return h;
return getAccessibleParent (h->getParent());
}
static AccessibilityHandler* getHandler (id self)
{
return getIvar<AccessibilityHandler*> (self, "handler");
}
};
//==============================================================================
class AccessibilityElement final : public AccessibleObjCClass<UIAccessibilityElement>
{
template <typename Func, typename... Items>
static constexpr void forEach (Func&& func, Items&&... items)
{
(func (std::forward<Items> (items)), ...);
}
public:
enum class Type { defaultElement, textElement };
static Holder create (AccessibilityHandler& handler)
{
static AccessibilityElement cls { Type::defaultElement };
static AccessibilityElement textCls { Type::textElement };
id instance = (hasEditableText (handler) ? textCls : cls).createInstance();
Holder element ([instance initWithAccessibilityContainer: static_cast<id> (handler.getComponent().getWindowHandle())]);
object_setInstanceVariable (element.get(), "handler", &handler);
return element;
}
AccessibilityElement (Type elementType)
{
addMethod (@selector (isAccessibilityElement), [] (id self, SEL)
{
auto* handler = getHandler (self);
const auto hasAccessiblePropertiesOrIsTableCell = [] (auto& handlerRef)
{
const auto isTableCell = [&]
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&handlerRef, &AccessibilityHandler::getTableInterface))
{
if (auto* tableInterface = tableHandler->getTableInterface())
{
return tableInterface->getRowSpan (handlerRef).hasValue()
&& tableInterface->getColumnSpan (handlerRef).hasValue();
}
}
return false;
};
return handlerRef.getTitle().isNotEmpty()
|| handlerRef.getHelp().isNotEmpty()
|| handlerRef.getTextInterface() != nullptr
|| handlerRef.getValueInterface() != nullptr
|| isTableCell();
};
return handler != nullptr
&& ! handler->isIgnored()
&& handler->getRole() != AccessibilityRole::window
&& hasAccessiblePropertiesOrIsTableCell (*handler);
});
addMethod (@selector (accessibilityContainer), [] (id self, SEL) -> id
{
if (auto* handler = getHandler (self))
{
if (handler->getComponent().isOnDesktop())
return static_cast<id> (handler->getComponent().getWindowHandle());
if ( ! handler->getChildren().empty()
|| AccessibilityHandler::getNativeChildForComponent (handler->getComponent()) != nullptr)
{
if (UIAccessibilityElement* container = getContainer (self))
return container;
static AccessibilityContainer cls;
id container = cls.createInstance();
object_setInstanceVariable (container, "handler", handler);
object_setInstanceVariable (self, "container", container);
return container;
}
if (auto* parent = handler->getParent())
return [static_cast<id> (parent->getNativeImplementation()) accessibilityContainer];
}
return nil;
});
addMethod (@selector (accessibilityFrame), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
return convertToCGRect (handler->getComponent().getScreenBounds());
return CGRectZero;
});
addMethod (@selector (accessibilityTraits), [] (id self, SEL)
{
auto traits = UIAccessibilityTraits{};
if (auto* handler = getHandler (self))
{
traits |= [&handler]
{
switch (handler->getRole())
{
case AccessibilityRole::button:
case AccessibilityRole::toggleButton:
case AccessibilityRole::radioButton:
case AccessibilityRole::comboBox: return UIAccessibilityTraitButton;
case AccessibilityRole::label:
case AccessibilityRole::staticText: return UIAccessibilityTraitStaticText;
case AccessibilityRole::image: return UIAccessibilityTraitImage;
case AccessibilityRole::tableHeader: return UIAccessibilityTraitHeader;
case AccessibilityRole::hyperlink: return UIAccessibilityTraitLink;
case AccessibilityRole::ignored: return UIAccessibilityTraitNotEnabled;
case AccessibilityRole::editableText: return UIAccessibilityTraitKeyboardKey;
case AccessibilityRole::slider:
case AccessibilityRole::menuItem:
case AccessibilityRole::menuBar:
case AccessibilityRole::popupMenu:
case AccessibilityRole::table:
case AccessibilityRole::column:
case AccessibilityRole::row:
case AccessibilityRole::cell:
case AccessibilityRole::list:
case AccessibilityRole::listItem:
case AccessibilityRole::tree:
case AccessibilityRole::treeItem:
case AccessibilityRole::progressBar:
case AccessibilityRole::group:
case AccessibilityRole::dialogWindow:
case AccessibilityRole::window:
case AccessibilityRole::scrollBar:
case AccessibilityRole::tooltip:
case AccessibilityRole::splashScreen:
case AccessibilityRole::unspecified: break;
}
return UIAccessibilityTraitNone;
}();
const auto state = handler->getCurrentState();
if (state.isSelected() || state.isChecked())
traits |= UIAccessibilityTraitSelected;
if (auto* valueInterface = getValueInterface (self))
if (! valueInterface->isReadOnly() && valueInterface->getRange().isValid())
traits |= UIAccessibilityTraitAdjustable;
}
return traits | sendSuperclassMessage<UIAccessibilityTraits> (self, @selector (accessibilityTraits));
});
addMethod (@selector (accessibilityLabel), getAccessibilityTitle);
addMethod (@selector (accessibilityHint), getAccessibilityHelp);
addMethod (@selector (accessibilityValue), [] (id self, SEL) -> NSString*
{
if (auto* handler = getHandler (self))
{
if (handler->getCurrentState().isCheckable())
return handler->getCurrentState().isChecked() ? @"1" : @"0";
return (NSString*) getAccessibilityValueFromInterfaces (*handler);
}
return nil;
});
addMethod (@selector (setAccessibilityValue:), setAccessibilityValue);
addMethod (@selector (accessibilityElementDidBecomeFocused), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
{
const WeakReference<Component> safeComponent (&handler->getComponent());
performActionIfSupported (self, AccessibilityActionType::focus);
if (safeComponent != nullptr)
handler->grabFocus();
}
});
addMethod (@selector (accessibilityElementDidLoseFocus), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
handler->giveAwayFocus();
});
addMethod (@selector (accessibilityElementIsFocused), [] (id self, SEL) -> BOOL
{
if (auto* handler = getHandler (self))
return handler->hasFocus (false);
return NO;
});
addMethod (@selector (accessibilityViewIsModal), getIsAccessibilityModal);
addMethod (@selector (accessibilityActivate), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
{
// Occasionally VoiceOver sends accessibilityActivate to the wrong element, so we first query
// which element it thinks has focus and forward the event on to that element if it differs
id focusedElement = UIAccessibilityFocusedElement (UIAccessibilityNotificationVoiceOverIdentifier);
if (focusedElement != nullptr && ! [static_cast<id> (handler->getNativeImplementation()) isEqual: focusedElement])
return [focusedElement accessibilityActivate];
if (handler->hasFocus (false))
return accessibilityPerformPress (self, {});
}
return NO;
});
addMethod (@selector (accessibilityIncrement), accessibilityPerformIncrement);
addMethod (@selector (accessibilityDecrement), accessibilityPerformDecrement);
addMethod (@selector (accessibilityPerformEscape), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
{
if (auto* modal = Component::getCurrentlyModalComponent())
{
if (auto* modalHandler = modal->getAccessibilityHandler())
{
if (modalHandler == handler || modalHandler->isParentOf (handler))
{
modal->exitModalState (0);
return YES;
}
}
}
}
return NO;
});
if (elementType == Type::textElement)
{
addMethod (@selector (deleteBackward), [] (id, SEL) {});
addMethod (@selector (insertText:), [] (id, SEL, NSString*) {});
forEach ([this] (auto signature) { addPassthroughMethodWithSignature (signature); },
SignatureHasText{},
SignatureSetSelectedTextRange{},
SignatureSelectedTextRange{},
SignatureMarkedTextRange{},
SignatureSetMarkedTextSelectedRange{},
SignatureUnmarkText{},
SignatureMarkedTextStyle{},
SignatureSetMarkedTextStyle{},
SignatureBeginningOfDocument{},
SignatureEndOfDocument{},
SignatureTokenizer{},
SignatureBaseWritingDirection{},
SignatureCaretRectForPosition{},
SignatureCharacterRangeByExtending{},
SignatureCharacterRangeAtPoint{},
SignatureClosestPositionToPoint{},
SignatureClosestPositionToPointInRange{},
SignatureComparePositionToPosition{},
SignatureOffsetFromPositionToPosition{},
SignaturePositionFromPositionInDirection{},
SignaturePositionFromPositionOffset{},
SignatureFirstRectForRange{},
SignatureSelectionRectsForRange{},
SignaturePositionWithinRange{},
SignatureReplaceRangeWithText{},
SignatureSetBaseWritingDirection{},
SignatureTextInRange{},
SignatureTextRangeFromPosition{},
SignatureSetInputDelegate{},
SignatureInputDelegate{},
SignatureKeyboardType{},
SignatureAutocapitalizationType{},
SignatureAutocorrectionType{});
addProtocol (@protocol (UITextInput));
}
if (@available (iOS 11.0, *))
{
addMethod (@selector (accessibilityRowRange), getAccessibilityRowIndexRange);
addMethod (@selector (accessibilityColumnRange), getAccessibilityColumnIndexRange);
addProtocol (@protocol (UIAccessibilityContainerDataTableCell));
}
addIvar<UIAccessibilityElement*> ("container");
registerClass();
}
private:
template <typename Result>
static auto getResult (NSInvocation* invocation, detail::Tag<Result>)
{
Result result{};
[invocation getReturnValue: &result];
return result;
}
static void getResult (NSInvocation*, detail::Tag<void>) {}
template <typename HasSelector, typename Result, typename... Args>
auto makePassthroughCallback (HasSelector, Signature<Result (Args...)>)
{
return [] (id self, SEL, Args... args) -> Result
{
if (auto* input = getPeerTextInput (self))
{
const auto s = detail::makeCompileTimeStr (@encode (Result), @encode (id), @encode (SEL), @encode (Args)...);
const auto signature = [NSMethodSignature signatureWithObjCTypes: s.data()];
if (signature == nullptr)
{
jassertfalse;
return {};
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
const auto invocation = [NSInvocation invocationWithMethodSignature: signature];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
invocation.selector = HasSelector::sel();
// Indices 0 and 1 are 'id self' and 'SEL _cmd' respectively
auto counter = 2;
forEach ([&] (auto& arg) { [invocation setArgument: &arg atIndex: counter++]; }, args...);
[invocation invokeWithTarget: input];
return getResult (invocation, detail::Tag<Result>{});
}
jassertfalse;
return {};
};
}
template <typename Signature>
void addPassthroughMethodWithSignature (Signature signature)
{
addMethod (Signature::sel(), makePassthroughCallback (signature, signature));
}
static UIAccessibilityElement* getContainer (id self)
{
return getIvar<UIAccessibilityElement*> (self, "container");
}
static UIViewComponentPeer* getPeer (id self)
{
if (auto* handler = getHandler (self))
return static_cast<UIViewComponentPeer*> (handler->getComponent().getPeer());
return nil;
}
static JuceTextView* getPeerTextInput (id self)
{
if (auto* peer = getPeer (self))
return peer->hiddenTextInput.get();
return nil;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement)
};
//==============================================================================
AccessibilityElement::Holder accessibilityElement;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl)
};
//==============================================================================
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
{
return (AccessibilityNativeHandle*) nativeImpl->getAccessibilityElement();
}
static bool areAnyAccessibilityClientsActive()
{
return UIAccessibilityIsVoiceOverRunning();
}
static void sendAccessibilityEvent (UIAccessibilityNotifications notification, id argument)
{
if (! areAnyAccessibilityClientsActive())
return;
jassert (notification != UIAccessibilityNotifications{});
UIAccessibilityPostNotification (notification, argument);
}
void detail::AccessibilityHelpers::notifyAccessibilityEvent (const AccessibilityHandler& handler, Event eventType)
{
auto notification = [eventType]
{
switch (eventType)
{
case Event::elementCreated:
case Event::elementDestroyed:
case Event::elementMovedOrResized:
case Event::focusChanged: return UIAccessibilityLayoutChangedNotification;
case Event::windowOpened:
case Event::windowClosed: return UIAccessibilityScreenChangedNotification;
}
return UIAccessibilityNotifications{};
}();
if (notification != UIAccessibilityNotifications{})
{
const bool moveToHandler = (eventType == Event::focusChanged && handler.hasFocus (false));
sendAccessibilityEvent (notification,
moveToHandler ? static_cast<id> (handler.getNativeImplementation()) : nil);
}
}
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const
{
auto notification = [eventType]
{
switch (eventType)
{
case AccessibilityEvent::textSelectionChanged:
case AccessibilityEvent::rowSelectionChanged:
case AccessibilityEvent::textChanged:
case AccessibilityEvent::valueChanged:
case AccessibilityEvent::titleChanged: break;
case AccessibilityEvent::structureChanged: return UIAccessibilityLayoutChangedNotification;
}
return UIAccessibilityNotifications{};
}();
if (notification != UIAccessibilityNotifications{})
sendAccessibilityEvent (notification, static_cast<id> (getNativeImplementation()));
}
void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority)
{
sendAccessibilityEvent (UIAccessibilityAnnouncementNotification, juceStringToNS (announcementString));
}
} // namespace juce
@@ -0,0 +1,960 @@
/*
==============================================================================
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.
==============================================================================
*/
API_AVAILABLE (macos (10.10))
static void juceFreeAccessibilityPlatformSpecificData (NSAccessibilityElement<NSAccessibility>*) {}
namespace juce
{
#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
//==============================================================================
class AccessibilityHandler::AccessibilityNativeImpl
{
public:
explicit AccessibilityNativeImpl (AccessibilityHandler& handler)
{
if (@available (macOS 10.10, *))
accessibilityElement = AccessibilityElement::create (handler);
}
API_AVAILABLE (macos (10.10))
NSAccessibilityElement<NSAccessibility>* getAccessibilityElement() const noexcept
{
return accessibilityElement.get();
}
private:
//==============================================================================
class API_AVAILABLE (macos (10.10)) AccessibilityElement final : public AccessibleObjCClass<NSAccessibilityElement<NSAccessibility>>
{
public:
static Holder create (AccessibilityHandler& handler)
{
if (@available (macOS 10.10, *))
{
static AccessibilityElement cls;
Holder element ([cls.createInstance() init]);
object_setInstanceVariable (element.get(), "handler", &handler);
return element;
}
return {};
}
private:
AccessibilityElement()
{
addMethod (@selector (accessibilityNotifiesWhenDestroyed), [] (id, SEL) { return YES; });
addMethod (@selector (isAccessibilityElement), getIsAccessibilityElement);
addMethod (@selector (isAccessibilityEnabled), [] (id self, SEL) -> BOOL
{
if (auto* handler = getHandler (self))
return handler->getComponent().isEnabled();
return NO;
});
addMethod (@selector (accessibilityWindow), getAccessibilityWindow);
addMethod (@selector (accessibilityTopLevelUIElement), getAccessibilityWindow);
addMethod (@selector (accessibilityChildren), getAccessibilityChildren);
addMethod (@selector (isAccessibilityModal), getIsAccessibilityModal);
addMethod (@selector (accessibilityFocusedUIElement), [] (id self, SEL) -> id
{
if (auto* handler = getHandler (self))
{
if (auto* modal = Component::getCurrentlyModalComponent())
{
const auto& handlerComponent = handler->getComponent();
if (! handlerComponent.isParentOf (modal)
&& handlerComponent.isCurrentlyBlockedByAnotherModalComponent())
{
if (auto* modalHandler = modal->getAccessibilityHandler())
{
if (auto* focusChild = modalHandler->getChildFocus())
return static_cast<id> (focusChild->getNativeImplementation());
return static_cast<id> (modalHandler->getNativeImplementation());
}
}
}
if (auto* focusChild = handler->getChildFocus())
return static_cast<id> (focusChild->getNativeImplementation());
}
return nil;
});
addMethod (@selector (accessibilityHitTest:), [] (id self, SEL, NSPoint point) -> id
{
if (auto* handler = getHandler (self))
{
if (auto* child = handler->getChildAt (roundToIntPoint (flippedScreenPoint (point))))
return static_cast<id> (child->getNativeImplementation());
return self;
}
return nil;
});
addMethod (@selector (accessibilityParent), [] (id self, SEL) -> id
{
if (auto* handler = getHandler (self))
{
if (auto* parentHandler = handler->getParent())
return NSAccessibilityUnignoredAncestor (static_cast<id> (parentHandler->getNativeImplementation()));
return NSAccessibilityUnignoredAncestor (static_cast<id> (handler->getComponent().getWindowHandle()));
}
return nil;
});
addMethod (@selector (isAccessibilityFocused), [] (id self, SEL)
{
return [[self accessibilityWindow] accessibilityFocusedUIElement] == self;
});
addMethod (@selector (setAccessibilityFocused:), [] (id self, SEL, BOOL focused)
{
if (auto* handler = getHandler (self))
{
if (focused)
{
const WeakReference<Component> safeComponent (&handler->getComponent());
performActionIfSupported (self, AccessibilityActionType::focus);
if (safeComponent != nullptr)
handler->grabFocus();
}
else
{
handler->giveAwayFocus();
}
}
});
addMethod (@selector (accessibilityFrame), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
return flippedScreenRect (makeNSRect (handler->getComponent().getScreenBounds()));
return NSZeroRect;
});
addMethod (@selector (accessibilityRole), [] (id self, SEL) -> NSAccessibilityRole
{
if (auto* handler = getHandler (self))
{
switch (handler->getRole())
{
case AccessibilityRole::popupMenu:
case AccessibilityRole::tooltip:
case AccessibilityRole::splashScreen:
case AccessibilityRole::dialogWindow:
case AccessibilityRole::window: return NSAccessibilityWindowRole;
case AccessibilityRole::tableHeader:
case AccessibilityRole::unspecified:
case AccessibilityRole::group: return NSAccessibilityGroupRole;
case AccessibilityRole::label:
case AccessibilityRole::staticText: return NSAccessibilityStaticTextRole;
case AccessibilityRole::tree:
case AccessibilityRole::list: return NSAccessibilityOutlineRole;
case AccessibilityRole::listItem:
case AccessibilityRole::treeItem: return NSAccessibilityRowRole;
case AccessibilityRole::button: return NSAccessibilityButtonRole;
case AccessibilityRole::toggleButton: return NSAccessibilityCheckBoxRole;
case AccessibilityRole::radioButton: return NSAccessibilityRadioButtonRole;
case AccessibilityRole::comboBox: return NSAccessibilityPopUpButtonRole;
case AccessibilityRole::image: return NSAccessibilityImageRole;
case AccessibilityRole::slider: return NSAccessibilitySliderRole;
case AccessibilityRole::editableText: return NSAccessibilityTextAreaRole;
case AccessibilityRole::menuItem: return NSAccessibilityMenuItemRole;
case AccessibilityRole::menuBar: return NSAccessibilityMenuRole;
case AccessibilityRole::table: return NSAccessibilityOutlineRole;
case AccessibilityRole::column: return NSAccessibilityColumnRole;
case AccessibilityRole::row: return NSAccessibilityRowRole;
case AccessibilityRole::cell: return NSAccessibilityCellRole;
case AccessibilityRole::hyperlink: return NSAccessibilityLinkRole;
case AccessibilityRole::progressBar: return NSAccessibilityProgressIndicatorRole;
case AccessibilityRole::scrollBar: return NSAccessibilityScrollBarRole;
case AccessibilityRole::ignored: break;
}
return NSAccessibilityUnknownRole;
}
return nil;
});
addMethod (@selector (accessibilitySubrole), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
{
if (auto* textInterface = getTextInterface (self))
if (textInterface->isDisplayingProtectedText())
return NSAccessibilitySecureTextFieldSubrole;
const auto handlerRole = handler->getRole();
if (handlerRole == AccessibilityRole::window) return NSAccessibilityStandardWindowSubrole;
if (handlerRole == AccessibilityRole::dialogWindow) return NSAccessibilityDialogSubrole;
if (handlerRole == AccessibilityRole::tooltip
|| handlerRole == AccessibilityRole::splashScreen) return NSAccessibilityFloatingWindowSubrole;
if (handlerRole == AccessibilityRole::toggleButton) return NSAccessibilityToggleSubrole;
if (handlerRole == AccessibilityRole::treeItem
|| handlerRole == AccessibilityRole::listItem) return NSAccessibilityOutlineRowSubrole;
if (handlerRole == AccessibilityRole::row && getCellInterface (self) != nullptr) return NSAccessibilityTableRowSubrole;
const auto& handlerComponent = handler->getComponent();
if (auto* documentWindow = handlerComponent.findParentComponentOfClass<DocumentWindow>())
{
if (handlerRole == AccessibilityRole::button)
{
if (&handlerComponent == documentWindow->getCloseButton()) return NSAccessibilityCloseButtonSubrole;
if (&handlerComponent == documentWindow->getMinimiseButton()) return NSAccessibilityMinimizeButtonSubrole;
if (&handlerComponent == documentWindow->getMaximiseButton()) return NSAccessibilityFullScreenButtonSubrole;
}
}
}
return NSAccessibilityUnknownRole;
});
addMethod (@selector (accessibilityLabel), [] (id self, SEL) -> NSString*
{
if (auto* handler = getHandler (self))
return juceStringToNS (handler->getDescription());
return nil;
});
addMethod (@selector (accessibilityValue), [] (id self, SEL) -> id
{
if (auto* handler = getHandler (self))
{
if (! handler->getCurrentState().isCheckable())
return getAccessibilityValueFromInterfaces (*handler);
const auto checked = handler->getCurrentState().isChecked();
if ( handler->getRole() == AccessibilityRole::toggleButton
|| handler->getRole() == AccessibilityRole::radioButton)
{
return checked ? @YES : @NO;
}
return juceStringToNS (checked ? TRANS ("On") : TRANS ("Off"));
}
return nil;
});
addMethod (@selector (accessibilityTitle), getAccessibilityTitle);
addMethod (@selector (accessibilityHelp), getAccessibilityHelp);
addMethod (@selector (setAccessibilityValue:), setAccessibilityValue);
addMethod (@selector (accessibilitySelectedChildren), [] (id self, SEL)
{
return getSelectedChildren ([self accessibilityChildren]);
});
addMethod (@selector (setAccessibilitySelectedChildren:), [] (id self, SEL, NSArray* selected)
{
setSelectedChildren ([self accessibilityChildren], selected);
});
addMethod (@selector (accessibilityOrientation), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
return handler->getComponent().getBounds().toFloat().getAspectRatio() > 1.0f
? NSAccessibilityOrientationHorizontal
: NSAccessibilityOrientationVertical;
return NSAccessibilityOrientationUnknown;
});
addMethod (@selector (accessibilityInsertionPointLineNumber), [] (id self, SEL) -> NSInteger
{
if (auto* textInterface = getTextInterface (self))
return [self accessibilityLineForIndex: textInterface->getTextInsertionOffset()];
return 0;
});
addMethod (@selector (accessibilityVisibleCharacterRange), [] (id self, SEL)
{
if (auto* textInterface = getTextInterface (self))
return juceRangeToNS ({ 0, textInterface->getTotalNumCharacters() });
return NSMakeRange (0, 0);
});
addMethod (@selector (accessibilityNumberOfCharacters), [] (id self, SEL)
{
if (auto* textInterface = getTextInterface (self))
return textInterface->getTotalNumCharacters();
return 0;
});
addMethod (@selector (accessibilitySelectedText), [] (id self, SEL) -> NSString*
{
if (auto* textInterface = getTextInterface (self))
return juceStringToNS (textInterface->getText (textInterface->getSelection()));
return nil;
});
addMethod (@selector (accessibilitySelectedTextRange), [] (id self, SEL)
{
if (auto* textInterface = getTextInterface (self))
{
const auto currentSelection = textInterface->getSelection();
if (currentSelection.isEmpty())
return NSMakeRange ((NSUInteger) textInterface->getTextInsertionOffset(), 0);
return juceRangeToNS (currentSelection);
}
return NSMakeRange (0, 0);
});
addMethod (@selector (accessibilityAttributedStringForRange:), [] (id self, SEL, NSRange range) -> NSAttributedString*
{
NSString* string = [self accessibilityStringForRange: range];
if (string != nil)
return [[[NSAttributedString alloc] initWithString: string] autorelease];
return nil;
});
addMethod (@selector (accessibilityRangeForLine:), [] (id self, SEL, NSInteger line)
{
if (auto* textInterface = getTextInterface (self))
{
auto text = textInterface->getText ({ 0, textInterface->getTotalNumCharacters() });
auto lines = StringArray::fromLines (text);
if (line < lines.size())
{
auto lineText = lines[(int) line];
auto start = text.indexOf (lineText);
if (start >= 0)
return NSMakeRange ((NSUInteger) start, (NSUInteger) lineText.length());
}
}
return NSMakeRange (0, 0);
});
addMethod (@selector (accessibilityStringForRange:), [] (id self, SEL, NSRange range) -> NSString*
{
if (auto* textInterface = getTextInterface (self))
return juceStringToNS (textInterface->getText (nsRangeToJuce (range)));
return nil;
});
addMethod (@selector (accessibilityRangeForPosition:), [] (id self, SEL, NSPoint position)
{
if (auto* handler = getHandler (self))
{
if (auto* textInterface = handler->getTextInterface())
{
auto screenPoint = roundToIntPoint (flippedScreenPoint (position));
if (handler->getComponent().getScreenBounds().contains (screenPoint))
{
auto offset = textInterface->getOffsetAtPoint (screenPoint);
if (offset >= 0)
return NSMakeRange ((NSUInteger) offset, 1);
}
}
}
return NSMakeRange (0, 0);
});
addMethod (@selector (accessibilityRangeForIndex:), [] (id self, SEL, NSInteger index)
{
if (auto* textInterface = getTextInterface (self))
if (isPositiveAndBelow (index, textInterface->getTotalNumCharacters()))
return NSMakeRange ((NSUInteger) index, 1);
return NSMakeRange (0, 0);
});
addMethod (@selector (accessibilityFrameForRange:), [] (id self, SEL, NSRange range)
{
if (auto* textInterface = getTextInterface (self))
return flippedScreenRect (makeNSRect (textInterface->getTextBounds (nsRangeToJuce (range)).getBounds()));
return NSZeroRect;
});
addMethod (@selector (accessibilityLineForIndex:), [] (id self, SEL, NSInteger index)
{
if (auto* textInterface = getTextInterface (self))
{
auto text = textInterface->getText ({ 0, (int) index });
if (! text.isEmpty())
return StringArray::fromLines (text).size() - 1;
}
return 0;
});
addMethod (@selector (setAccessibilitySelectedTextRange:), [] (id self, SEL, NSRange selectedRange)
{
if (auto* textInterface = getTextInterface (self))
textInterface->setSelection (nsRangeToJuce (selectedRange));
});
addMethod (@selector (accessibilityRows), [] (id self, SEL) -> NSArray*
{
if (auto* tableInterface = getTableInterface (self))
{
auto* rows = [[NSMutableArray new] autorelease];
for (int row = 0, numRows = tableInterface->getNumRows(); row < numRows; ++row)
{
if (auto* rowHandler = tableInterface->getRowHandler (row))
{
[rows addObject: static_cast<id> (rowHandler->getNativeImplementation())];
}
else
{
[rows addObject: [NSAccessibilityElement accessibilityElementWithRole: NSAccessibilityRowRole
frame: NSZeroRect
label: @"Offscreen Row"
parent: self]];
}
}
return rows;
}
return nil;
});
addMethod (@selector (accessibilitySelectedRows), [] (id self, SEL)
{
return getSelectedChildren ([self accessibilityRows]);
});
addMethod (@selector (setAccessibilitySelectedRows:), [] (id self, SEL, NSArray* selected)
{
setSelectedChildren ([self accessibilityRows], selected);
});
addMethod (@selector (accessibilityHeader), [] (id self, SEL) -> id
{
if (auto* tableInterface = getTableInterface (self))
if (auto* handler = tableInterface->getHeaderHandler())
return static_cast<id> (handler->getNativeImplementation());
return nil;
});
addMethod (@selector (accessibilityRowCount), getAccessibilityRowCount);
addMethod (@selector (accessibilityColumnCount), getAccessibilityColumnCount);
addMethod (@selector (accessibilityRowIndexRange), getAccessibilityRowIndexRange);
addMethod (@selector (accessibilityColumnIndexRange), getAccessibilityColumnIndexRange);
addMethod (@selector (accessibilityIndex), [] (id self, SEL) -> NSInteger
{
if (auto* handler = getHandler (self))
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (handler, &AccessibilityHandler::getTableInterface))
{
if (auto* tableInterface = tableHandler->getTableInterface())
{
NSAccessibilityRole handlerRole = [self accessibilityRole];
if ([handlerRole isEqual: NSAccessibilityRowRole])
if (const auto span = tableInterface->getRowSpan (*handler))
return span->begin;
if ([handlerRole isEqual: NSAccessibilityColumnRole])
if (const auto span = tableInterface->getColumnSpan (*handler))
return span->begin;
}
}
}
return 0;
});
addMethod (@selector (accessibilityDisclosureLevel), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
if (auto* cellInterface = handler->getCellInterface())
return cellInterface->getDisclosureLevel();
return 0;
});
addMethod (@selector (accessibilityDisclosedRows), [] (id self, SEL) -> id
{
if (auto* handler = getHandler (self))
{
if (auto* cellInterface = handler->getCellInterface())
{
const auto rows = cellInterface->getDisclosedRows();
auto* result = [NSMutableArray arrayWithCapacity: rows.size()];
for (const auto& row : rows)
{
if (row != nullptr)
[result addObject: static_cast<id> (row->getNativeImplementation())];
else
[result addObject: [NSAccessibilityElement accessibilityElementWithRole: NSAccessibilityRowRole
frame: NSZeroRect
label: @"Offscreen Row"
parent: self]];
}
return result;
}
}
return nil;
});
addMethod (@selector (isAccessibilityExpanded), [] (id self, SEL) -> BOOL
{
if (auto* handler = getHandler (self))
return handler->getCurrentState().isExpanded();
return NO;
});
addMethod (@selector (accessibilityPerformIncrement), accessibilityPerformIncrement);
addMethod (@selector (accessibilityPerformDecrement), accessibilityPerformDecrement);
addMethod (@selector (accessibilityPerformDelete), [] (id self, SEL)
{
if (auto* handler = getHandler (self))
{
if (hasEditableText (*handler))
{
handler->getTextInterface()->setText ({});
return YES;
}
if (auto* valueInterface = handler->getValueInterface())
{
if (! valueInterface->isReadOnly())
{
valueInterface->setValue ({});
return YES;
}
}
}
return NO;
});
addMethod (@selector (accessibilityPerformPress), accessibilityPerformPress);
addMethod (@selector (accessibilityPerformShowMenu), [] (id self, SEL)
{
return performActionIfSupported (self, AccessibilityActionType::showMenu);
});
addMethod (@selector (accessibilityPerformRaise), [] (id self, SEL)
{
[self setAccessibilityFocused: YES]; return YES;
});
addMethod (@selector (isAccessibilitySelectorAllowed:), [] (id self, SEL, SEL selector) -> BOOL
{
if (auto* handler = getHandler (self))
{
const auto handlerRole = handler->getRole();
const auto currentState = handler->getCurrentState();
for (auto textSelector : { @selector (accessibilityInsertionPointLineNumber),
@selector (accessibilityVisibleCharacterRange),
@selector (accessibilityNumberOfCharacters),
@selector (accessibilitySelectedText),
@selector (accessibilitySelectedTextRange),
@selector (accessibilityAttributedStringForRange:),
@selector (accessibilityRangeForLine:),
@selector (accessibilityStringForRange:),
@selector (accessibilityRangeForPosition:),
@selector (accessibilityRangeForIndex:),
@selector (accessibilityFrameForRange:),
@selector (accessibilityLineForIndex:),
@selector (setAccessibilitySelectedTextRange:) })
{
if (selector == textSelector)
return handler->getTextInterface() != nullptr;
}
for (auto tableSelector : { @selector (accessibilityRowCount),
@selector (accessibilityRows),
@selector (accessibilitySelectedRows),
@selector (accessibilityColumnCount),
@selector (accessibilityHeader) })
{
if (selector == tableSelector)
return handler->getTableInterface() != nullptr;
}
for (auto cellSelector : { @selector (accessibilityRowIndexRange),
@selector (accessibilityColumnIndexRange),
@selector (accessibilityIndex),
@selector (accessibilityDisclosureLevel) })
{
if (selector == cellSelector)
return handler->getCellInterface() != nullptr;
}
for (auto valueSelector : { @selector (accessibilityValue),
@selector (setAccessibilityValue:),
@selector (accessibilityPerformDelete),
@selector (accessibilityPerformIncrement),
@selector (accessibilityPerformDecrement) })
{
if (selector != valueSelector)
continue;
auto* valueInterface = handler->getValueInterface();
if (selector == @selector (accessibilityValue))
return valueInterface != nullptr
|| hasEditableText (*handler)
|| currentState.isCheckable();
auto hasEditableValue = [valueInterface] { return valueInterface != nullptr && ! valueInterface->isReadOnly(); };
if (selector == @selector (setAccessibilityValue:)
|| selector == @selector (accessibilityPerformDelete))
return hasEditableValue() || hasEditableText (*handler);
auto isRanged = [valueInterface] { return valueInterface != nullptr && valueInterface->getRange().isValid(); };
if (selector == @selector (accessibilityPerformIncrement)
|| selector == @selector (accessibilityPerformDecrement))
return hasEditableValue() && isRanged();
return NO;
}
for (auto actionSelector : { @selector (accessibilityPerformPress),
@selector (accessibilityPerformShowMenu),
@selector (accessibilityPerformRaise),
@selector (setAccessibilityFocused:) })
{
if (selector != actionSelector)
continue;
if (selector == @selector (accessibilityPerformPress))
return (handler->getCurrentState().isCheckable() && handler->getActions().contains (AccessibilityActionType::toggle))
|| handler->getActions().contains (AccessibilityActionType::press);
if (selector == @selector (accessibilityPerformShowMenu))
return handler->getActions().contains (AccessibilityActionType::showMenu);
if (selector == @selector (accessibilityPerformRaise))
return [[self accessibilityRole] isEqual: NSAccessibilityWindowRole];
if (selector == @selector (setAccessibilityFocused:))
return currentState.isFocusable();
}
if (selector == @selector (accessibilitySelectedChildren))
return handlerRole == AccessibilityRole::popupMenu;
if (selector == @selector (accessibilityOrientation))
return handlerRole == AccessibilityRole::scrollBar;
if (selector == @selector (isAccessibilityExpanded))
return currentState.isExpandable();
return sendSuperclassMessage<BOOL> (self, @selector (isAccessibilitySelectorAllowed:), selector);
}
return NO;
});
addMethod (@selector (accessibilityChildrenInNavigationOrder), getAccessibilityChildren);
registerClass();
}
//==============================================================================
static bool isSelectable (AccessibleState state) noexcept
{
return state.isSelectable() || state.isMultiSelectable();
}
static NSArray* getSelectedChildren (NSArray* children)
{
NSMutableArray* selected = [[NSMutableArray new] autorelease];
for (id child in children)
{
if (auto* handler = getHandler (child))
{
const auto currentState = handler->getCurrentState();
if (isSelectable (currentState) && currentState.isSelected())
[selected addObject: child];
}
}
return selected;
}
static void setSelected (id item, bool selected)
{
auto* handler = getHandler (item);
if (handler == nullptr)
return;
const auto currentState = handler->getCurrentState();
if (isSelectable (currentState))
{
if (currentState.isSelected() != selected)
handler->getActions().invoke (AccessibilityActionType::toggle);
}
else if (currentState.isFocusable())
{
[item setAccessibilityFocused: selected];
}
}
static void setSelectedChildren (NSArray* children, NSArray* selected)
{
for (id child in children)
setSelected (child, [selected containsObject: child]);
}
//==============================================================================
static id getAccessibilityWindow (id self, SEL)
{
return [[self accessibilityParent] accessibilityWindow];
}
static NSArray* getAccessibilityChildren (id self, SEL)
{
if (auto* handler = getHandler (self))
{
auto children = handler->getChildren();
auto* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()];
for (auto* childHandler : children)
[accessibleChildren addObject: static_cast<id> (childHandler->getNativeImplementation())];
if (id nativeChild = static_cast<id> (AccessibilityHandler::getNativeChildForComponent (handler->getComponent())))
{
// Having both native and non-native children would require implementing an
// ordering. However, this situation doesn't occur with any of our current
// use-cases.
jassert ([accessibleChildren count] == 0);
if ([nativeChild isAccessibilityElement])
[accessibleChildren addObject:nativeChild];
else if (auto* childrenOfChild = [nativeChild accessibilityChildren]; childrenOfChild != nil)
[accessibleChildren addObjectsFromArray:(NSArray* _Nonnull) childrenOfChild];
}
return accessibleChildren;
}
return nil;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement)
};
//==============================================================================
API_AVAILABLE (macos (10.10))
AccessibilityElement::Holder accessibilityElement;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl)
};
//==============================================================================
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
{
if (@available (macOS 10.10, *))
return (AccessibilityNativeHandle*) nativeImpl->getAccessibilityElement();
return nullptr;
}
static bool areAnyAccessibilityClientsActive()
{
const String voiceOverKeyString ("voiceOverOnOffKey");
const String applicationIDString ("com.apple.universalaccess");
CFUniquePtr<CFStringRef> cfKey (voiceOverKeyString.toCFString());
CFUniquePtr<CFStringRef> cfID (applicationIDString.toCFString());
CFUniquePtr<CFPropertyListRef> value (CFPreferencesCopyAppValue (cfKey.get(), cfID.get()));
if (value != nullptr)
return CFBooleanGetValue ((CFBooleanRef) value.get());
return false;
}
static void sendAccessibilityEvent (id accessibilityElement,
NSAccessibilityNotificationName notification,
NSDictionary* userInfo)
{
jassert (notification != NSAccessibilityNotificationName{});
NSAccessibilityPostNotificationWithUserInfo (accessibilityElement, notification, userInfo);
}
static void sendHandlerNotification (const AccessibilityHandler& handler,
NSAccessibilityNotificationName notification)
{
if (! areAnyAccessibilityClientsActive() || notification == NSAccessibilityNotificationName{})
return;
if (@available (macOS 10.9, *))
{
if (id accessibilityElement = static_cast<id> (handler.getNativeImplementation()))
{
sendAccessibilityEvent (accessibilityElement, notification,
(notification == NSAccessibilityLayoutChangedNotification
? @{ NSAccessibilityUIElementsKey: @[ accessibilityElement ] }
: nil));
}
}
}
static NSAccessibilityNotificationName layoutChangedNotification()
{
if (@available (macOS 10.9, *))
return NSAccessibilityLayoutChangedNotification;
static NSString* layoutChangedString = @"AXLayoutChanged";
return layoutChangedString;
}
void detail::AccessibilityHelpers::notifyAccessibilityEvent (const AccessibilityHandler& handler, Event eventType)
{
auto notification = [eventType]
{
switch (eventType)
{
case Event::elementCreated: return NSAccessibilityCreatedNotification;
case Event::elementDestroyed: return NSAccessibilityUIElementDestroyedNotification;
case Event::elementMovedOrResized: return layoutChangedNotification();
case Event::focusChanged: return NSAccessibilityFocusedUIElementChangedNotification;
case Event::windowOpened: return NSAccessibilityWindowCreatedNotification;
case Event::windowClosed: break;
}
return NSAccessibilityNotificationName{};
}();
sendHandlerNotification (handler, notification);
}
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const
{
auto notification = [eventType]
{
switch (eventType)
{
case AccessibilityEvent::textSelectionChanged: return NSAccessibilitySelectedTextChangedNotification;
case AccessibilityEvent::rowSelectionChanged: return NSAccessibilitySelectedRowsChangedNotification;
case AccessibilityEvent::textChanged:
case AccessibilityEvent::valueChanged: return NSAccessibilityValueChangedNotification;
case AccessibilityEvent::titleChanged: return NSAccessibilityTitleChangedNotification;
case AccessibilityEvent::structureChanged: return layoutChangedNotification();
}
return NSAccessibilityNotificationName{};
}();
sendHandlerNotification (*this, notification);
}
void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority priority)
{
if (! areAnyAccessibilityClientsActive())
return;
if (@available (macOS 10.9, *))
{
auto nsPriority = [priority]
{
// The below doesn't get noticed by the @available check above
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability")
switch (priority)
{
case AnnouncementPriority::low: return NSAccessibilityPriorityLow;
case AnnouncementPriority::medium: return NSAccessibilityPriorityMedium;
case AnnouncementPriority::high: return NSAccessibilityPriorityHigh;
}
jassertfalse;
return NSAccessibilityPriorityLow;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}();
sendAccessibilityEvent (static_cast<id> ([NSApp mainWindow]),
NSAccessibilityAnnouncementRequestedNotification,
@{ NSAccessibilityAnnouncementKey: juceStringToNS (announcementString),
NSAccessibilityPriorityKey: @(nsPriority) });
}
}
} // namespace juce
@@ -0,0 +1,331 @@
/*
==============================================================================
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
{
#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
static bool isStartingUpOrShuttingDown()
{
if (auto* app = JUCEApplicationBase::getInstance())
if (app->isInitialising())
return true;
if (auto* mm = MessageManager::getInstanceWithoutCreating())
if (mm->hasStopMessageBeenSent())
return true;
return false;
}
static bool isHandlerValid (const AccessibilityHandler& handler)
{
if (auto* provider = handler.getNativeImplementation())
return provider->isElementValid();
return false;
}
//==============================================================================
class AccessibilityHandler::AccessibilityNativeImpl
{
public:
explicit AccessibilityNativeImpl (AccessibilityHandler& owner)
: accessibilityElement (new AccessibilityNativeHandle (owner))
{
++providerCount;
}
~AccessibilityNativeImpl()
{
ComSmartPtr<IRawElementProviderSimple> provider;
accessibilityElement->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
accessibilityElement->invalidateElement();
--providerCount;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
{
uiaWrapper->disconnectProvider (provider);
if (providerCount == 0 && JUCEApplicationBase::isStandaloneApp())
uiaWrapper->disconnectAllProviders();
}
}
//==============================================================================
ComSmartPtr<AccessibilityNativeHandle> accessibilityElement;
static int providerCount;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityNativeImpl)
};
int AccessibilityHandler::AccessibilityNativeImpl::providerCount = 0;
//==============================================================================
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const
{
return nativeImpl->accessibilityElement;
}
static bool areAnyAccessibilityClientsActive()
{
const auto areClientsListening = []
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
return uiaWrapper->clientsAreListening() != 0;
return false;
};
const auto isScreenReaderRunning = []
{
BOOL isRunning = FALSE;
SystemParametersInfo (SPI_GETSCREENREADER, 0, (PVOID) &isRunning, 0);
return isRunning != 0;
};
return areClientsListening() || isScreenReaderRunning();
}
template <typename Callback>
void getProviderWithCheckedWrapper (const AccessibilityHandler& handler, Callback&& callback)
{
if (! areAnyAccessibilityClientsActive() || isStartingUpOrShuttingDown() || ! isHandlerValid (handler))
return;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
{
ComSmartPtr<IRawElementProviderSimple> provider;
handler.getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
callback (uiaWrapper, provider);
}
}
void sendAccessibilityAutomationEvent (const AccessibilityHandler& handler, EVENTID event)
{
jassert (event != EVENTID{});
getProviderWithCheckedWrapper (handler, [event] (WindowsUIAWrapper* uiaWrapper, ComSmartPtr<IRawElementProviderSimple>& provider)
{
uiaWrapper->raiseAutomationEvent (provider, event);
});
}
void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler& handler, PROPERTYID property, VARIANT newValue)
{
jassert (property != PROPERTYID{});
getProviderWithCheckedWrapper (handler, [property, newValue] (WindowsUIAWrapper* uiaWrapper, ComSmartPtr<IRawElementProviderSimple>& provider)
{
VARIANT oldValue;
VariantHelpers::clear (&oldValue);
uiaWrapper->raiseAutomationPropertyChangedEvent (provider, property, oldValue, newValue);
});
}
void detail::AccessibilityHelpers::notifyAccessibilityEvent (const AccessibilityHandler& handler, Event eventType)
{
using namespace ComTypes::Constants;
if (eventType == Event::elementCreated
|| eventType == Event::elementDestroyed)
{
if (auto* parent = handler.getParent())
sendAccessibilityAutomationEvent (*parent, UIA_LayoutInvalidatedEventId);
return;
}
if (eventType == Event::windowOpened
|| eventType == Event::windowClosed)
{
if (auto* peer = handler.getComponent().getPeer())
if ((peer->getStyleFlags() & ComponentPeer::windowHasTitleBar) == 0)
return;
}
auto event = [eventType]() -> EVENTID
{
switch (eventType)
{
case Event::focusChanged: return UIA_AutomationFocusChangedEventId;
case Event::windowOpened: return UIA_Window_WindowOpenedEventId;
case Event::windowClosed: return UIA_Window_WindowClosedEventId;
case Event::elementCreated:
case Event::elementDestroyed:
case Event::elementMovedOrResized: break;
}
return {};
}();
if (event != EVENTID{})
sendAccessibilityAutomationEvent (handler, event);
}
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent eventType) const
{
if (eventType == AccessibilityEvent::titleChanged)
{
VARIANT newValue;
VariantHelpers::setString (getTitle(), &newValue);
sendAccessibilityPropertyChangedEvent (*this, UIA_NamePropertyId, newValue);
return;
}
if (eventType == AccessibilityEvent::valueChanged)
{
if (auto* valueInterface = getValueInterface())
{
const auto propertyType = getRole() == AccessibilityRole::slider ? UIA_RangeValueValuePropertyId
: UIA_ValueValuePropertyId;
const auto value = getRole() == AccessibilityRole::slider
? VariantHelpers::getWithValue (valueInterface->getCurrentValue())
: VariantHelpers::getWithValue (valueInterface->getCurrentValueAsString());
sendAccessibilityPropertyChangedEvent (*this, propertyType, value);
}
return;
}
auto event = [eventType]() -> EVENTID
{
using namespace ComTypes::Constants;
switch (eventType)
{
case AccessibilityEvent::textSelectionChanged: return UIA_Text_TextSelectionChangedEventId;
case AccessibilityEvent::textChanged: return UIA_Text_TextChangedEventId;
case AccessibilityEvent::structureChanged: return UIA_StructureChangedEventId;
case AccessibilityEvent::rowSelectionChanged: return UIA_SelectionItem_ElementSelectedEventId;
case AccessibilityEvent::titleChanged:
case AccessibilityEvent::valueChanged: break;
}
return {};
}();
if (event != EVENTID{})
sendAccessibilityAutomationEvent (*this, event);
}
struct SpVoiceWrapper final : public DeletedAtShutdown
{
SpVoiceWrapper()
{
[[maybe_unused]] auto hr = voice.CoCreateInstance (ComTypes::CLSID_SpVoice);
jassert (SUCCEEDED (hr));
}
~SpVoiceWrapper() override
{
clearSingletonInstance();
}
ComSmartPtr<ISpVoice> voice;
JUCE_DECLARE_SINGLETON (SpVoiceWrapper, false)
};
JUCE_IMPLEMENT_SINGLETON (SpVoiceWrapper)
void AccessibilityHandler::postAnnouncement (const String& announcementString, AnnouncementPriority priority)
{
if (! areAnyAccessibilityClientsActive())
return;
if (auto* sharedVoice = SpVoiceWrapper::getInstance())
{
auto voicePriority = [priority]
{
switch (priority)
{
case AnnouncementPriority::low: return SPVPRI_OVER;
case AnnouncementPriority::medium: return SPVPRI_NORMAL;
case AnnouncementPriority::high: return SPVPRI_ALERT;
}
jassertfalse;
return SPVPRI_OVER;
}();
sharedVoice->voice->SetPriority (voicePriority);
sharedVoice->voice->Speak (announcementString.toWideCharPointer(), SPF_ASYNC, nullptr);
}
}
//==============================================================================
namespace WindowsAccessibility
{
static long getUiaRootObjectId()
{
return static_cast<long> (UiaRootObjectId);
}
static bool handleWmGetObject (AccessibilityHandler* handler, WPARAM wParam, LPARAM lParam, LRESULT* res)
{
if (isStartingUpOrShuttingDown() || (handler == nullptr || ! isHandlerValid (*handler)))
return false;
if (auto* uiaWrapper = WindowsUIAWrapper::getInstance())
{
ComSmartPtr<IRawElementProviderSimple> provider;
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
if (! uiaWrapper->isProviderDisconnecting (provider))
*res = uiaWrapper->returnRawElementProvider ((HWND) handler->getComponent().getWindowHandle(), wParam, lParam, provider);
return true;
}
return false;
}
static void revokeUIAMapEntriesForWindow (HWND hwnd)
{
if (auto* uiaWrapper = WindowsUIAWrapper::getInstanceWithoutCreating())
uiaWrapper->returnRawElementProvider (hwnd, 0, 0, nullptr);
}
}
JUCE_IMPLEMENT_SINGLETON (WindowsUIAWrapper)
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
} // namespace juce
@@ -0,0 +1,500 @@
/*
==============================================================================
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::ComTypes
{
/*
These interfaces would normally be included in the system platform headers.
However, those headers are likely to be incomplete when building with
MinGW. In order to allow building accessible applications under MinGW,
we reproduce all necessary definitions here.
*/
struct UiaPoint
{
double x;
double y;
};
struct UiaRect
{
double left;
double top;
double width;
double height;
};
enum NavigateDirection
{
NavigateDirection_Parent = 0,
NavigateDirection_NextSibling = 1,
NavigateDirection_PreviousSibling = 2,
NavigateDirection_FirstChild = 3,
NavigateDirection_LastChild = 4
};
enum ExpandCollapseState
{
ExpandCollapseState_Collapsed = 0,
ExpandCollapseState_Expanded = 1,
ExpandCollapseState_PartiallyExpanded = 2,
ExpandCollapseState_LeafNode = 3
};
enum TextPatternRangeEndpoint
{
TextPatternRangeEndpoint_Start = 0,
TextPatternRangeEndpoint_End = 1
};
enum TextUnit
{
TextUnit_Character = 0,
TextUnit_Format = 1,
TextUnit_Word = 2,
TextUnit_Line = 3,
TextUnit_Paragraph = 4,
TextUnit_Page = 5,
TextUnit_Document = 6
};
enum SupportedTextSelection
{
SupportedTextSelection_None = 0,
SupportedTextSelection_Single = 1,
SupportedTextSelection_Multiple = 2
};
enum CaretPosition
{
CaretPosition_Unknown = 0,
CaretPosition_EndOfLine = 1,
CaretPosition_BeginningOfLine = 2
};
enum ToggleState
{
ToggleState_Off = 0,
ToggleState_On = 1,
ToggleState_Indeterminate = 2
};
enum WindowVisualState
{
WindowVisualState_Normal = 0,
WindowVisualState_Maximized = 1,
WindowVisualState_Minimized = 2
};
enum WindowInteractionState
{
WindowInteractionState_Running = 0,
WindowInteractionState_Closing = 1,
WindowInteractionState_ReadyForUserInteraction = 2,
WindowInteractionState_BlockedByModalWindow = 3,
WindowInteractionState_NotResponding = 4
};
enum RowOrColumnMajor
{
RowOrColumnMajor_RowMajor = 0,
RowOrColumnMajor_ColumnMajor = 1,
RowOrColumnMajor_Indeterminate = 2
};
enum ScrollAmount
{
ScrollAmount_LargeDecrement = 0,
ScrollAmount_SmallDecrement = 1,
ScrollAmount_NoAmount = 2,
ScrollAmount_LargeIncrement = 3,
ScrollAmount_SmallIncrement = 4
};
namespace Constants
{
#undef UIA_InvokePatternId
#undef UIA_SelectionPatternId
#undef UIA_ValuePatternId
#undef UIA_RangeValuePatternId
#undef UIA_ScrollPatternId
#undef UIA_ExpandCollapsePatternId
#undef UIA_GridPatternId
#undef UIA_GridItemPatternId
#undef UIA_WindowPatternId
#undef UIA_SelectionItemPatternId
#undef UIA_TablePatternId
#undef UIA_TableItemPatternId
#undef UIA_TextPatternId
#undef UIA_TogglePatternId
#undef UIA_TransformPatternId
#undef UIA_ScrollItemPatternId
#undef UIA_TextPattern2Id
#undef UIA_StructureChangedEventId
#undef UIA_MenuOpenedEventId
#undef UIA_AutomationFocusChangedEventId
#undef UIA_MenuClosedEventId
#undef UIA_LayoutInvalidatedEventId
#undef UIA_Invoke_InvokedEventId
#undef UIA_SelectionItem_ElementSelectedEventId
#undef UIA_Text_TextSelectionChangedEventId
#undef UIA_Text_TextChangedEventId
#undef UIA_Window_WindowOpenedEventId
#undef UIA_Window_WindowClosedEventId
#undef UIA_IsPeripheralPropertyId
#undef UIA_FullDescriptionPropertyId
#undef UIA_IsDialogPropertyId
#undef UIA_IsReadOnlyAttributeId
#undef UIA_CaretPositionAttributeId
#undef UIA_ButtonControlTypeId
#undef UIA_CheckBoxControlTypeId
#undef UIA_ComboBoxControlTypeId
#undef UIA_EditControlTypeId
#undef UIA_HyperlinkControlTypeId
#undef UIA_ImageControlTypeId
#undef UIA_ListItemControlTypeId
#undef UIA_ListControlTypeId
#undef UIA_MenuBarControlTypeId
#undef UIA_MenuItemControlTypeId
#undef UIA_ProgressBarControlTypeId
#undef UIA_RadioButtonControlTypeId
#undef UIA_ScrollBarControlTypeId
#undef UIA_SliderControlTypeId
#undef UIA_TextControlTypeId
#undef UIA_ToolTipControlTypeId
#undef UIA_TreeControlTypeId
#undef UIA_TreeItemControlTypeId
#undef UIA_CustomControlTypeId
#undef UIA_GroupControlTypeId
#undef UIA_DataItemControlTypeId
#undef UIA_WindowControlTypeId
#undef UIA_HeaderControlTypeId
#undef UIA_HeaderItemControlTypeId
#undef UIA_TableControlTypeId
const long UIA_InvokePatternId = 10000;
const long UIA_SelectionPatternId = 10001;
const long UIA_ValuePatternId = 10002;
const long UIA_RangeValuePatternId = 10003;
const long UIA_ScrollPatternId = 10004;
const long UIA_ExpandCollapsePatternId = 10005;
const long UIA_GridPatternId = 10006;
const long UIA_GridItemPatternId = 10007;
const long UIA_WindowPatternId = 10009;
const long UIA_SelectionItemPatternId = 10010;
const long UIA_TablePatternId = 10012;
const long UIA_TableItemPatternId = 10013;
const long UIA_TextPatternId = 10014;
const long UIA_TogglePatternId = 10015;
const long UIA_TransformPatternId = 10016;
const long UIA_ScrollItemPatternId = 10017;
const long UIA_TextPattern2Id = 10024;
const long UIA_StructureChangedEventId = 20002;
const long UIA_MenuOpenedEventId = 20003;
const long UIA_AutomationFocusChangedEventId = 20005;
const long UIA_MenuClosedEventId = 20007;
const long UIA_LayoutInvalidatedEventId = 20008;
const long UIA_Invoke_InvokedEventId = 20009;
const long UIA_SelectionItem_ElementSelectedEventId = 20012;
const long UIA_Text_TextSelectionChangedEventId = 20014;
const long UIA_Text_TextChangedEventId = 20015;
const long UIA_Window_WindowOpenedEventId = 20016;
const long UIA_Window_WindowClosedEventId = 20017;
const long UIA_IsPeripheralPropertyId = 30150;
const long UIA_FullDescriptionPropertyId = 30159;
const long UIA_IsDialogPropertyId = 30174;
const long UIA_IsReadOnlyAttributeId = 40015;
const long UIA_CaretPositionAttributeId = 40038;
const long UIA_ButtonControlTypeId = 50000;
const long UIA_CheckBoxControlTypeId = 50002;
const long UIA_ComboBoxControlTypeId = 50003;
const long UIA_EditControlTypeId = 50004;
const long UIA_HyperlinkControlTypeId = 50005;
const long UIA_ImageControlTypeId = 50006;
const long UIA_ListItemControlTypeId = 50007;
const long UIA_ListControlTypeId = 50008;
const long UIA_MenuBarControlTypeId = 50010;
const long UIA_MenuItemControlTypeId = 50011;
const long UIA_ProgressBarControlTypeId = 50012;
const long UIA_RadioButtonControlTypeId = 50013;
const long UIA_ScrollBarControlTypeId = 50014;
const long UIA_SliderControlTypeId = 50015;
const long UIA_TextControlTypeId = 50020;
const long UIA_ToolTipControlTypeId = 50022;
const long UIA_TreeControlTypeId = 50023;
const long UIA_TreeItemControlTypeId = 50024;
const long UIA_CustomControlTypeId = 50025;
const long UIA_GroupControlTypeId = 50026;
const long UIA_DataItemControlTypeId = 50029;
const long UIA_WindowControlTypeId = 50032;
const long UIA_HeaderControlTypeId = 50034;
const long UIA_HeaderItemControlTypeId = 50035;
const long UIA_TableControlTypeId = 50036;
} // namespace Constants
interface IRawElementProviderFragmentRoot;
interface IRawElementProviderFragment;
JUCE_COMCLASS (IRawElementProviderFragmentRoot, "620ce2a5-ab8f-40a9-86cb-de3c75599b58") : public IUnknown
{
public:
JUCE_COMCALL ElementProviderFromPoint (double x, double y, __RPC__deref_out_opt IRawElementProviderFragment** pRetVal) = 0;
JUCE_COMCALL GetFocus (__RPC__deref_out_opt IRawElementProviderFragment * *pRetVal) = 0;
};
JUCE_COMCLASS (IRawElementProviderFragment, "f7063da8-8359-439c-9297-bbc5299a7d87") : public IUnknown
{
public:
JUCE_COMCALL Navigate (NavigateDirection direction, __RPC__deref_out_opt IRawElementProviderFragment** pRetVal) = 0;
JUCE_COMCALL GetRuntimeId (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL get_BoundingRectangle (__RPC__out UiaRect * pRetVal) = 0;
JUCE_COMCALL GetEmbeddedFragmentRoots (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL SetFocus() = 0;
JUCE_COMCALL get_FragmentRoot (__RPC__deref_out_opt IRawElementProviderFragmentRoot * *pRetVal) = 0;
};
JUCE_COMCLASS (IRawElementProviderHwndOverride, "1d5df27c-8947-4425-b8d9-79787bb460b8") : public IUnknown
{
public:
JUCE_COMCALL GetOverrideProviderForHwnd (__RPC__in HWND hwnd,
__RPC__deref_out_opt IRawElementProviderSimple** pRetVal) = 0;
};
JUCE_COMCLASS (IExpandCollapseProvider, "d847d3a5-cab0-4a98-8c32-ecb45c59ad24") : public IUnknown
{
public:
JUCE_COMCALL Expand() = 0;
JUCE_COMCALL Collapse() = 0;
JUCE_COMCALL get_ExpandCollapseState (__RPC__out ExpandCollapseState * pRetVal) = 0;
};
JUCE_COMCLASS (IGridItemProvider, "d02541f1-fb81-4d64-ae32-f520f8a6dbd1") : public IUnknown
{
public:
JUCE_COMCALL get_Row (__RPC__out int* pRetVal) = 0;
JUCE_COMCALL get_Column (__RPC__out int* pRetVal) = 0;
JUCE_COMCALL get_RowSpan (__RPC__out int* pRetVal) = 0;
JUCE_COMCALL get_ColumnSpan (__RPC__out int* pRetVal) = 0;
JUCE_COMCALL get_ContainingGrid (__RPC__deref_out_opt IRawElementProviderSimple * *pRetVal) = 0;
};
JUCE_COMCLASS (IGridProvider, "b17d6187-0907-464b-a168-0ef17a1572b1") : public IUnknown
{
public:
JUCE_COMCALL GetItem (int row, int column, __RPC__deref_out_opt IRawElementProviderSimple** pRetVal) = 0;
JUCE_COMCALL get_RowCount (__RPC__out int* pRetVal) = 0;
JUCE_COMCALL get_ColumnCount (__RPC__out int* pRetVal) = 0;
};
JUCE_COMCLASS (ITableItemProvider, "b9734fa6-771f-4d78-9c90-2517999349cd") : public IUnknown
{
public:
JUCE_COMCALL GetRowHeaderItems (SAFEARRAY** pRetVal) = 0;
JUCE_COMCALL GetColumnHeaderItems (SAFEARRAY** pRetVal) = 0;
};
JUCE_COMCLASS (ITableProvider, "9c860395-97b3-490a-b52a-858cc22af166") : public IUnknown
{
public:
JUCE_COMCALL GetRowHeaders (SAFEARRAY** pRetVal) = 0;
JUCE_COMCALL GetColumnHeaders (SAFEARRAY** pRetVal) = 0;
JUCE_COMCALL get_RowOrColumnMajor (RowOrColumnMajor* pRetVal) = 0;
};
JUCE_COMCLASS (IInvokeProvider, "54fcb24b-e18e-47a2-b4d3-eccbe77599a2") : public IUnknown
{
public:
JUCE_COMCALL Invoke() = 0;
};
JUCE_COMCLASS (IRangeValueProvider, "36dc7aef-33e6-4691-afe1-2be7274b3d33") : public IUnknown
{
public:
JUCE_COMCALL SetValue (double val) = 0;
JUCE_COMCALL get_Value (__RPC__out double* pRetVal) = 0;
JUCE_COMCALL get_IsReadOnly (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_Maximum (__RPC__out double* pRetVal) = 0;
JUCE_COMCALL get_Minimum (__RPC__out double* pRetVal) = 0;
JUCE_COMCALL get_LargeChange (__RPC__out double* pRetVal) = 0;
JUCE_COMCALL get_SmallChange (__RPC__out double* pRetVal) = 0;
};
JUCE_COMCLASS (ISelectionProvider, "fb8b03af-3bdf-48d4-bd36-1a65793be168") : public IUnknown
{
public:
JUCE_COMCALL GetSelection (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL get_CanSelectMultiple (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_IsSelectionRequired (__RPC__out BOOL * pRetVal) = 0;
};
JUCE_COMCLASS (ISelectionProvider2, "14f68475-ee1c-44f6-a869-d239381f0fe7") : public ISelectionProvider
{
JUCE_COMCALL get_FirstSelectedItem (IRawElementProviderSimple * *retVal) = 0;
JUCE_COMCALL get_LastSelectedItem (IRawElementProviderSimple * *retVal) = 0;
JUCE_COMCALL get_CurrentSelectedItem (IRawElementProviderSimple * *retVal) = 0;
JUCE_COMCALL get_ItemCount (int* retVal) = 0;
};
JUCE_COMCLASS (ISelectionItemProvider, "2acad808-b2d4-452d-a407-91ff1ad167b2") : public IUnknown
{
public:
JUCE_COMCALL Select() = 0;
JUCE_COMCALL AddToSelection() = 0;
JUCE_COMCALL RemoveFromSelection() = 0;
JUCE_COMCALL get_IsSelected (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_SelectionContainer (__RPC__deref_out_opt IRawElementProviderSimple * *pRetVal) = 0;
};
JUCE_COMCLASS (ITextRangeProvider, "5347ad7b-c355-46f8-aff5-909033582f63") : public IUnknown
{
public:
JUCE_COMCALL Clone (__RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL Compare (__RPC__in_opt ITextRangeProvider * range, __RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL CompareEndpoints (TextPatternRangeEndpoint endpoint, __RPC__in_opt ITextRangeProvider * targetRange, TextPatternRangeEndpoint targetEndpoint, __RPC__out int* pRetVal) = 0;
JUCE_COMCALL ExpandToEnclosingUnit (TextUnit unit) = 0;
JUCE_COMCALL FindAttribute (TEXTATTRIBUTEID attributeId, VARIANT val, BOOL backward, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL FindText (__RPC__in BSTR text, BOOL backward, BOOL ignoreCase, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL GetAttributeValue (TEXTATTRIBUTEID attributeId, __RPC__out VARIANT * pRetVal) = 0;
JUCE_COMCALL GetBoundingRectangles (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL GetEnclosingElement (__RPC__deref_out_opt IRawElementProviderSimple * *pRetVal) = 0;
JUCE_COMCALL GetText (int maxLength, __RPC__deref_out_opt BSTR* pRetVal) = 0;
JUCE_COMCALL Move (TextUnit unit, int count, __RPC__out int* pRetVal) = 0;
JUCE_COMCALL MoveEndpointByUnit (TextPatternRangeEndpoint endpoint, TextUnit unit, int count, __RPC__out int* pRetVal) = 0;
JUCE_COMCALL MoveEndpointByRange (TextPatternRangeEndpoint endpoint, __RPC__in_opt ITextRangeProvider * targetRange, TextPatternRangeEndpoint targetEndpoint) = 0;
JUCE_COMCALL Select() = 0;
JUCE_COMCALL AddToSelection() = 0;
JUCE_COMCALL RemoveFromSelection() = 0;
JUCE_COMCALL ScrollIntoView (BOOL alignToTop) = 0;
JUCE_COMCALL GetChildren (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
};
JUCE_COMCLASS (ITextProvider, "3589c92c-63f3-4367-99bb-ada653b77cf2") : public IUnknown
{
public:
JUCE_COMCALL GetSelection (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL GetVisibleRanges (__RPC__deref_out_opt SAFEARRAY * *pRetVal) = 0;
JUCE_COMCALL RangeFromChild (__RPC__in_opt IRawElementProviderSimple * childElement, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL RangeFromPoint (UiaPoint point, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL get_DocumentRange (__RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL get_SupportedTextSelection (__RPC__out SupportedTextSelection * pRetVal) = 0;
};
JUCE_COMCLASS (ITextProvider2, "0dc5e6ed-3e16-4bf1-8f9a-a979878bc195") : public ITextProvider
{
public:
JUCE_COMCALL RangeFromAnnotation (__RPC__in_opt IRawElementProviderSimple * annotationElement, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
JUCE_COMCALL GetCaretRange (__RPC__out BOOL * isActive, __RPC__deref_out_opt ITextRangeProvider * *pRetVal) = 0;
};
JUCE_COMCLASS (IToggleProvider, "56d00bd0-c4f4-433c-a836-1a52a57e0892") : public IUnknown
{
public:
JUCE_COMCALL Toggle() = 0;
JUCE_COMCALL get_ToggleState (__RPC__out ToggleState * pRetVal) = 0;
};
JUCE_COMCLASS (ITransformProvider, "6829ddc4-4f91-4ffa-b86f-bd3e2987cb4c") : public IUnknown
{
public:
JUCE_COMCALL Move (double x, double y) = 0;
JUCE_COMCALL Resize (double width, double height) = 0;
JUCE_COMCALL Rotate (double degrees) = 0;
JUCE_COMCALL get_CanMove (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_CanResize (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_CanRotate (__RPC__out BOOL * pRetVal) = 0;
};
JUCE_COMCLASS (IValueProvider, "c7935180-6fb3-4201-b174-7df73adbf64a") : public IUnknown
{
public:
JUCE_COMCALL SetValue (__RPC__in LPCWSTR val) = 0;
JUCE_COMCALL get_Value (__RPC__deref_out_opt BSTR * pRetVal) = 0;
JUCE_COMCALL get_IsReadOnly (__RPC__out BOOL * pRetVal) = 0;
};
JUCE_COMCLASS (IWindowProvider, "987df77b-db06-4d77-8f8a-86a9c3bb90b9") : public IUnknown
{
public:
JUCE_COMCALL SetVisualState (WindowVisualState state) = 0;
JUCE_COMCALL Close() = 0;
JUCE_COMCALL WaitForInputIdle (int milliseconds, __RPC__out BOOL* pRetVal) = 0;
JUCE_COMCALL get_CanMaximize (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_CanMinimize (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_IsModal (__RPC__out BOOL * pRetVal) = 0;
JUCE_COMCALL get_WindowVisualState (__RPC__out WindowVisualState * pRetVal) = 0;
JUCE_COMCALL get_WindowInteractionState (__RPC__out WindowInteractionState * pRetVal) = 0;
JUCE_COMCALL get_IsTopmost (__RPC__out BOOL * pRetVal) = 0;
};
JUCE_COMCLASS (IScrollProvider, "b38b8077-1fc3-42a5-8cae-d40c2215055a") : public IUnknown
{
public:
JUCE_COMCALL Scroll (ScrollAmount horizontalAmount, ScrollAmount verticalAmount) = 0;
JUCE_COMCALL SetScrollPercent (double horizontalPercent,double verticalPercent) = 0;
JUCE_COMCALL get_HorizontalScrollPercent (double* pRetVal) = 0;
JUCE_COMCALL get_VerticalScrollPercent (double* pRetVal) = 0;
JUCE_COMCALL get_HorizontalViewSize (double* pRetVal) = 0;
JUCE_COMCALL get_VerticalViewSize (double* pRetVal) = 0;
JUCE_COMCALL get_HorizontallyScrollable (BOOL* pRetVal) = 0;
JUCE_COMCALL get_VerticallyScrollable (BOOL* pRetVal) = 0;
};
JUCE_COMCLASS (IScrollItemProvider, "2360c714-4bf1-4b26-ba65-9b21316127eb") : public IUnknown
{
public:
JUCE_COMCALL ScrollIntoView() = 0;
};
constexpr CLSID CLSID_SpVoice { 0x96749377, 0x3391, 0x11D2, { 0x9E, 0xE3, 0x00, 0xC0, 0x4F, 0x79, 0x73, 0x96 } };
} // namespace juce::ComTypes
#ifdef __CRT_UUID_DECL
__CRT_UUID_DECL (juce::ComTypes::IRawElementProviderFragmentRoot, 0x620ce2a5, 0xab8f, 0x40a9, 0x86, 0xcb, 0xde, 0x3c, 0x75, 0x59, 0x9b, 0x58)
__CRT_UUID_DECL (juce::ComTypes::IRawElementProviderFragment, 0xf7063da8, 0x8359, 0x439c, 0x92, 0x97, 0xbb, 0xc5, 0x29, 0x9a, 0x7d, 0x87)
__CRT_UUID_DECL (juce::ComTypes::IRawElementProviderHwndOverride, 0x1d5df27c, 0x8947, 0x4425, 0xb8, 0xd9, 0x79, 0x78, 0x7b, 0xb4, 0x60, 0xb8)
__CRT_UUID_DECL (juce::ComTypes::IExpandCollapseProvider, 0xd847d3a5, 0xcab0, 0x4a98, 0x8c, 0x32, 0xec, 0xb4, 0x5c, 0x59, 0xad, 0x24)
__CRT_UUID_DECL (juce::ComTypes::IGridItemProvider, 0xd02541f1, 0xfb81, 0x4d64, 0xae, 0x32, 0xf5, 0x20, 0xf8, 0xa6, 0xdb, 0xd1)
__CRT_UUID_DECL (juce::ComTypes::IGridProvider, 0xb17d6187, 0x0907, 0x464b, 0xa1, 0x68, 0x0e, 0xf1, 0x7a, 0x15, 0x72, 0xb1)
__CRT_UUID_DECL (juce::ComTypes::IInvokeProvider, 0x54fcb24b, 0xe18e, 0x47a2, 0xb4, 0xd3, 0xec, 0xcb, 0xe7, 0x75, 0x99, 0xa2)
__CRT_UUID_DECL (juce::ComTypes::IRangeValueProvider, 0x36dc7aef, 0x33e6, 0x4691, 0xaf, 0xe1, 0x2b, 0xe7, 0x27, 0x4b, 0x3d, 0x33)
__CRT_UUID_DECL (juce::ComTypes::ISelectionProvider, 0xfb8b03af, 0x3bdf, 0x48d4, 0xbd, 0x36, 0x1a, 0x65, 0x79, 0x3b, 0xe1, 0x68)
__CRT_UUID_DECL (juce::ComTypes::ISelectionProvider2, 0x14f68475, 0xee1c, 0x44f6, 0xa8, 0x69, 0xd2, 0x39, 0x38, 0x1f, 0x0f, 0xe7)
__CRT_UUID_DECL (juce::ComTypes::ISelectionItemProvider, 0x2acad808, 0xb2d4, 0x452d, 0xa4, 0x07, 0x91, 0xff, 0x1a, 0xd1, 0x67, 0xb2)
__CRT_UUID_DECL (juce::ComTypes::ITextRangeProvider, 0x5347ad7b, 0xc355, 0x46f8, 0xaf, 0xf5, 0x90, 0x90, 0x33, 0x58, 0x2f, 0x63)
__CRT_UUID_DECL (juce::ComTypes::ITextProvider, 0x3589c92c, 0x63f3, 0x4367, 0x99, 0xbb, 0xad, 0xa6, 0x53, 0xb7, 0x7c, 0xf2)
__CRT_UUID_DECL (juce::ComTypes::ITextProvider2, 0x0dc5e6ed, 0x3e16, 0x4bf1, 0x8f, 0x9a, 0xa9, 0x79, 0x87, 0x8b, 0xc1, 0x95)
__CRT_UUID_DECL (juce::ComTypes::IToggleProvider, 0x56d00bd0, 0xc4f4, 0x433c, 0xa8, 0x36, 0x1a, 0x52, 0xa5, 0x7e, 0x08, 0x92)
__CRT_UUID_DECL (juce::ComTypes::ITransformProvider, 0x6829ddc4, 0x4f91, 0x4ffa, 0xb8, 0x6f, 0xbd, 0x3e, 0x29, 0x87, 0xcb, 0x4c)
__CRT_UUID_DECL (juce::ComTypes::IValueProvider, 0xc7935180, 0x6fb3, 0x4201, 0xb1, 0x74, 0x7d, 0xf7, 0x3a, 0xdb, 0xf6, 0x4a)
__CRT_UUID_DECL (juce::ComTypes::IWindowProvider, 0x987df77b, 0xdb06, 0x4d77, 0x8f, 0x8a, 0x86, 0xa9, 0xc3, 0xbb, 0x90, 0xb9)
__CRT_UUID_DECL (juce::ComTypes::ITableItemProvider, 0xb9734fa6, 0x771f, 0x4d78, 0x9c, 0x90, 0x25, 0x17, 0x99, 0x93, 0x49, 0xcd)
__CRT_UUID_DECL (juce::ComTypes::ITableProvider, 0x9c860395, 0x97b3, 0x490a, 0xb5, 0x2a, 0x85, 0x8c, 0xc2, 0x2a, 0xf1, 0x66)
__CRT_UUID_DECL (juce::ComTypes::IScrollProvider, 0xb38b8077, 0x1fc3, 0x42a5, 0x8c, 0xae, 0xd4, 0x0c, 0x22, 0x15, 0x05, 0x5a)
__CRT_UUID_DECL (juce::ComTypes::IScrollItemProvider, 0x2360c714, 0x4bf1, 0x4b26, 0xba, 0x65, 0x9b, 0x21, 0x31, 0x61, 0x27, 0xeb)
#endif
@@ -0,0 +1,85 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAExpandCollapseProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IExpandCollapseProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT Expand() override
{
return invokeShowMenu();
}
JUCE_COMRESULT Collapse() override
{
return invokeShowMenu();
}
JUCE_COMRESULT get_ExpandCollapseState (ComTypes::ExpandCollapseState* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = getHandler().getCurrentState().isExpanded()
? ComTypes::ExpandCollapseState_Expanded
: ComTypes::ExpandCollapseState_Collapsed;
return S_OK;
});
}
private:
JUCE_COMRESULT invokeShowMenu()
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
if (handler.getActions().invoke (AccessibilityActionType::showMenu))
{
using namespace ComTypes::Constants;
sendAccessibilityAutomationEvent (handler, handler.getCurrentState().isExpanded()
? UIA_MenuOpenedEventId
: UIA_MenuClosedEventId);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAExpandCollapseProvider)
};
} // namespace juce
@@ -0,0 +1,153 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAGridItemProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IGridItemProvider, ComTypes::ITableItemProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT get_Row (int* pRetVal) override
{
return withTableSpan (pRetVal,
&AccessibilityTableInterface::getRowSpan,
&AccessibilityTableInterface::Span::begin);
}
JUCE_COMRESULT get_Column (int* pRetVal) override
{
return withTableSpan (pRetVal,
&AccessibilityTableInterface::getColumnSpan,
&AccessibilityTableInterface::Span::begin);
}
JUCE_COMRESULT get_RowSpan (int* pRetVal) override
{
return withTableSpan (pRetVal,
&AccessibilityTableInterface::getRowSpan,
&AccessibilityTableInterface::Span::num);
}
JUCE_COMRESULT get_ColumnSpan (int* pRetVal) override
{
return withTableSpan (pRetVal,
&AccessibilityTableInterface::getColumnSpan,
&AccessibilityTableInterface::Span::num);
}
JUCE_COMRESULT get_ContainingGrid (IRawElementProviderSimple** pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityHandler& tableHandler)
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
tableHandler.getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
return true;
});
}
JUCE_COMRESULT GetRowHeaderItems (SAFEARRAY**) override
{
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT GetColumnHeaderItems (SAFEARRAY** pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityHandler& tableHandler)
{
if (auto* tableInterface = tableHandler.getTableInterface())
{
if (const auto column = tableInterface->getColumnSpan (getHandler()))
{
if (auto* header = tableInterface->getHeaderHandler())
{
const auto children = header->getChildren();
if (isPositiveAndBelow (column->begin, children.size()))
{
ComSmartPtr<IRawElementProviderSimple> provider;
if (auto* child = children[(size_t) column->begin])
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
if (child->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress())) == S_OK && provider != nullptr)
{
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1);
LONG index = 0;
const auto hr = SafeArrayPutElement (*pRetVal, &index, provider);
return ! FAILED (hr);
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
}
}
}
}
return false;
});
}
private:
template <typename Value, typename Callback>
JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* handler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface))
if (handler->getTableInterface() != nullptr && callback (*handler))
return S_OK;
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT withTableSpan (int* pRetVal,
Optional<AccessibilityTableInterface::Span> (AccessibilityTableInterface::* getSpan) (const AccessibilityHandler&) const,
int AccessibilityTableInterface::Span::* spanMember) const
{
return withTableInterface (pRetVal, [&] (const AccessibilityHandler& handler)
{
if (const auto span = ((handler.getTableInterface())->*getSpan) (getHandler()))
{
*pRetVal = (*span).*spanMember;
return true;
}
return false;
});
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridItemProvider)
};
} // namespace juce
@@ -0,0 +1,151 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAGridProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IGridProvider, ComTypes::ITableProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT GetItem (int row, int column, IRawElementProviderSimple** pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface)
{
if (! isPositiveAndBelow (row, tableInterface.getNumRows())
|| ! isPositiveAndBelow (column, tableInterface.getNumColumns()))
return E_INVALIDARG;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
if (auto* cellHandler = tableInterface.getCellHandler (row, column))
{
cellHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
}
if (auto* rowHandler = tableInterface.getRowHandler (row))
{
rowHandler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
return E_FAIL;
});
}
JUCE_COMRESULT get_RowCount (int* pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface)
{
*pRetVal = tableInterface.getNumRows();
return S_OK;
});
}
JUCE_COMRESULT get_ColumnCount (int* pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface)
{
*pRetVal = tableInterface.getNumColumns();
return S_OK;
});
}
JUCE_COMRESULT GetRowHeaders (SAFEARRAY**) override
{
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT GetColumnHeaders (SAFEARRAY** pRetVal) override
{
return withTableInterface (pRetVal, [&] (const AccessibilityTableInterface& tableInterface)
{
if (auto* header = tableInterface.getHeaderHandler())
{
const auto children = header->getChildren();
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, (ULONG) children.size());
LONG index = 0;
for (const auto& child : children)
{
ComSmartPtr<IRawElementProviderSimple> provider;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
if (child != nullptr)
child->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
if (provider == nullptr)
return E_FAIL;
const auto hr = SafeArrayPutElement (*pRetVal, &index, provider);
if (FAILED (hr))
return E_FAIL;
++index;
}
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_RowOrColumnMajor (ComTypes::RowOrColumnMajor* pRetVal) override
{
*pRetVal = ComTypes::RowOrColumnMajor_RowMajor;
return S_OK;
}
private:
template <typename Value, typename Callback>
JUCE_COMRESULT withTableInterface (Value* pRetVal, Callback&& callback) const
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* tableHandler = detail::AccessibilityHelpers::getEnclosingHandlerWithInterface (&getHandler(), &AccessibilityHandler::getTableInterface))
if (auto* tableInterface = tableHandler->getTableInterface())
return callback (*tableInterface);
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAGridProvider)
};
} // namespace juce
@@ -0,0 +1,119 @@
/*
==============================================================================
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
{
namespace VariantHelpers
{
namespace Detail
{
template <typename Fn, typename ValueType>
inline VARIANT getWithValueGeneric (Fn&& setter, ValueType value)
{
VARIANT result{};
setter (value, &result);
return result;
}
}
inline void clear (VARIANT* variant)
{
variant->vt = VT_EMPTY;
}
inline void setInt (int value, VARIANT* variant)
{
variant->vt = VT_I4;
variant->lVal = value;
}
inline void setBool (bool value, VARIANT* variant)
{
variant->vt = VT_BOOL;
variant->boolVal = value ? -1 : 0;
}
inline void setString (const String& value, VARIANT* variant)
{
variant->vt = VT_BSTR;
variant->bstrVal = SysAllocString ((const OLECHAR*) value.toWideCharPointer());
}
inline void setDouble (double value, VARIANT* variant)
{
variant->vt = VT_R8;
variant->dblVal = value;
}
inline VARIANT getWithValue (double value) { return Detail::getWithValueGeneric (&setDouble, value); }
inline VARIANT getWithValue (const String& value) { return Detail::getWithValueGeneric (&setString, value); }
}
inline JUCE_COMRESULT addHandlersToArray (const std::vector<const AccessibilityHandler*>& handlers, SAFEARRAY** pRetVal)
{
auto numHandlers = handlers.size();
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, (ULONG) numHandlers);
if (pRetVal != nullptr)
{
for (LONG i = 0; i < (LONG) numHandlers; ++i)
{
auto* handler = handlers[(size_t) i];
if (handler == nullptr)
continue;
ComSmartPtr<IRawElementProviderSimple> provider;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
handler->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (provider.resetAndGetPointerAddress()));
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
auto hr = SafeArrayPutElement (*pRetVal, &i, provider);
if (FAILED (hr))
return E_FAIL;
}
}
return S_OK;
}
template <typename Value, typename Object, typename Callback>
inline JUCE_COMRESULT withCheckedComArgs (Value* pRetVal, Object& handle, Callback&& callback)
{
if (pRetVal == nullptr)
return E_INVALIDARG;
*pRetVal = Value{};
if (! handle.isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
return callback();
}
} // namespace juce
@@ -0,0 +1,61 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAInvokeProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IInvokeProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT Invoke() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
if (handler.getActions().invoke (AccessibilityActionType::press))
{
using namespace ComTypes::Constants;
if (isElementValid())
sendAccessibilityAutomationEvent (handler, UIA_Invoke_InvokedEventId);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAInvokeProvider)
};
} // namespace juce
@@ -0,0 +1,58 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAProviderBase
{
public:
explicit UIAProviderBase (AccessibilityNativeHandle* nativeHandleIn)
: nativeHandle (nativeHandleIn)
{
}
bool isElementValid() const
{
if (nativeHandle != nullptr)
return nativeHandle->isElementValid();
return false;
}
const AccessibilityHandler& getHandler() const
{
return nativeHandle->getHandler();
}
private:
ComSmartPtr<AccessibilityNativeHandle> nativeHandle;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAProviderBase)
};
} // namespace juce
@@ -0,0 +1,43 @@
/*
==============================================================================
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
{
void sendAccessibilityAutomationEvent (const AccessibilityHandler&, EVENTID);
void sendAccessibilityPropertyChangedEvent (const AccessibilityHandler&, PROPERTYID, VARIANT);
} // namespace juce
#include "juce_UIAProviderBase_windows.h"
#include "juce_UIAExpandCollapseProvider_windows.h"
#include "juce_UIAGridItemProvider_windows.h"
#include "juce_UIAGridProvider_windows.h"
#include "juce_UIAInvokeProvider_windows.h"
#include "juce_UIARangeValueProvider_windows.h"
#include "juce_UIASelectionProvider_windows.h"
#include "juce_UIATextProvider_windows.h"
#include "juce_UIAToggleProvider_windows.h"
#include "juce_UIATransformProvider_windows.h"
#include "juce_UIAValueProvider_windows.h"
#include "juce_UIAWindowProvider_windows.h"
@@ -0,0 +1,137 @@
/*
==============================================================================
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
{
//==============================================================================
class UIARangeValueProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IRangeValueProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT SetValue (double val) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
if (auto* valueInterface = handler.getValueInterface())
{
auto range = valueInterface->getRange();
if (range.isValid())
{
if (val < range.getMinimumValue() || val > range.getMaximumValue())
return E_INVALIDARG;
if (! valueInterface->isReadOnly())
{
valueInterface->setValue (val);
VARIANT newValue;
VariantHelpers::setDouble (valueInterface->getCurrentValue(), &newValue);
sendAccessibilityPropertyChangedEvent (handler, UIA_RangeValueValuePropertyId, newValue);
return S_OK;
}
}
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT get_Value (double* pRetVal) override
{
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface)
{
return valueInterface.getCurrentValue();
});
}
JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override
{
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface)
{
return valueInterface.isReadOnly();
});
}
JUCE_COMRESULT get_Maximum (double* pRetVal) override
{
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface)
{
return valueInterface.getRange().getMaximumValue();
});
}
JUCE_COMRESULT get_Minimum (double* pRetVal) override
{
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface)
{
return valueInterface.getRange().getMinimumValue();
});
}
JUCE_COMRESULT get_LargeChange (double* pRetVal) override
{
return get_SmallChange (pRetVal);
}
JUCE_COMRESULT get_SmallChange (double* pRetVal) override
{
return withValueInterface (pRetVal, [] (const AccessibilityValueInterface& valueInterface)
{
return valueInterface.getRange().getInterval();
});
}
private:
template <typename Value, typename Callback>
JUCE_COMRESULT withValueInterface (Value* pRetVal, Callback&& callback) const
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* valueInterface = getHandler().getValueInterface())
{
if (valueInterface->getRange().isValid())
{
*pRetVal = callback (*valueInterface);
return S_OK;
}
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIARangeValueProvider)
};
} // namespace juce
@@ -0,0 +1,247 @@
/*
==============================================================================
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
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
//==============================================================================
class UIASelectionItemProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::ISelectionItemProvider>
{
public:
explicit UIASelectionItemProvider (AccessibilityNativeHandle* handle)
: UIAProviderBase (handle),
isRadioButton (getHandler().getRole() == AccessibilityRole::radioButton)
{
}
//==============================================================================
JUCE_COMRESULT AddToSelection() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
if (isRadioButton)
{
using namespace ComTypes::Constants;
handler.getActions().invoke (AccessibilityActionType::press);
sendAccessibilityAutomationEvent (handler, UIA_SelectionItem_ElementSelectedEventId);
return S_OK;
}
handler.getActions().invoke (AccessibilityActionType::toggle);
handler.getActions().invoke (AccessibilityActionType::press);
return S_OK;
}
JUCE_COMRESULT get_IsSelected (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
const auto state = getHandler().getCurrentState();
*pRetVal = isRadioButton ? state.isChecked() : state.isSelected();
return S_OK;
});
}
JUCE_COMRESULT get_SelectionContainer (IRawElementProviderSimple** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
if (! isRadioButton)
if (auto* parent = getHandler().getParent())
parent->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT RemoveFromSelection() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (! isRadioButton)
{
const auto& handler = getHandler();
if (handler.getCurrentState().isSelected())
getHandler().getActions().invoke (AccessibilityActionType::toggle);
}
return S_OK;
}
JUCE_COMRESULT Select() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
AddToSelection();
if (isElementValid() && ! isRadioButton)
{
const auto& handler = getHandler();
if (auto* parent = handler.getParent())
for (auto* child : parent->getChildren())
if (child != &handler && child->getCurrentState().isSelected())
child->getActions().invoke (AccessibilityActionType::toggle);
}
return S_OK;
}
private:
const bool isRadioButton;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionItemProvider)
};
//==============================================================================
class UIASelectionProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::ISelectionProvider2>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override
{
if (iid == __uuidof (IUnknown) || iid == __uuidof (ComTypes::ISelectionProvider))
return castToType<ComTypes::ISelectionProvider> (result);
if (iid == __uuidof (ComTypes::ISelectionProvider2))
return castToType<ComTypes::ISelectionProvider2> (result);
*result = nullptr;
return E_NOINTERFACE;
}
//==============================================================================
JUCE_COMRESULT get_CanSelectMultiple (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = isMultiSelectable();
return S_OK;
});
}
JUCE_COMRESULT get_IsSelectionRequired (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = getSelectedChildren().size() > 0 && ! isMultiSelectable();
return S_OK;
});
}
JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
return addHandlersToArray (getSelectedChildren(), pRetVal);
});
}
//==============================================================================
JUCE_COMRESULT get_FirstSelectedItem (IRawElementProviderSimple** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto selectedChildren = getSelectedChildren();
if (! selectedChildren.empty())
selectedChildren.front()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT get_LastSelectedItem (IRawElementProviderSimple** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto selectedChildren = getSelectedChildren();
if (! selectedChildren.empty())
selectedChildren.back()->getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
}
JUCE_COMRESULT get_CurrentSelectedItem (IRawElementProviderSimple** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
get_FirstSelectedItem (pRetVal);
return S_OK;
});
}
JUCE_COMRESULT get_ItemCount (int* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = (int) getSelectedChildren().size();
return S_OK;
});
}
private:
bool isMultiSelectable() const noexcept
{
return getHandler().getCurrentState().isMultiSelectable();
}
std::vector<const AccessibilityHandler*> getSelectedChildren() const
{
std::vector<const AccessibilityHandler*> selectedHandlers;
for (auto* child : getHandler().getComponent().getChildren())
if (auto* handler = child->getAccessibilityHandler())
if (handler->getCurrentState().isSelected())
selectedHandlers.push_back (handler);
return selectedHandlers;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIASelectionProvider)
};
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
} // namespace juce
@@ -0,0 +1,619 @@
/*
==============================================================================
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
{
//==============================================================================
class UIATextProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::ITextProvider2>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT QueryInterface (REFIID iid, void** result) override
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
if (iid == __uuidof (IUnknown) || iid == __uuidof (ComTypes::ITextProvider))
return castToType<ComTypes::ITextProvider> (result);
if (iid == __uuidof (ComTypes::ITextProvider2))
return castToType<ComTypes::ITextProvider2> (result);
*result = nullptr;
return E_NOINTERFACE;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
//==============================================================================
JUCE_COMRESULT get_DocumentRange (ComTypes::ITextRangeProvider** pRetVal) override
{
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
*pRetVal = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() });
return S_OK;
});
}
JUCE_COMRESULT get_SupportedTextSelection (ComTypes::SupportedTextSelection* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = ComTypes::SupportedTextSelection_Single;
return S_OK;
});
}
JUCE_COMRESULT GetSelection (SAFEARRAY** pRetVal) override
{
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1);
if (pRetVal != nullptr)
{
auto selection = textInterface.getSelection();
auto hasSelection = ! selection.isEmpty();
auto cursorPos = textInterface.getTextInsertionOffset();
auto* rangeProvider = new UIATextRangeProvider (*this,
{ hasSelection ? selection.getStart() : cursorPos,
hasSelection ? selection.getEnd() : cursorPos });
LONG pos = 0;
auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast<IUnknown*> (rangeProvider));
if (FAILED (hr))
return E_FAIL;
rangeProvider->Release();
}
return S_OK;
});
}
JUCE_COMRESULT GetVisibleRanges (SAFEARRAY** pRetVal) override
{
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 1);
if (pRetVal != nullptr)
{
auto* rangeProvider = new UIATextRangeProvider (*this, { 0, textInterface.getTotalNumCharacters() });
LONG pos = 0;
auto hr = SafeArrayPutElement (*pRetVal, &pos, static_cast<IUnknown*> (rangeProvider));
if (FAILED (hr))
return E_FAIL;
rangeProvider->Release();
}
return S_OK;
});
}
JUCE_COMRESULT RangeFromChild (IRawElementProviderSimple*, ComTypes::ITextRangeProvider** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, []
{
return S_OK;
});
}
JUCE_COMRESULT RangeFromPoint (ComTypes::UiaPoint point, ComTypes::ITextRangeProvider** pRetVal) override
{
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
auto offset = textInterface.getOffsetAtPoint ({ roundToInt (point.x), roundToInt (point.y) });
if (offset > 0)
*pRetVal = new UIATextRangeProvider (*this, { offset, offset });
return S_OK;
});
}
//==============================================================================
JUCE_COMRESULT GetCaretRange (BOOL* isActive, ComTypes::ITextRangeProvider** pRetVal) override
{
return withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
*isActive = getHandler().hasFocus (false);
auto cursorPos = textInterface.getTextInsertionOffset();
*pRetVal = new UIATextRangeProvider (*this, { cursorPos, cursorPos });
return S_OK;
});
}
JUCE_COMRESULT RangeFromAnnotation (IRawElementProviderSimple*, ComTypes::ITextRangeProvider** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, []
{
return S_OK;
});
}
private:
//==============================================================================
template <typename Value, typename Callback>
JUCE_COMRESULT withTextInterface (Value* pRetVal, Callback&& callback) const
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* textInterface = getHandler().getTextInterface())
return callback (*textInterface);
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
//==============================================================================
class UIATextRangeProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::ITextRangeProvider>
{
public:
UIATextRangeProvider (UIATextProvider& textProvider, Range<int> range)
: UIAProviderBase (textProvider.getHandler().getNativeImplementation()),
owner (&textProvider),
selectionRange (range)
{
}
//==============================================================================
Range<int> getSelectionRange() const noexcept { return selectionRange; }
//==============================================================================
JUCE_COMRESULT AddToSelection() override
{
return Select();
}
JUCE_COMRESULT Clone (ComTypes::ITextRangeProvider** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = new UIATextRangeProvider (*owner, selectionRange);
return S_OK;
});
}
JUCE_COMRESULT Compare (ComTypes::ITextRangeProvider* range, BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = (selectionRange == static_cast<UIATextRangeProvider*> (range)->getSelectionRange());
return S_OK;
});
}
JUCE_COMRESULT CompareEndpoints (ComTypes::TextPatternRangeEndpoint endpoint,
ComTypes::ITextRangeProvider* targetRange,
ComTypes::TextPatternRangeEndpoint targetEndpoint,
int* pRetVal) override
{
if (targetRange == nullptr)
return E_INVALIDARG;
return withCheckedComArgs (pRetVal, *this, [&]
{
auto offset = (endpoint == ComTypes::TextPatternRangeEndpoint_Start ? selectionRange.getStart()
: selectionRange.getEnd());
auto otherRange = static_cast<UIATextRangeProvider*> (targetRange)->getSelectionRange();
auto otherOffset = (targetEndpoint == ComTypes::TextPatternRangeEndpoint_Start ? otherRange.getStart()
: otherRange.getEnd());
*pRetVal = offset - otherOffset;
return S_OK;
});
}
JUCE_COMRESULT ExpandToEnclosingUnit (ComTypes::TextUnit unit) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* textInterface = owner->getHandler().getTextInterface())
{
using ATH = AccessibilityTextHelpers;
const auto boundaryType = getBoundaryType (unit);
const auto start = ATH::findTextBoundary (*textInterface,
selectionRange.getStart(),
boundaryType,
ATH::Direction::backwards,
ATH::IncludeThisBoundary::yes,
ATH::IncludeWhitespaceAfterWords::no);
const auto end = ATH::findTextBoundary (*textInterface,
start,
boundaryType,
ATH::Direction::forwards,
ATH::IncludeThisBoundary::no,
ATH::IncludeWhitespaceAfterWords::yes);
selectionRange = Range<int> (start, end);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT FindAttribute (TEXTATTRIBUTEID, VARIANT, BOOL, ComTypes::ITextRangeProvider** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, []
{
return S_OK;
});
}
JUCE_COMRESULT FindText (BSTR text, BOOL backward, BOOL ignoreCase,
ComTypes::ITextRangeProvider** pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
auto selectionText = textInterface.getText (selectionRange);
String textToSearchFor (text);
auto offset = (backward ? (ignoreCase ? selectionText.lastIndexOfIgnoreCase (textToSearchFor) : selectionText.lastIndexOf (textToSearchFor))
: (ignoreCase ? selectionText.indexOfIgnoreCase (textToSearchFor) : selectionText.indexOf (textToSearchFor)));
if (offset != -1)
*pRetVal = new UIATextRangeProvider (*owner, { offset, offset + textToSearchFor.length() });
return S_OK;
});
}
JUCE_COMRESULT GetAttributeValue (TEXTATTRIBUTEID attributeId, VARIANT* pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
VariantHelpers::clear (pRetVal);
using namespace ComTypes::Constants;
switch (attributeId)
{
case UIA_IsReadOnlyAttributeId:
{
VariantHelpers::setBool (textInterface.isReadOnly(), pRetVal);
break;
}
case UIA_CaretPositionAttributeId:
{
auto cursorPos = textInterface.getTextInsertionOffset();
auto caretPos = [&]
{
if (cursorPos == 0)
return ComTypes::CaretPosition_BeginningOfLine;
if (cursorPos == textInterface.getTotalNumCharacters())
return ComTypes::CaretPosition_EndOfLine;
return ComTypes::CaretPosition_Unknown;
}();
VariantHelpers::setInt (caretPos, pRetVal);
break;
}
}
return S_OK;
});
}
JUCE_COMRESULT GetBoundingRectangles (SAFEARRAY** pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
auto rectangleList = textInterface.getTextBounds (selectionRange);
auto numRectangles = rectangleList.getNumRectangles();
*pRetVal = SafeArrayCreateVector (VT_R8, 0, 4 * (ULONG) numRectangles);
if (*pRetVal == nullptr)
return E_FAIL;
if (numRectangles > 0)
{
double* doubleArr = nullptr;
if (FAILED (SafeArrayAccessData (*pRetVal, reinterpret_cast<void**> (&doubleArr))))
{
SafeArrayDestroy (*pRetVal);
return E_FAIL;
}
for (int i = 0; i < numRectangles; ++i)
{
auto r = Desktop::getInstance().getDisplays().logicalToPhysical (rectangleList.getRectangle (i));
doubleArr[i * 4] = r.getX();
doubleArr[i * 4 + 1] = r.getY();
doubleArr[i * 4 + 2] = r.getWidth();
doubleArr[i * 4 + 3] = r.getHeight();
}
if (FAILED (SafeArrayUnaccessData (*pRetVal)))
{
SafeArrayDestroy (*pRetVal);
return E_FAIL;
}
}
return S_OK;
});
}
JUCE_COMRESULT GetChildren (SAFEARRAY** pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = SafeArrayCreateVector (VT_UNKNOWN, 0, 0);
return S_OK;
});
}
JUCE_COMRESULT GetEnclosingElement (IRawElementProviderSimple** pRetVal) override
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
return withCheckedComArgs (pRetVal, *this, [&]
{
getHandler().getNativeImplementation()->QueryInterface (IID_PPV_ARGS (pRetVal));
return S_OK;
});
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
JUCE_COMRESULT GetText (int maxLength, BSTR* pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
auto text = textInterface.getText (selectionRange);
if (maxLength >= 0 && text.length() > maxLength)
text = text.substring (0, maxLength);
*pRetVal = SysAllocString ((const OLECHAR*) text.toWideCharPointer());
return S_OK;
});
}
JUCE_COMRESULT Move (ComTypes::TextUnit unit, int count, int* pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
using ATH = AccessibilityTextHelpers;
const auto boundaryType = getBoundaryType (unit);
const auto previousUnitBoundary = ATH::findTextBoundary (textInterface,
selectionRange.getStart(),
boundaryType,
ATH::Direction::backwards,
ATH::IncludeThisBoundary::yes,
ATH::IncludeWhitespaceAfterWords::no);
auto numMoved = 0;
auto movedEndpoint = previousUnitBoundary;
for (; numMoved < std::abs (count); ++numMoved)
{
const auto nextEndpoint = ATH::findTextBoundary (textInterface,
movedEndpoint,
boundaryType,
count > 0 ? ATH::Direction::forwards : ATH::Direction::backwards,
ATH::IncludeThisBoundary::no,
count > 0 ? ATH::IncludeWhitespaceAfterWords::yes : ATH::IncludeWhitespaceAfterWords::no);
if (nextEndpoint == movedEndpoint)
break;
movedEndpoint = nextEndpoint;
}
*pRetVal = numMoved;
ExpandToEnclosingUnit (unit);
return S_OK;
});
}
JUCE_COMRESULT MoveEndpointByRange (ComTypes::TextPatternRangeEndpoint endpoint,
ComTypes::ITextRangeProvider* targetRange,
ComTypes::TextPatternRangeEndpoint targetEndpoint) override
{
if (targetRange == nullptr)
return E_INVALIDARG;
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (owner->getHandler().getTextInterface() != nullptr)
{
auto otherRange = static_cast<UIATextRangeProvider*> (targetRange)->getSelectionRange();
auto targetPoint = (targetEndpoint == ComTypes::TextPatternRangeEndpoint_Start ? otherRange.getStart()
: otherRange.getEnd());
setEndpointChecked (endpoint, targetPoint);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT MoveEndpointByUnit (ComTypes::TextPatternRangeEndpoint endpoint,
ComTypes::TextUnit unit,
int count,
int* pRetVal) override
{
return owner->withTextInterface (pRetVal, [&] (const AccessibilityTextInterface& textInterface)
{
if (count == 0 || textInterface.getTotalNumCharacters() == 0)
return S_OK;
const auto endpointToMove = (endpoint == ComTypes::TextPatternRangeEndpoint_Start ? selectionRange.getStart()
: selectionRange.getEnd());
using ATH = AccessibilityTextHelpers;
const auto direction = (count > 0 ? ATH::Direction::forwards
: ATH::Direction::backwards);
const auto boundaryType = getBoundaryType (unit);
auto movedEndpoint = endpointToMove;
int numMoved = 0;
for (; numMoved < std::abs (count); ++numMoved)
{
auto nextEndpoint = ATH::findTextBoundary (textInterface,
movedEndpoint,
boundaryType,
direction,
ATH::IncludeThisBoundary::no,
direction == ATH::Direction::forwards ? ATH::IncludeWhitespaceAfterWords::yes
: ATH::IncludeWhitespaceAfterWords::no);
if (nextEndpoint == movedEndpoint)
break;
movedEndpoint = nextEndpoint;
}
*pRetVal = numMoved;
setEndpointChecked (endpoint, movedEndpoint);
return S_OK;
});
}
JUCE_COMRESULT RemoveFromSelection() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* textInterface = owner->getHandler().getTextInterface())
{
textInterface->setSelection ({});
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT ScrollIntoView (BOOL) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT Select() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* textInterface = owner->getHandler().getTextInterface())
{
textInterface->setSelection ({});
textInterface->setSelection (selectionRange);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
private:
static AccessibilityTextHelpers::BoundaryType getBoundaryType (ComTypes::TextUnit unit)
{
switch (unit)
{
case ComTypes::TextUnit_Character:
return AccessibilityTextHelpers::BoundaryType::character;
case ComTypes::TextUnit_Format:
case ComTypes::TextUnit_Word:
return AccessibilityTextHelpers::BoundaryType::word;
case ComTypes::TextUnit_Line:
return AccessibilityTextHelpers::BoundaryType::line;
case ComTypes::TextUnit_Paragraph:
case ComTypes::TextUnit_Page:
case ComTypes::TextUnit_Document:
return AccessibilityTextHelpers::BoundaryType::document;
};
jassertfalse;
return AccessibilityTextHelpers::BoundaryType::character;
}
void setEndpointChecked (ComTypes::TextPatternRangeEndpoint endpoint, int newEndpoint)
{
if (endpoint == ComTypes::TextPatternRangeEndpoint_Start)
{
if (selectionRange.getEnd() < newEndpoint)
selectionRange.setEnd (newEndpoint);
selectionRange.setStart (newEndpoint);
}
else
{
if (selectionRange.getStart() > newEndpoint)
selectionRange.setStart (newEndpoint);
selectionRange.setEnd (newEndpoint);
}
}
ComSmartPtr<UIATextProvider> owner;
Range<int> selectionRange;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextRangeProvider)
};
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATextProvider)
};
} // namespace juce
@@ -0,0 +1,78 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAToggleProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IToggleProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT Toggle() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
if (handler.getActions().invoke (AccessibilityActionType::toggle)
|| handler.getActions().invoke (AccessibilityActionType::press))
{
VARIANT newValue;
VariantHelpers::setInt (getCurrentToggleState(), &newValue);
sendAccessibilityPropertyChangedEvent (handler, UIA_ToggleToggleStatePropertyId, newValue);
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT get_ToggleState (ComTypes::ToggleState* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = getCurrentToggleState();
return S_OK;
});
}
private:
ComTypes::ToggleState getCurrentToggleState() const
{
return getHandler().getCurrentState().isChecked() ? ComTypes::ToggleState_On
: ComTypes::ToggleState_Off;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAToggleProvider)
};
} // namespace juce
@@ -0,0 +1,122 @@
/*
==============================================================================
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
{
//==============================================================================
class UIATransformProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::ITransformProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT Move (double x, double y) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* peer = getPeer())
{
RECT rect;
GetWindowRect ((HWND) peer->getNativeHandle(), &rect);
rect.left = roundToInt (x);
rect.top = roundToInt (y);
auto bounds = Rectangle<int>::leftTopRightBottom (rect.left, rect.top, rect.right, rect.bottom);
peer->setBounds (Desktop::getInstance().getDisplays().physicalToLogical (bounds),
peer->isFullScreen());
}
return S_OK;
}
JUCE_COMRESULT Resize (double width, double height) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* peer = getPeer())
{
auto scale = peer->getPlatformScaleFactor();
peer->getComponent().setSize (roundToInt (width / scale),
roundToInt (height / scale));
}
return S_OK;
}
JUCE_COMRESULT Rotate (double) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT get_CanMove (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = true;
return S_OK;
});
}
JUCE_COMRESULT get_CanResize (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
if (auto* peer = getPeer())
*pRetVal = ((peer->getStyleFlags() & ComponentPeer::windowIsResizable) != 0);
return S_OK;
});
}
JUCE_COMRESULT get_CanRotate (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = false;
return S_OK;
});
}
private:
ComponentPeer* getPeer() const
{
return getHandler().getComponent().getPeer();
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIATransformProvider)
};
} // namespace juce
@@ -0,0 +1,83 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAValueProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IValueProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT SetValue (LPCWSTR val) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
const auto& handler = getHandler();
auto& valueInterface = *handler.getValueInterface();
if (valueInterface.isReadOnly())
return (HRESULT) UIA_E_NOTSUPPORTED;
valueInterface.setValueAsString (String (val));
VARIANT newValue;
VariantHelpers::setString (valueInterface.getCurrentValueAsString(), &newValue);
sendAccessibilityPropertyChangedEvent (handler, UIA_ValueValuePropertyId, newValue);
return S_OK;
}
JUCE_COMRESULT get_Value (BSTR* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
auto currentValueString = getHandler().getValueInterface()->getCurrentValueAsString();
*pRetVal = SysAllocString ((const OLECHAR*) currentValueString.toWideCharPointer());
return S_OK;
});
}
JUCE_COMRESULT get_IsReadOnly (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]
{
*pRetVal = getHandler().getValueInterface()->isReadOnly();
return S_OK;
});
}
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAValueProvider)
};
} // namespace juce
@@ -0,0 +1,194 @@
/*
==============================================================================
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
{
//==============================================================================
class UIAWindowProvider : public UIAProviderBase,
public ComBaseClassHelper<ComTypes::IWindowProvider>
{
public:
using UIAProviderBase::UIAProviderBase;
//==============================================================================
JUCE_COMRESULT SetVisualState (ComTypes::WindowVisualState state) override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* peer = getPeer())
{
switch (state)
{
case ComTypes::WindowVisualState_Maximized:
peer->setFullScreen (true);
break;
case ComTypes::WindowVisualState_Minimized:
peer->setMinimised (true);
break;
case ComTypes::WindowVisualState_Normal:
peer->setFullScreen (false);
peer->setMinimised (false);
break;
default:
break;
}
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT Close() override
{
if (! isElementValid())
return (HRESULT) UIA_E_ELEMENTNOTAVAILABLE;
if (auto* peer = getPeer())
{
peer->handleUserClosingWindow();
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT WaitForInputIdle (int, BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, []
{
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_CanMaximize (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
*pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMaximiseButton) != 0;
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_CanMinimize (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
*pRetVal = (peer->getStyleFlags() & ComponentPeer::windowHasMinimiseButton) != 0;
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_IsModal (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
*pRetVal = peer->getComponent().isCurrentlyModal();
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_WindowVisualState (ComTypes::WindowVisualState* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
if (peer->isFullScreen())
*pRetVal = ComTypes::WindowVisualState_Maximized;
else if (peer->isMinimised())
*pRetVal = ComTypes::WindowVisualState_Minimized;
else
*pRetVal = ComTypes::WindowVisualState_Normal;
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_WindowInteractionState (ComTypes::WindowInteractionState* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
*pRetVal = peer->getComponent().isCurrentlyBlockedByAnotherModalComponent()
? ComTypes::WindowInteractionState::WindowInteractionState_BlockedByModalWindow
: ComTypes::WindowInteractionState::WindowInteractionState_Running;
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
JUCE_COMRESULT get_IsTopmost (BOOL* pRetVal) override
{
return withCheckedComArgs (pRetVal, *this, [&]() -> HRESULT
{
if (auto* peer = getPeer())
{
*pRetVal = peer->isFocused();
return S_OK;
}
return (HRESULT) UIA_E_NOTSUPPORTED;
});
}
private:
ComponentPeer* getPeer() const
{
return getHandler().getComponent().getPeer();
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (UIAWindowProvider)
};
} // namespace juce
@@ -0,0 +1,160 @@
/*
==============================================================================
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
{
class WindowsUIAWrapper : public DeletedAtShutdown
{
public:
bool isLoaded() const noexcept
{
return uiaReturnRawElementProvider != nullptr
&& uiaHostProviderFromHwnd != nullptr
&& uiaRaiseAutomationPropertyChangedEvent != nullptr
&& uiaRaiseAutomationEvent != nullptr
&& uiaClientsAreListening != nullptr
&& uiaDisconnectProvider != nullptr
&& uiaDisconnectAllProviders != nullptr;
}
//==============================================================================
LRESULT returnRawElementProvider (HWND hwnd, WPARAM wParam, LPARAM lParam, IRawElementProviderSimple* provider)
{
return uiaReturnRawElementProvider != nullptr ? uiaReturnRawElementProvider (hwnd, wParam, lParam, provider)
: (LRESULT) nullptr;
}
JUCE_COMRESULT hostProviderFromHwnd (HWND hwnd, IRawElementProviderSimple** provider)
{
return uiaHostProviderFromHwnd != nullptr ? uiaHostProviderFromHwnd (hwnd, provider)
: (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT raiseAutomationPropertyChangedEvent (IRawElementProviderSimple* provider, PROPERTYID propID, VARIANT oldValue, VARIANT newValue)
{
return uiaRaiseAutomationPropertyChangedEvent != nullptr ? uiaRaiseAutomationPropertyChangedEvent (provider, propID, oldValue, newValue)
: (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT raiseAutomationEvent (IRawElementProviderSimple* provider, EVENTID eventID)
{
return uiaRaiseAutomationEvent != nullptr ? uiaRaiseAutomationEvent (provider, eventID)
: (HRESULT) UIA_E_NOTSUPPORTED;
}
BOOL clientsAreListening()
{
return uiaClientsAreListening != nullptr ? uiaClientsAreListening()
: false;
}
JUCE_COMRESULT disconnectProvider (IRawElementProviderSimple* provider)
{
if (uiaDisconnectProvider != nullptr)
{
const ScopedValueSetter<IRawElementProviderSimple*> disconnectingProviderSetter (disconnectingProvider, provider);
return uiaDisconnectProvider (provider);
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
JUCE_COMRESULT disconnectAllProviders()
{
if (uiaDisconnectAllProviders != nullptr)
{
const ScopedValueSetter<bool> disconnectingAllProvidersSetter (disconnectingAllProviders, true);
return uiaDisconnectAllProviders();
}
return (HRESULT) UIA_E_NOTSUPPORTED;
}
//==============================================================================
bool isProviderDisconnecting (IRawElementProviderSimple* provider)
{
return disconnectingProvider == provider || disconnectingAllProviders;
}
//==============================================================================
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (WindowsUIAWrapper)
private:
//==============================================================================
WindowsUIAWrapper()
{
// force UIA COM library initialisation here to prevent an exception when calling methods from SendMessage()
if (isLoaded())
returnRawElementProvider (nullptr, 0, 0, nullptr);
else
jassertfalse; // UIAutomationCore could not be loaded!
}
~WindowsUIAWrapper()
{
disconnectAllProviders();
if (uiaHandle != nullptr)
::FreeLibrary (uiaHandle);
clearSingletonInstance();
}
//==============================================================================
template <typename FuncType>
static FuncType getUiaFunction (HMODULE module, LPCSTR funcName)
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wcast-function-type")
return (FuncType) GetProcAddress (module, funcName);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
//==============================================================================
using UiaReturnRawElementProviderFunc = LRESULT (WINAPI*) (HWND, WPARAM, LPARAM, IRawElementProviderSimple*);
using UiaHostProviderFromHwndFunc = HRESULT (WINAPI*) (HWND, IRawElementProviderSimple**);
using UiaRaiseAutomationPropertyChangedEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, PROPERTYID, VARIANT, VARIANT);
using UiaRaiseAutomationEventFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*, EVENTID);
using UiaClientsAreListeningFunc = BOOL (WINAPI*) ();
using UiaDisconnectProviderFunc = HRESULT (WINAPI*) (IRawElementProviderSimple*);
using UiaDisconnectAllProvidersFunc = HRESULT (WINAPI*) ();
HMODULE uiaHandle = ::LoadLibraryA ("UIAutomationCore.dll");
UiaReturnRawElementProviderFunc uiaReturnRawElementProvider = getUiaFunction<UiaReturnRawElementProviderFunc> (uiaHandle, "UiaReturnRawElementProvider");
UiaHostProviderFromHwndFunc uiaHostProviderFromHwnd = getUiaFunction<UiaHostProviderFromHwndFunc> (uiaHandle, "UiaHostProviderFromHwnd");
UiaRaiseAutomationPropertyChangedEventFunc uiaRaiseAutomationPropertyChangedEvent = getUiaFunction<UiaRaiseAutomationPropertyChangedEventFunc> (uiaHandle, "UiaRaiseAutomationPropertyChangedEvent");
UiaRaiseAutomationEventFunc uiaRaiseAutomationEvent = getUiaFunction<UiaRaiseAutomationEventFunc> (uiaHandle, "UiaRaiseAutomationEvent");
UiaClientsAreListeningFunc uiaClientsAreListening = getUiaFunction<UiaClientsAreListeningFunc> (uiaHandle, "UiaClientsAreListening");
UiaDisconnectProviderFunc uiaDisconnectProvider = getUiaFunction<UiaDisconnectProviderFunc> (uiaHandle, "UiaDisconnectProvider");
UiaDisconnectAllProvidersFunc uiaDisconnectAllProviders = getUiaFunction<UiaDisconnectAllProvidersFunc> (uiaHandle, "UiaDisconnectAllProviders");
IRawElementProviderSimple* disconnectingProvider = nullptr;
bool disconnectingAllProviders = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsUIAWrapper)
};
} // namespace juce
@@ -0,0 +1,978 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Pair;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.view.Choreographer;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public final class ComponentPeerView extends ViewGroup
implements View.OnFocusChangeListener, Application.ActivityLifecycleCallbacks, Choreographer.FrameCallback
{
public ComponentPeerView (Context context, boolean opaque_, long host)
{
super (context);
if (Application.class.isInstance (context))
{
((Application) context).registerActivityLifecycleCallbacks (this);
}
else
{
((Application) context.getApplicationContext()).registerActivityLifecycleCallbacks (this);
}
this.host = host;
setWillNotDraw (false);
opaque = opaque_;
setFocusable (true);
setFocusableInTouchMode (true);
setOnFocusChangeListener (this);
// swap red and blue colours to match internal opengl texture format
ColorMatrix colorMatrix = new ColorMatrix();
float[] colorTransform = {0, 0, 1.0f, 0, 0,
0, 1.0f, 0, 0, 0,
1.0f, 0, 0, 0, 0,
0, 0, 0, 1.0f, 0};
colorMatrix.set (colorTransform);
paint.setColorFilter (new ColorMatrixColorFilter (colorMatrix));
java.lang.reflect.Method method = null;
try
{
method = getClass().getMethod ("setLayerType", int.class, Paint.class);
}
catch (SecurityException e)
{
}
catch (NoSuchMethodException e)
{
}
if (method != null)
{
try
{
int layerTypeNone = 0;
method.invoke (this, layerTypeNone, null);
}
catch (java.lang.IllegalArgumentException e)
{
}
catch (java.lang.IllegalAccessException e)
{
}
catch (java.lang.reflect.InvocationTargetException e)
{
}
}
Choreographer.getInstance().postFrameCallback (this);
}
public void clear()
{
host = 0;
}
//==============================================================================
private native void handlePaint (long host, Canvas canvas, Paint paint);
@Override
public void onDraw (Canvas canvas)
{
if (host == 0)
return;
handlePaint (host, canvas, paint);
}
private native void handleDoFrame (long host, long frameTimeNanos);
@Override
public void doFrame (long frameTimeNanos)
{
if (host == 0)
return;
handleDoFrame (host, frameTimeNanos);
Choreographer.getInstance().postFrameCallback (this);
}
@Override
public boolean isOpaque()
{
return opaque;
}
private final boolean opaque;
private long host;
private final Paint paint = new Paint();
//==============================================================================
private native void handleMouseDown (long host, int index, float x, float y, long time);
private native void handleMouseDrag (long host, int index, float x, float y, long time);
private native void handleMouseUp (long host, int index, float x, float y, long time);
private native void handleAccessibilityHover (long host, int action, float x, float y, long time);
@Override
public boolean onTouchEvent (MotionEvent event)
{
if (host == 0)
return false;
int action = event.getAction();
long time = event.getEventTime();
switch (action & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
handleMouseDown (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
handleMouseUp (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
return true;
case MotionEvent.ACTION_MOVE:
{
handleMouseDrag (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
int n = event.getPointerCount();
if (n > 1)
{
int point[] = new int[2];
getLocationOnScreen (point);
for (int i = 1; i < n; ++i)
handleMouseDrag (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
}
return true;
}
case MotionEvent.ACTION_POINTER_UP:
{
int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
if (i == 0)
{
handleMouseUp (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
}
else
{
int point[] = new int[2];
getLocationOnScreen (point);
handleMouseUp (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
}
return true;
}
case MotionEvent.ACTION_POINTER_DOWN:
{
int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
if (i == 0)
{
handleMouseDown (host, event.getPointerId (0), event.getRawX(), event.getRawY(), time);
}
else
{
int point[] = new int[2];
getLocationOnScreen (point);
handleMouseDown (host, event.getPointerId (i), event.getX (i) + point[0], event.getY (i) + point[1], time);
}
return true;
}
default:
break;
}
return false;
}
@Override
public boolean onHoverEvent (MotionEvent event)
{
if (accessibilityManager.isTouchExplorationEnabled())
{
handleAccessibilityHover (host, event.getActionMasked(), event.getRawX(), event.getRawY(), event.getEventTime());
return true;
}
return false;
}
//==============================================================================
public static class TextInputTarget
{
public TextInputTarget (long owner) { host = owner; }
public boolean isTextInputActive() { return ComponentPeerView.textInputTargetIsTextInputActive (host); }
public int getHighlightedRegionBegin() { return ComponentPeerView.textInputTargetGetHighlightedRegionBegin (host); }
public int getHighlightedRegionEnd() { return ComponentPeerView.textInputTargetGetHighlightedRegionEnd (host); }
public void setHighlightedRegion (int b, int e) { ComponentPeerView.textInputTargetSetHighlightedRegion (host, b, e); }
public String getTextInRange (int b, int e) { return ComponentPeerView.textInputTargetGetTextInRange (host, b, e); }
public void insertTextAtCaret (String text) { ComponentPeerView.textInputTargetInsertTextAtCaret (host, text); }
public int getCaretPosition() { return ComponentPeerView.textInputTargetGetCaretPosition (host); }
public int getTotalNumChars() { return ComponentPeerView.textInputTargetGetTotalNumChars (host); }
public int getCharIndexForPoint (Point point) { return ComponentPeerView.textInputTargetGetCharIndexForPoint (host, point); }
public int getKeyboardType() { return ComponentPeerView.textInputTargetGetKeyboardType (host); }
public void setTemporaryUnderlining (List<Pair<Integer, Integer>> list) { ComponentPeerView.textInputTargetSetTemporaryUnderlining (host, list); }
//==============================================================================
private final long host;
}
private native static boolean textInputTargetIsTextInputActive (long host);
private native static int textInputTargetGetHighlightedRegionBegin (long host);
private native static int textInputTargetGetHighlightedRegionEnd (long host);
private native static void textInputTargetSetHighlightedRegion (long host, int begin, int end);
private native static String textInputTargetGetTextInRange (long host, int begin, int end);
private native static void textInputTargetInsertTextAtCaret (long host, String text);
private native static int textInputTargetGetCaretPosition (long host);
private native static int textInputTargetGetTotalNumChars (long host);
private native static int textInputTargetGetCharIndexForPoint (long host, Point point);
private native static int textInputTargetGetKeyboardType (long host);
private native static void textInputTargetSetTemporaryUnderlining (long host, List<Pair<Integer, Integer>> list);
private native long getFocusedTextInputTargetPointer (long host);
private TextInputTarget getFocusedTextInputTarget (long host)
{
final long ptr = getFocusedTextInputTargetPointer (host);
return ptr != 0 ? new TextInputTarget (ptr) : null;
}
//==============================================================================
private native void handleKeyDown (long host, int keycode, int textchar, int kbFlags);
private native void handleKeyUp (long host, int keycode, int textchar);
private native void handleBackButton (long host);
private native void handleKeyboardHidden (long host);
private static int getInputTypeForJuceVirtualKeyboardType (int type)
{
switch (type)
{
case 0: // textKeyboard
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_VARIATION_NORMAL
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
case 1: // numericKeyboard
return InputType.TYPE_CLASS_NUMBER
| InputType.TYPE_NUMBER_VARIATION_NORMAL;
case 2: // decimalKeyboard
return InputType.TYPE_CLASS_NUMBER
| InputType.TYPE_NUMBER_VARIATION_NORMAL
| InputType.TYPE_NUMBER_FLAG_DECIMAL;
case 3: // urlKeyboard
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_VARIATION_URI
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
case 4: // emailAddressKeyboard
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
case 5: // phoneNumberKeyboard
return InputType.TYPE_CLASS_PHONE;
case 6: // passwordKeyboard
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_VARIATION_PASSWORD
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
}
return 0;
}
InputMethodManager getInputMethodManager()
{
return (InputMethodManager) getContext().getSystemService (Context.INPUT_METHOD_SERVICE);
}
public void closeInputMethodContext()
{
InputMethodManager imm = getInputMethodManager();
if (imm == null)
return;
if (cachedConnection != null)
cachedConnection.closeConnection();
imm.restartInput (this);
}
public void showKeyboard (int virtualKeyboardType, int selectionStart, int selectionEnd)
{
InputMethodManager imm = getInputMethodManager();
if (imm == null)
return;
// restartingInput causes a call back to onCreateInputConnection, where we'll pick
// up the correct keyboard characteristics to use for the focused TextInputTarget.
imm.restartInput (this);
imm.showSoftInput (this, 0);
keyboardDismissListener.startListening();
}
public void hideKeyboard()
{
InputMethodManager imm = getInputMethodManager();
if (imm == null)
return;
imm.hideSoftInputFromWindow (getWindowToken(), 0);
keyboardDismissListener.stopListening();
}
public void backButtonPressed()
{
if (host == 0)
return;
handleBackButton (host);
}
@Override
public boolean onKeyDown (int keyCode, KeyEvent event)
{
if (host == 0)
return false;
// The key event may move the cursor, or in some cases it might enter characters (e.g.
// digits). In this case, we need to reset the IME so that it's aware of the new contents
// of the TextInputTarget.
closeInputMethodContext();
switch (keyCode)
{
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
return super.onKeyDown (keyCode, event);
case KeyEvent.KEYCODE_BACK:
{
backButtonPressed();
return true;
}
default:
break;
}
handleKeyDown (host,
keyCode,
event.getUnicodeChar(),
event.getMetaState());
return true;
}
@Override
public boolean onKeyUp (int keyCode, KeyEvent event)
{
if (host == 0)
return false;
handleKeyUp (host, keyCode, event.getUnicodeChar());
return true;
}
@Override
public boolean onKeyMultiple (int keyCode, int count, KeyEvent event)
{
if (host == 0)
return false;
if (keyCode != KeyEvent.KEYCODE_UNKNOWN || (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && event.getAction() != KeyEvent.ACTION_MULTIPLE))
return super.onKeyMultiple (keyCode, count, event);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && event.getCharacters() != null)
{
int utf8Char = event.getCharacters().codePointAt (0);
handleKeyDown (host,
keyCode,
utf8Char,
event.getMetaState());
return true;
}
return false;
}
//==============================================================================
private final class KeyboardDismissListener
{
public KeyboardDismissListener (ComponentPeerView viewToUse)
{
view = viewToUse;
}
private void startListening()
{
view.getViewTreeObserver().addOnGlobalLayoutListener (viewTreeObserver);
}
private void stopListening()
{
view.getViewTreeObserver().removeOnGlobalLayoutListener (viewTreeObserver);
}
private class TreeObserver implements ViewTreeObserver.OnGlobalLayoutListener
{
TreeObserver()
{
keyboardShown = false;
}
@Override
public void onGlobalLayout()
{
Rect r = new Rect();
View parentView = getRootView();
int diff;
if (parentView == null)
{
getWindowVisibleDisplayFrame (r);
diff = getHeight() - (r.bottom - r.top);
}
else
{
parentView.getWindowVisibleDisplayFrame (r);
diff = parentView.getHeight() - (r.bottom - r.top);
}
// Arbitrary threshold, surely keyboard would take more than 20 pix.
if (diff < 20 && keyboardShown)
{
keyboardShown = false;
handleKeyboardHidden (view.host);
}
if (! keyboardShown && diff > 20)
keyboardShown = true;
}
private boolean keyboardShown;
}
private final ComponentPeerView view;
private final TreeObserver viewTreeObserver = new TreeObserver();
}
private final KeyboardDismissListener keyboardDismissListener = new KeyboardDismissListener (this);
//==============================================================================
// This implementation is quite similar to the ChangeListener in Android's built-in TextView.
private static final class ChangeWatcher implements SpanWatcher, TextWatcher
{
public ChangeWatcher (ComponentPeerView viewIn, Editable editableIn, TextInputTarget targetIn)
{
view = viewIn;
editable = editableIn;
target = targetIn;
updateEditableSelectionFromTarget (editable, target);
}
@Override
public void onSpanAdded (Spannable text, Object what, int start, int end)
{
updateTargetRangesFromEditable (editable, target);
}
@Override
public void onSpanRemoved (Spannable text, Object what, int start, int end)
{
updateTargetRangesFromEditable (editable, target);
}
@Override
public void onSpanChanged (Spannable text, Object what, int ostart, int oend, int nstart, int nend)
{
updateTargetRangesFromEditable (editable, target);
}
@Override
public void afterTextChanged (Editable s)
{
}
@Override
public void beforeTextChanged (CharSequence s, int start, int count, int after)
{
contentsBeforeChange = s.toString();
}
@Override
public void onTextChanged (CharSequence s, int start, int before, int count)
{
if (editable != s || contentsBeforeChange == null)
return;
final String newText = s.subSequence (start, start + count).toString();
int code = 0;
if (newText.endsWith ("\n") || newText.endsWith ("\r"))
code = KeyEvent.KEYCODE_ENTER;
if (newText.endsWith ("\t"))
code = KeyEvent.KEYCODE_TAB;
target.setHighlightedRegion (contentsBeforeChange.codePointCount (0, start),
contentsBeforeChange.codePointCount (0, start + before));
target.insertTextAtCaret (code != 0 ? newText.substring (0, newText.length() - 1)
: newText);
// Treating return/tab as individual keypresses rather than part of the composition
// sequence allows TextEditor onReturn and onTab to work as expected.
if (code != 0)
view.onKeyDown (code, new KeyEvent (KeyEvent.ACTION_DOWN, code));
updateTargetRangesFromEditable (editable, target);
contentsBeforeChange = null;
}
private static void updateEditableSelectionFromTarget (Editable editable, TextInputTarget text)
{
final int start = text.getHighlightedRegionBegin();
final int end = text.getHighlightedRegionEnd();
if (start < 0 || end < 0)
return;
final String string = editable.toString();
Selection.setSelection (editable,
string.offsetByCodePoints (0, start),
string.offsetByCodePoints (0, end));
}
private static void updateTargetSelectionFromEditable (Editable editable, TextInputTarget target)
{
final int start = Selection.getSelectionStart (editable);
final int end = Selection.getSelectionEnd (editable);
if (start < 0 || end < 0)
return;
final String string = editable.toString();
target.setHighlightedRegion (string.codePointCount (0, start),
string.codePointCount (0, end));
}
private static List<Pair<Integer, Integer>> getUnderlinedRanges (Editable editable)
{
final int start = BaseInputConnection.getComposingSpanStart (editable);
final int end = BaseInputConnection.getComposingSpanEnd (editable);
if (start < 0 || end < 0)
return null;
final String string = editable.toString();
final ArrayList<Pair<Integer, Integer>> pairs = new ArrayList<>();
pairs.add (new Pair<> (string.codePointCount (0, start), string.codePointCount (0, end)));
return pairs;
}
private static void updateTargetCompositionRangesFromEditable (Editable editable, TextInputTarget target)
{
target.setTemporaryUnderlining (getUnderlinedRanges (editable));
}
private static void updateTargetRangesFromEditable (Editable editable, TextInputTarget target)
{
updateTargetSelectionFromEditable (editable, target);
updateTargetCompositionRangesFromEditable (editable, target);
}
private final ComponentPeerView view;
private final TextInputTarget target;
private final Editable editable;
private String contentsBeforeChange;
}
private static final class Connection extends BaseInputConnection
{
Connection (ComponentPeerView viewIn, boolean fullEditor, TextInputTarget targetIn)
{
super (viewIn, fullEditor);
view = viewIn;
target = targetIn;
}
@Override
public Editable getEditable()
{
if (cached != null)
return cached;
if (target == null)
return cached = super.getEditable();
int length = target.getTotalNumChars();
String initialText = target.getTextInRange (0, length);
cached = new SpannableStringBuilder (initialText);
// Span the entire range of text, so that we pick up changes at any location.
// Use cached.length rather than target.getTotalNumChars here, because this
// range is in UTF-16 code units, rather than code points.
changeWatcher = new ChangeWatcher (view, cached, target);
cached.setSpan (changeWatcher, 0, cached.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
return cached;
}
/** Call this to stop listening for selection/composition updates.
We do this before closing the current input method context (e.g. when the user
taps on a text view to move the cursor), because otherwise the input system
might send another round of notifications *during* the restartInput call, after we've
requested that the input session should end.
*/
@Override
public void closeConnection()
{
if (cached != null && changeWatcher != null)
cached.removeSpan (changeWatcher);
cached = null;
target = null;
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
super.closeConnection();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
setImeConsumesInput (false);
}
else
{
finishComposingText();
}
}
private ComponentPeerView view;
private TextInputTarget target;
private Editable cached;
private ChangeWatcher changeWatcher;
}
@Override
public InputConnection onCreateInputConnection (EditorInfo outAttrs)
{
TextInputTarget focused = getFocusedTextInputTarget (host);
outAttrs.actionLabel = "";
outAttrs.hintText = "";
outAttrs.initialCapsMode = 0;
outAttrs.initialSelStart = focused != null ? focused.getHighlightedRegionBegin() : -1;
outAttrs.initialSelEnd = focused != null ? focused.getHighlightedRegionEnd() : -1;
outAttrs.label = "";
outAttrs.imeOptions = EditorInfo.IME_ACTION_UNSPECIFIED
| EditorInfo.IME_FLAG_NO_EXTRACT_UI
| EditorInfo.IME_FLAG_NO_ENTER_ACTION;
outAttrs.inputType = focused != null ? getInputTypeForJuceVirtualKeyboardType (focused.getKeyboardType())
: 0;
cachedConnection = new Connection (this, true, focused);
return cachedConnection;
}
private Connection cachedConnection;
//==============================================================================
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh)
{
super.onSizeChanged (w, h, oldw, oldh);
if (host != 0)
viewSizeChanged (host);
}
@Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom)
{
}
private native void viewSizeChanged (long host);
@Override
public void onFocusChange (View v, boolean hasFocus)
{
if (host == 0)
return;
if (v == this)
focusChanged (host, hasFocus);
}
private native void focusChanged (long host, boolean hasFocus);
public void setViewName (String newName)
{
}
public void setSystemUiVisibilityCompat (int visibility)
{
Method systemUIVisibilityMethod = null;
try
{
systemUIVisibilityMethod = this.getClass().getMethod ("setSystemUiVisibility", int.class);
}
catch (SecurityException e)
{
return;
}
catch (NoSuchMethodException e)
{
return;
}
if (systemUIVisibilityMethod == null) return;
try
{
systemUIVisibilityMethod.invoke (this, visibility);
}
catch (java.lang.IllegalArgumentException e)
{
}
catch (java.lang.IllegalAccessException e)
{
}
catch (java.lang.reflect.InvocationTargetException e)
{
}
}
public boolean isVisible()
{
return getVisibility() == VISIBLE;
}
public void setVisible (boolean b)
{
setVisibility (b ? VISIBLE : INVISIBLE);
}
public boolean containsPoint (int x, int y)
{
return true; //xxx needs to check overlapping views
}
//==============================================================================
private native void handleAppPaused (long host);
private native void handleAppResumed (long host);
@Override
public void onActivityPaused (Activity activity)
{
if (host == 0)
return;
handleAppPaused (host);
}
@Override
public void onActivityStopped (Activity activity)
{
}
@Override
public void onActivitySaveInstanceState (Activity activity, Bundle bundle)
{
}
@Override
public void onActivityDestroyed (Activity activity)
{
}
@Override
public void onActivityCreated (Activity activity, Bundle bundle)
{
}
@Override
public void onActivityStarted (Activity activity)
{
}
@Override
public void onActivityResumed (Activity activity)
{
if (host == 0)
return;
// Ensure that navigation/status bar visibility is correctly restored.
handleAppResumed (host);
}
//==============================================================================
private native View getNativeView (long host, int virtualViewId);
private native boolean populateAccessibilityNodeInfo (long host, int virtualViewId, AccessibilityNodeInfo info);
private native boolean handlePerformAction (long host, int virtualViewId, int action, Bundle arguments);
private native Integer getInputFocusViewId (long host);
private native Integer getAccessibilityFocusViewId (long host);
private final class JuceAccessibilityNodeProvider extends AccessibilityNodeProvider
{
public JuceAccessibilityNodeProvider (ComponentPeerView viewToUse)
{
view = viewToUse;
}
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo (int virtualViewId)
{
if (host == 0)
return null;
View nativeView = getNativeView (host, virtualViewId);
if (nativeView != null)
return nativeView.createAccessibilityNodeInfo();
final AccessibilityNodeInfo nodeInfo;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R)
{
nodeInfo = new AccessibilityNodeInfo (view, virtualViewId);
}
else
{
nodeInfo = AccessibilityNodeInfo.obtain (view, virtualViewId);
}
if (! populateAccessibilityNodeInfo (host, virtualViewId, nodeInfo))
{
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R)
nodeInfo.recycle();
return null;
}
return nodeInfo;
}
@Override
public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText (String text, int virtualViewId)
{
return new ArrayList<>();
}
@Override
public AccessibilityNodeInfo findFocus (int focus)
{
if (host == 0)
return null;
Integer focusViewId = (focus == AccessibilityNodeInfo.FOCUS_INPUT ? getInputFocusViewId (host)
: getAccessibilityFocusViewId (host));
if (focusViewId != null)
return createAccessibilityNodeInfo (focusViewId);
return null;
}
@Override
public boolean performAction (int virtualViewId, int action, Bundle arguments)
{
if (host == 0)
return false;
return handlePerformAction (host, virtualViewId, action, arguments);
}
private final ComponentPeerView view;
}
private final JuceAccessibilityNodeProvider nodeProvider = new JuceAccessibilityNodeProvider (this);
private final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService (Context.ACCESSIBILITY_SERVICE);
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider()
{
return nodeProvider;
}
}
@@ -0,0 +1,53 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.database.Cursor;
import android.database.MatrixCursor;
import java.lang.String;
public final class JuceContentProviderCursor extends MatrixCursor
{
public JuceContentProviderCursor (long hostToUse, String[] columnNames)
{
super (columnNames);
host = hostToUse;
}
@Override
public void close ()
{
super.close ();
contentSharerCursorClosed (host);
}
private native void contentSharerCursorClosed (long host);
private long host;
}
@@ -0,0 +1,48 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.os.FileObserver;
import java.lang.String;
public final class JuceContentProviderFileObserver extends FileObserver
{
public JuceContentProviderFileObserver (long hostToUse, String path, int mask)
{
super (path, mask);
host = hostToUse;
}
public void onEvent (int event, String path)
{
contentSharerFileObserverEvent (host, event, path);
}
private long host;
private native void contentSharerFileObserverEvent (long host, int event, String path);
}
@@ -0,0 +1,54 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.app.Activity;
import android.content.Intent;
//==============================================================================
public class JuceActivity extends Activity
{
//==============================================================================
private native void appNewIntent (Intent intent);
private native void appOnResume();
@Override
protected void onNewIntent (Intent intent)
{
super.onNewIntent(intent);
setIntent(intent);
appNewIntent (intent);
}
@Override
protected void onResume()
{
super.onResume();
appOnResume();
}
}
@@ -0,0 +1,119 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.FileObserver;
import android.os.ParcelFileDescriptor;
import java.lang.String;
public final class JuceSharingContentProvider extends ContentProvider
{
private Object lock = new Object ();
private native Cursor contentSharerQuery (Uri uri, String[] projection);
private native AssetFileDescriptor contentSharerOpenFile (Uri uri, String mode);
private native String[] contentSharerGetStreamTypes (Uri uri, String mimeTypeFilter);
@Override
public boolean onCreate ()
{
return true;
}
@Override
public Cursor query (Uri url, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
synchronized (lock)
{
return contentSharerQuery (url, projection);
}
}
@Override
public Uri insert (Uri uri, ContentValues values)
{
return null;
}
@Override
public int update (Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
return 0;
}
@Override
public int delete (Uri uri, String selection, String[] selectionArgs)
{
return 0;
}
@Override
public String getType (Uri uri)
{
return null;
}
@Override
public AssetFileDescriptor openAssetFile (Uri uri, String mode)
{
synchronized (lock)
{
return contentSharerOpenFile (uri, mode);
}
}
@Override
public ParcelFileDescriptor openFile (Uri uri, String mode)
{
synchronized (lock)
{
AssetFileDescriptor result = contentSharerOpenFile (uri, mode);
if (result != null)
return result.getParcelFileDescriptor ();
return null;
}
}
public String[] getStreamTypes (Uri uri, String mimeTypeFilter)
{
synchronized (lock)
{
return contentSharerGetStreamTypes (uri, mimeTypeFilter);
}
}
}
@@ -0,0 +1,42 @@
/*
==============================================================================
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.
==============================================================================
*/
package com.rmsl.juce;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
//==============================================================================
public class Receiver extends BroadcastReceiver
{
@Override
public void onReceive (Context context, Intent intent)
{
onBroadcastResultNative (intent.getIntExtra ("com.rmsl.juce.JUCE_REQUEST_CODE", 0));
}
private native void onBroadcastResultNative (int requestCode);
}
@@ -0,0 +1,375 @@
/*
==============================================================================
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.
==============================================================================
*/
// The CoreGraphicsMetalLayerRenderer requires macOS 10.14 and iOS 12.
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wunguarded-availability", "-Wunguarded-availability-new")
namespace juce
{
//==============================================================================
class CoreGraphicsMetalLayerRenderer
{
public:
//==============================================================================
static auto create()
{
ObjCObjectHandle<id<MTLDevice>> device { MTLCreateSystemDefaultDevice() };
return rawToUniquePtr (device != nullptr ? new CoreGraphicsMetalLayerRenderer (device)
: nullptr);
}
~CoreGraphicsMetalLayerRenderer()
{
if (memoryBlitCommandBuffer != nullptr)
{
stopGpuCommandSubmission = true;
[memoryBlitCommandBuffer.get() waitUntilCompleted];
}
}
/* Returns any regions that weren't redrawn, and which should be retried next frame. */
template <typename Callback>
[[nodiscard]] RectangleList<float> drawRectangleList (CAMetalLayer* layer,
float scaleFactor,
Callback&& drawRectWithContext,
RectangleList<float> dirtyRegions,
const bool renderSync)
{
layer.presentsWithTransaction = renderSync;
if (memoryBlitCommandBuffer != nullptr)
{
switch ([memoryBlitCommandBuffer.get() status])
{
case MTLCommandBufferStatusNotEnqueued:
case MTLCommandBufferStatusEnqueued:
case MTLCommandBufferStatusCommitted:
case MTLCommandBufferStatusScheduled:
// If we haven't finished blitting the CPU texture to the GPU then
// report that we have been unable to draw anything.
return dirtyRegions;
case MTLCommandBufferStatusCompleted:
case MTLCommandBufferStatusError:
break;
}
}
layer.contentsScale = scaleFactor;
const auto drawableSizeTransform = CGAffineTransformMakeScale (layer.contentsScale, layer.contentsScale);
const auto transformedFrameSize = CGSizeApplyAffineTransform (layer.bounds.size, drawableSizeTransform);
if (CGSizeEqualToSize (transformedFrameSize, CGSizeZero))
return dirtyRegions;
if (resources == nullptr || ! CGSizeEqualToSize (layer.drawableSize, transformedFrameSize))
{
layer.drawableSize = transformedFrameSize;
resources = std::make_unique<Resources> (device.get(), layer);
dirtyRegions.clear();
dirtyRegions.add (convertToRectFloat (layer.bounds));
}
auto gpuTexture = resources->getGpuTexture();
if (gpuTexture == nullptr)
{
jassertfalse;
return dirtyRegions;
}
auto cgContext = resources->getCGContext();
for (auto rect : dirtyRegions)
{
const auto cgRect = convertToCGRect (rect);
CGContextSaveGState (cgContext);
CGContextClipToRect (cgContext, cgRect);
drawRectWithContext (cgContext, cgRect);
CGContextRestoreGState (cgContext);
}
resources->signalBufferModifiedByCpu();
auto sharedTexture = resources->getSharedTexture();
auto encodeBlit = [] (id<MTLCommandBuffer> commandBuffer,
id<MTLTexture> source,
id<MTLTexture> destination)
{
auto blitCommandEncoder = [commandBuffer blitCommandEncoder];
[blitCommandEncoder copyFromTexture: source
sourceSlice: 0
sourceLevel: 0
sourceOrigin: MTLOrigin{}
sourceSize: MTLSize { source.width, source.height, 1 }
toTexture: destination
destinationSlice: 0
destinationLevel: 0
destinationOrigin: MTLOrigin{}];
[blitCommandEncoder endEncoding];
};
if (renderSync)
{
@autoreleasepool
{
id<MTLCommandBuffer> commandBuffer = [commandQueue.get() commandBuffer];
id<CAMetalDrawable> drawable = [layer nextDrawable];
encodeBlit (commandBuffer, sharedTexture, drawable.texture);
[commandBuffer commit];
[commandBuffer waitUntilScheduled];
[drawable present];
}
}
else
{
// Command buffers are usually considered temporary, and are automatically released by
// the operating system when the rendering pipeline is finished. However, we want to keep
// this one alive so that we can wait for pipeline completion in the destructor.
memoryBlitCommandBuffer.reset ([[commandQueue.get() commandBuffer] retain]);
encodeBlit (memoryBlitCommandBuffer.get(), sharedTexture, gpuTexture);
[memoryBlitCommandBuffer.get() addScheduledHandler: ^(id<MTLCommandBuffer>)
{
// We're on a Metal thread, so we can make a blocking nextDrawable call
// without stalling the message thread.
// Check if we can do an early exit.
if (stopGpuCommandSubmission)
return;
@autoreleasepool
{
id<CAMetalDrawable> drawable = [layer nextDrawable];
id<MTLCommandBuffer> presentationCommandBuffer = [commandQueue.get() commandBuffer];
encodeBlit (presentationCommandBuffer, gpuTexture, drawable.texture);
[presentationCommandBuffer addScheduledHandler: ^(id<MTLCommandBuffer>)
{
[drawable present];
}];
[presentationCommandBuffer commit];
}
}];
[memoryBlitCommandBuffer.get() commit];
}
dirtyRegions.clear();
return dirtyRegions;
}
private:
//==============================================================================
explicit CoreGraphicsMetalLayerRenderer (ObjCObjectHandle<id<MTLDevice>> mtlDevice)
: device (mtlDevice),
commandQueue ([device.get() newCommandQueue])
{
}
//==============================================================================
static auto alignTo (size_t n, size_t alignment)
{
return ((n + alignment - 1) / alignment) * alignment;
}
//==============================================================================
class GpuTexturePool
{
public:
GpuTexturePool (id<MTLDevice> metalDevice, MTLTextureDescriptor* descriptor)
{
for (auto& t : textureCache)
t.reset ((descriptor.width != 0 && descriptor.height != 0) ? [metalDevice newTextureWithDescriptor: descriptor]
: nullptr);
}
id<MTLTexture> take() const
{
auto iter = std::find_if (textureCache.begin(), textureCache.end(),
[] (const ObjCObjectHandle<id<MTLTexture>>& t) { return [t.get() retainCount] == 1; });
return iter == textureCache.end() ? nullptr : (*iter).get();
}
private:
std::array<ObjCObjectHandle<id<MTLTexture>>, 3> textureCache;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GpuTexturePool)
JUCE_DECLARE_NON_MOVEABLE (GpuTexturePool)
};
//==============================================================================
class Resources
{
public:
Resources (id<MTLDevice> metalDevice, CAMetalLayer* layer)
{
const auto bytesPerRow = alignTo ((size_t) layer.drawableSize.width * 4, 256);
const auto allocationSize = cpuRenderMemory.ensureSize (bytesPerRow * (size_t) layer.drawableSize.height);
buffer.reset ([metalDevice newBufferWithBytesNoCopy: cpuRenderMemory.get()
length: allocationSize
options:
#if JUCE_MAC
MTLResourceStorageModeManaged
#else
MTLResourceStorageModeShared
#endif
deallocator: nullptr]);
auto* textureDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: layer.pixelFormat
width: (NSUInteger) layer.drawableSize.width
height: (NSUInteger) layer.drawableSize.height
mipmapped: NO];
textureDesc.storageMode =
#if JUCE_MAC
MTLStorageModeManaged;
#else
MTLStorageModeShared;
#endif
textureDesc.usage = MTLTextureUsageShaderRead;
sharedTexture.reset ([buffer.get() newTextureWithDescriptor: textureDesc
offset: 0
bytesPerRow: bytesPerRow]);
cgContext.reset (CGBitmapContextCreate (cpuRenderMemory.get(),
(size_t) layer.drawableSize.width,
(size_t) layer.drawableSize.height,
8, // Bits per component
bytesPerRow,
CGColorSpaceCreateWithName (kCGColorSpaceSRGB),
(uint32_t) kCGImageAlphaPremultipliedFirst | (uint32_t) kCGBitmapByteOrder32Host));
CGContextTranslateCTM (cgContext.get(), 0, layer.drawableSize.height);
CGContextScaleCTM (cgContext.get(), layer.contentsScale, -layer.contentsScale);
textureDesc.storageMode = MTLStorageModePrivate;
gpuTexturePool = std::make_unique<GpuTexturePool> (metalDevice, textureDesc);
}
CGContextRef getCGContext() const noexcept { return cgContext.get(); }
id<MTLTexture> getSharedTexture() const noexcept { return sharedTexture.get(); }
id<MTLTexture> getGpuTexture() noexcept { return gpuTexturePool == nullptr ? nullptr : gpuTexturePool->take(); }
void signalBufferModifiedByCpu()
{
#if JUCE_MAC
[buffer.get() didModifyRange: { 0, buffer.get().length }];
#endif
}
private:
class AlignedMemory
{
public:
AlignedMemory() = default;
void* get()
{
return allocation != nullptr ? allocation->data : nullptr;
}
size_t ensureSize (size_t newSize)
{
const auto alignedSize = alignTo (newSize, pagesize);
if (alignedSize > size)
{
size = std::max (alignedSize, alignTo ((size_t) ((float) size * growthFactor), pagesize));
allocation = std::make_unique<AllocationWrapper> (pagesize, size);
}
return size;
}
private:
static constexpr float growthFactor = 1.3f;
const size_t pagesize = (size_t) getpagesize();
struct AllocationWrapper
{
AllocationWrapper (size_t alignment, size_t allocationSize)
{
if (posix_memalign (&data, alignment, allocationSize) != 0)
jassertfalse;
}
~AllocationWrapper()
{
::free (data);
}
void* data = nullptr;
};
std::unique_ptr<AllocationWrapper> allocation;
size_t size = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlignedMemory)
JUCE_DECLARE_NON_MOVEABLE (AlignedMemory)
};
AlignedMemory cpuRenderMemory;
detail::ContextPtr cgContext;
ObjCObjectHandle<id<MTLBuffer>> buffer;
ObjCObjectHandle<id<MTLTexture>> sharedTexture;
std::unique_ptr<GpuTexturePool> gpuTexturePool;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Resources)
JUCE_DECLARE_NON_MOVEABLE (Resources)
};
//==============================================================================
std::unique_ptr<Resources> resources;
ObjCObjectHandle<id<MTLDevice>> device;
ObjCObjectHandle<id<MTLCommandQueue>> commandQueue;
ObjCObjectHandle<id<MTLCommandBuffer>> memoryBlitCommandBuffer;
std::atomic<bool> stopGpuCommandSubmission { false };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CoreGraphicsMetalLayerRenderer)
JUCE_DECLARE_NON_MOVEABLE (CoreGraphicsMetalLayerRenderer)
};
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
} // namespace juce
@@ -0,0 +1,957 @@
/*
==============================================================================
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
{
//==============================================================================
// This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderCursor.java with min sdk version 16
// See juce_core/native/java/README.txt on how to generate this byte-code.
static const uint8 javaJuceContentProviderCursor[] =
{31,139,8,8,191,114,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,67,117,
114,115,111,114,46,100,101,120,0,117,147,177,111,211,64,20,198,223,157,157,148,150,54,164,192,208,14,64,144,16,18,67,235,138,2,
75,40,162,10,44,150,65,149,2,25,218,233,176,173,198,37,241,69,182,19,121,96,160,21,136,37,19,98,234,80,85,149,152,88,24,248,
3,24,146,63,130,141,137,129,13,169,99,7,190,203,157,33,18,194,210,207,247,222,229,189,239,157,206,95,130,48,159,91,91,191,75,227,
60,200,143,134,239,247,151,62,189,43,175,127,249,246,235,241,215,241,112,231,231,193,237,135,22,81,143,136,242,214,157,139,
100,158,99,78,84,37,189,95,2,159,129,13,70,128,129,83,179,127,102,242,27,120,157,129,71,224,16,156,128,143,96,12,126,128,69,232,
93,6,75,224,10,184,14,238,129,13,224,130,16,188,4,3,174,245,44,51,79,205,152,53,171,101,206,86,54,241,27,20,206,152,120,136,
248,156,137,63,32,134,12,45,76,206,166,187,148,230,28,169,125,62,201,249,159,156,209,188,201,23,77,93,241,187,122,134,38,40,225,
52,42,124,197,245,252,94,141,104,147,182,113,95,21,76,208,83,222,114,125,86,89,101,168,109,162,162,183,134,46,86,249,71,215,
158,228,54,149,239,71,113,148,61,32,230,210,85,183,239,135,13,25,103,97,156,109,37,114,16,5,97,210,232,39,169,76,86,247,196,64,
208,53,79,196,65,34,163,192,9,68,38,94,136,52,116,158,136,44,137,114,93,84,167,91,158,47,187,78,210,77,59,206,30,164,156,255,
234,213,137,181,136,183,92,178,90,174,135,192,163,75,59,158,154,225,116,68,188,235,52,33,26,239,214,169,228,119,100,26,210,121,
95,118,250,221,248,169,232,134,41,45,251,90,176,217,22,73,33,215,80,101,1,217,109,153,102,52,171,222,207,228,115,52,218,89,
59,74,233,38,191,48,63,83,217,88,161,85,194,178,141,139,224,184,28,190,255,218,30,113,126,192,201,98,223,249,130,185,27,54,181,
22,222,227,83,254,43,60,49,50,235,180,15,11,47,150,167,252,200,106,186,95,121,146,85,255,122,134,215,180,190,242,169,101,106,
212,119,165,154,238,157,124,243,170,142,213,255,224,55,143,234,50,200,64,3,0,0,0,0};
// This byte-code is generated from native/java/app/com/rmsl/juce/JuceContentProviderFileObserver.java with min sdk version 16
// See juce_core/native/java/README.txt on how to generate this byte-code.
static const uint8 javaJuceContentProviderFileObserver[] =
{31,139,8,8,194,122,161,94,0,3,106,97,118,97,74,117,99,101,67,111,110,116,101,110,116,80,114,111,118,105,100,101,114,70,105,
108,101,79,98,115,101,114,118,101,114,46,100,101,120,0,133,147,205,107,19,65,24,198,223,249,72,98,171,46,105,235,69,16,201,65,81,
68,221,136,10,66,84,144,250,65,194,130,197,212,32,5,15,155,100,104,182,38,187,97,119,141,241,32,126,30,196,147,23,79,246,216,
131,120,202,77,169,80,212,191,64,193,66,143,30,60,138,255,130,62,179,51,165,219,147,129,223,188,239,188,239,204,179,179,179,79,
186,106,60,93,61,123,158,54,159,255,248,112,97,210,120,124,98,237,251,177,7,109,245,115,253,225,198,159,47,243,171,135,198,130,
104,72,68,227,214,185,89,178,191,45,78,116,128,76,189,8,62,3,169,235,128,129,61,204,204,203,204,204,171,24,142,99,207,2,226,
4,124,4,159,192,6,248,5,254,130,42,250,87,193,13,224,129,91,224,14,184,11,30,129,23,224,21,120,3,222,130,53,240,158,27,125,110,
159,95,176,231,41,233,51,216,249,75,44,152,178,249,107,228,211,54,95,69,190,215,230,239,144,11,40,57,153,150,200,222,81,100,
170,166,190,47,139,68,51,185,200,237,93,8,27,191,218,66,17,138,186,54,225,230,44,195,42,209,149,194,18,238,206,201,58,250,121,
235,182,215,172,160,191,200,137,159,113,172,158,204,246,50,251,62,38,151,89,103,251,29,139,23,131,48,72,47,19,171,19,107,208,
145,198,253,142,154,143,194,84,133,233,66,28,141,130,174,138,175,7,125,117,179,157,168,120,164,226,211,43,254,200,167,131,158,
31,118,227,40,232,186,81,226,230,219,53,114,189,78,52,112,227,65,210,119,87,32,229,254,71,175,70,179,158,150,116,251,126,184,
236,54,211,56,8,151,107,196,90,36,90,117,143,100,171,97,70,175,142,2,134,195,29,35,213,236,249,241,110,161,107,35,148,169,160,
178,32,123,81,146,210,148,30,23,163,219,137,34,57,240,147,123,84,138,66,179,76,14,253,180,71,50,237,5,9,29,21,229,185,153,146,
115,233,20,157,228,206,92,201,89,194,21,113,70,156,61,125,34,191,113,246,12,223,143,253,198,101,237,183,223,133,229,226,182,103,
121,206,183,34,231,93,153,243,111,129,118,60,92,164,29,31,179,138,217,175,189,204,202,102,141,246,24,175,24,125,237,111,97,
215,104,15,80,197,236,205,252,81,54,185,254,255,252,3,243,31,208,130,120,3,0,0,0,0};
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
FIELD (authority, "authority", "Ljava/lang/String;")
DECLARE_JNI_CLASS (AndroidProviderInfo, "android/content/pm/ProviderInfo")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "(Landroid/os/ParcelFileDescriptor;JJ)V") \
METHOD (createInputStream, "createInputStream", "()Ljava/io/FileInputStream;") \
METHOD (getLength, "getLength", "()J")
DECLARE_JNI_CLASS (AssetFileDescriptor, "android/content/res/AssetFileDescriptor")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (close, "close", "()V")
DECLARE_JNI_CLASS (JavaCloseable, "java/io/Closeable")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
STATICMETHOD (open, "open", "(Ljava/io/File;I)Landroid/os/ParcelFileDescriptor;")
DECLARE_JNI_CLASS (ParcelFileDescriptor, "android/os/ParcelFileDescriptor")
#undef JNI_CLASS_MEMBERS
//==============================================================================
class AndroidContentSharerCursor
{
public:
AndroidContentSharerCursor (JNIEnv* env,
const LocalRef<jobject>& contentProvider,
const LocalRef<jobjectArray>& resultColumns,
std::function<void (AndroidContentSharerCursor&)> onCloseIn)
: onClose (std::move (onCloseIn)),
cursor (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderCursor,
JuceContentProviderCursor.constructor,
reinterpret_cast<jlong> (this),
resultColumns.get()))))
{
// the content provider must be created first
jassert (contentProvider.get() != nullptr);
}
jobject getNativeCursor() const { return cursor.get(); }
void addRow (LocalRef<jobjectArray>& values)
{
auto* env = getEnv();
env->CallVoidMethod (cursor.get(), JuceContentProviderCursor.addRow, values.get());
}
private:
static void cursorClosed (JNIEnv*, AndroidContentSharerCursor& t)
{
MessageManager::callAsync ([&t]
{
NullCheckedInvocation::invoke (t.onClose, t);
});
}
std::function<void (AndroidContentSharerCursor&)> onClose;
GlobalRef cursor;
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (addRow, "addRow", "([Ljava/lang/Object;)V") \
METHOD (constructor, "<init>", "(J[Ljava/lang/String;)V") \
CALLBACK (generatedCallback<&AndroidContentSharerCursor::cursorClosed>, "contentSharerCursorClosed", "(J)V") \
DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderCursor, "com/rmsl/juce/JuceContentProviderCursor", 16, javaJuceContentProviderCursor)
#undef JNI_CLASS_MEMBERS
};
//==============================================================================
class AndroidContentSharerFileObserver
{
public:
AndroidContentSharerFileObserver (JNIEnv* env,
const LocalRef<jobject>& contentProvider,
const File& filepathToUse,
std::function<void()> onCloseIn)
: onClose (std::move (onCloseIn)),
filepath (filepathToUse),
fileObserver (GlobalRef (LocalRef<jobject> (env->NewObject (JuceContentProviderFileObserver,
JuceContentProviderFileObserver.constructor,
reinterpret_cast<jlong> (this),
javaString (filepath.getFullPathName()).get(),
open | access | closeWrite | closeNoWrite))))
{
// the content provider must be created first
jassert (contentProvider.get() != nullptr);
env->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.startWatching);
}
void onFileEvent (int event, const LocalRef<jstring>&)
{
if (event == open)
{
++numOpenedHandles;
}
else if (event == access)
{
fileWasRead = true;
}
else if (event == closeNoWrite || event == closeWrite)
{
--numOpenedHandles;
// numOpenedHandles may get negative if we don't receive open handle event.
if (fileWasRead && numOpenedHandles <= 0)
{
MessageManager::callAsync ([fileObserver = fileObserver, onClose = onClose]
{
getEnv()->CallVoidMethod (fileObserver, JuceContentProviderFileObserver.stopWatching);
NullCheckedInvocation::invoke (onClose);
});
}
}
}
private:
static constexpr int open = 32;
static constexpr int access = 1;
static constexpr int closeWrite = 8;
static constexpr int closeNoWrite = 16;
std::function<void()> onClose;
bool fileWasRead = false;
int numOpenedHandles = 0;
File filepath;
GlobalRef fileObserver;
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "(JLjava/lang/String;I)V") \
METHOD (startWatching, "startWatching", "()V") \
METHOD (stopWatching, "stopWatching", "()V") \
CALLBACK (generatedCallback<&AndroidContentSharerFileObserver::onFileEventCallback>, "contentSharerFileObserverEvent", "(JILjava/lang/String;)V") \
DECLARE_JNI_CLASS_WITH_BYTECODE (JuceContentProviderFileObserver, "com/rmsl/juce/JuceContentProviderFileObserver", 16, javaJuceContentProviderFileObserver)
#undef JNI_CLASS_MEMBERS
static void onFileEventCallback (JNIEnv*, AndroidContentSharerFileObserver& t, jint event, jstring path)
{
t.onFileEvent (event, LocalRef<jstring> (path));
}
};
//==============================================================================
class ContentSharerGlobalImpl
{
public:
static ContentSharerGlobalImpl& getInstance()
{
static ContentSharerGlobalImpl result;
return result;
}
const String packageName = juceString (LocalRef<jstring> ((jstring) getEnv()->CallObjectMethod (getAppContext().get(),
AndroidContext.getPackageName)));
const String uriBase = "content://" + packageName + ".sharingcontentprovider/";
std::unique_ptr<ActivityLauncher> sharePreparedFiles (const std::map<String, File>& fileForUriIn,
const StringArray& mimeTypes,
std::function<void (bool)> callback)
{
// This function should be called from the main thread, but must not race with singleton
// access from other threads.
const ScopedLock lock { mutex };
if (! isContentSharingEnabled())
{
// You need to enable "Content Sharing" in Projucer's Android exporter.
jassertfalse;
NullCheckedInvocation::invoke (callback, false);
return {};
}
auto* env = getEnv();
fileForUri.insert (fileForUriIn.begin(), fileForUriIn.end());
LocalRef<jobject> fileUris (env->NewObject (JavaArrayList, JavaArrayList.constructor, fileForUriIn.size()));
for (const auto& pair : fileForUriIn)
{
env->CallBooleanMethod (fileUris,
JavaArrayList.add,
env->CallStaticObjectMethod (AndroidUri,
AndroidUri.parse,
javaString (pair.first).get()));
}
LocalRef<jobject> intent (env->NewObject (AndroidIntent, AndroidIntent.constructor));
env->CallObjectMethod (intent,
AndroidIntent.setAction,
javaString ("android.intent.action.SEND_MULTIPLE").get());
env->CallObjectMethod (intent,
AndroidIntent.setType,
javaString (getCommonMimeType (mimeTypes)).get());
const auto permissions = [&]
{
constexpr int grantReadUriPermission = 1;
constexpr int grantPrefixUriPermission = 128;
if (getAndroidSDKVersion() < 21)
return grantReadUriPermission;
return grantReadUriPermission | grantPrefixUriPermission;
};
env->CallObjectMethod (intent, AndroidIntent.setFlags, permissions);
env->CallObjectMethod (intent,
AndroidIntent.putParcelableArrayListExtra,
javaString ("android.intent.extra.STREAM").get(),
fileUris.get());
return doIntent (intent, callback);
}
std::unique_ptr<ActivityLauncher> shareText (const String& text,
std::function<void (bool)> callback)
{
// This function should be called from the main thread, but must not race with singleton
// access from other threads.
const ScopedLock lock { mutex };
if (! isContentSharingEnabled())
{
// You need to enable "Content Sharing" in Projucer's Android exporter.
jassertfalse;
NullCheckedInvocation::invoke (callback, false);
return {};
}
auto* env = getEnv();
LocalRef<jobject> intent (env->NewObject (AndroidIntent, AndroidIntent.constructor));
env->CallObjectMethod (intent,
AndroidIntent.setAction,
javaString ("android.intent.action.SEND").get());
env->CallObjectMethod (intent,
AndroidIntent.putExtra,
javaString ("android.intent.extra.TEXT").get(),
javaString (text).get());
env->CallObjectMethod (intent, AndroidIntent.setType, javaString ("text/plain").get());
return doIntent (intent, callback);
}
static void onBroadcastResultReceive (JNIEnv*, jobject, int requestCode)
{
getInstance().sharingFinished (requestCode, true);
}
static jobject JNICALL contentSharerQuery (JNIEnv*, jobject contentProvider, jobject uri, jobjectArray projection)
{
return getInstance().query (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
LocalRef<jobject> (static_cast<jobject> (uri)),
LocalRef<jobjectArray> (static_cast<jobjectArray> (projection)));
}
static jobject JNICALL contentSharerOpenFile (JNIEnv*, jobject contentProvider, jobject uri, jstring mode)
{
return getInstance().openFile (LocalRef<jobject> (static_cast<jobject> (contentProvider)),
LocalRef<jobject> (static_cast<jobject> (uri)),
LocalRef<jstring> (static_cast<jstring> (mode)));
}
static jobjectArray JNICALL contentSharerGetStreamTypes (JNIEnv*, jobject /*contentProvider*/, jobject uri, jstring mimeTypeFilter)
{
return getInstance().getStreamTypes (addLocalRefOwner (uri),
addLocalRefOwner (mimeTypeFilter));
}
private:
ContentSharerGlobalImpl() = default;
LocalRef<jobject> makeChooser (const LocalRef<jobject>& intent, int request) const
{
auto* env = getEnv();
const auto text = javaString ("Choose share target");
if (getAndroidSDKVersion() < 22)
return LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent,
AndroidIntent.createChooser,
intent.get(),
text.get()));
constexpr jint FLAG_UPDATE_CURRENT = 0x08000000;
constexpr jint FLAG_IMMUTABLE = 0x04000000;
const auto context = getAppContext();
auto* klass = env->FindClass ("com/rmsl/juce/Receiver");
const LocalRef<jobject> replyIntent (env->NewObject (AndroidIntent, AndroidIntent.constructorWithContextAndClass, context.get(), klass));
getEnv()->CallObjectMethod (replyIntent, AndroidIntent.putExtraInt, javaString ("com.rmsl.juce.JUCE_REQUEST_CODE").get(), request);
const auto flags = FLAG_UPDATE_CURRENT | (getAndroidSDKVersion() <= 23 ? 0 : FLAG_IMMUTABLE);
const LocalRef<jobject> pendingIntent (env->CallStaticObjectMethod (AndroidPendingIntent,
AndroidPendingIntent.getBroadcast,
context.get(),
request,
replyIntent.get(),
flags));
return LocalRef<jobject> (env->CallStaticObjectMethod (AndroidIntent22,
AndroidIntent22.createChooser,
intent.get(),
text.get(),
env->CallObjectMethod (pendingIntent,
AndroidPendingIntent.getIntentSender)));
}
//==============================================================================
jobject openFile (const LocalRef<jobject>& contentProvider,
const LocalRef<jobject>& uri,
[[maybe_unused]] const LocalRef<jstring>& mode)
{
// This function can be called from multiple threads.
const ScopedLock lock { mutex };
auto* env = getEnv();
auto uriElements = getContentUriElements (env, uri);
if (uriElements.file == File())
return nullptr;
return getAssetFileDescriptor (env, contentProvider, uriElements.file);
}
jobject query (const LocalRef<jobject>& contentProvider,
const LocalRef<jobject>& uri,
const LocalRef<jobjectArray>& projection)
{
// This function can be called from multiple threads.
const ScopedLock lock { mutex };
StringArray requestedColumns = javaStringArrayToJuce (projection);
StringArray supportedColumns = getSupportedColumns();
StringArray resultColumns;
for (const auto& col : supportedColumns)
{
if (requestedColumns.contains (col))
resultColumns.add (col);
}
// Unsupported columns were queried, file sharing may fail.
if (resultColumns.isEmpty())
return nullptr;
auto resultJavaColumns = juceStringArrayToJava (resultColumns);
auto* env = getEnv();
const auto uriElements = getContentUriElements (env, uri);
const auto callback = [info = uriElements.file] (auto& ref)
{
auto& pimplCursors = ContentSharerGlobalImpl::getInstance().cursors;
const auto iter = std::lower_bound (pimplCursors.begin(), pimplCursors.end(), &ref, [] (const auto& managed, const auto* ptr)
{
return managed.get() == ptr;
});
if (iter != pimplCursors.end() && iter->get() == &ref)
pimplCursors.erase (iter);
};
auto [iter, inserted] = cursors.emplace (new AndroidContentSharerCursor (env,
contentProvider,
resultJavaColumns,
callback));
if (uriElements.file == File())
return (*iter)->getNativeCursor();
LocalRef<jobjectArray> values (env->NewObjectArray ((jsize) resultColumns.size(), JavaObject, nullptr));
for (int i = 0; i < resultColumns.size(); ++i)
{
if (resultColumns.getReference (i) == "_display_name")
{
env->SetObjectArrayElement (values, i, javaString (uriElements.filename).get());
}
else if (resultColumns.getReference (i) == "_size")
{
LocalRef<jobject> javaFile (env->NewObject (JavaFile,
JavaFile.constructor,
javaString (uriElements.file.getFullPathName()).get()));
jlong fileLength = env->CallLongMethod (javaFile, JavaFile.length);
env->SetObjectArrayElement (values, i, env->NewObject (JavaLong, JavaLong.constructor, fileLength));
}
}
(*iter)->addRow (values);
return (*iter)->getNativeCursor();
}
jobjectArray getStreamTypes (const LocalRef<jobject>& uri, const LocalRef<jstring>& mimeTypeFilter)
{
// This function can be called from multiple threads.
const ScopedLock lock { mutex };
auto* env = getEnv();
auto extension = getContentUriElements (env, uri).filename.fromLastOccurrenceOf (".", false, true);
if (extension.isEmpty())
return nullptr;
return juceStringArrayToJava (filterMimeTypes (detail::MimeTypeTable::getMimeTypesForFileExtension (extension),
juceString (mimeTypeFilter.get()))).release();
}
std::unique_ptr<ActivityLauncher> doIntent (const LocalRef<jobject>& intent,
std::function<void (bool)> callback)
{
static std::atomic<int> lastRequest = 1003;
const auto requestCode = lastRequest++;
callbackForRequest.emplace (requestCode, callback);
const auto chooser = makeChooser (intent, requestCode);
auto launcher = std::make_unique<ActivityLauncher> (chooser, requestCode);
launcher->callback = [] (int request, int resultCode, LocalRef<jobject>)
{
ContentSharerGlobalImpl::getInstance().sharingFinished (request, resultCode == -1);
};
launcher->open();
return launcher;
}
void sharingFinished (int request, bool succeeded)
{
// This function should be called from the main thread, but must not race with singleton
// access from other threads.
const ScopedLock lock { mutex };
const auto iter = callbackForRequest.find (request);
if (iter == callbackForRequest.end())
return;
const ScopeGuard scope { [&] { callbackForRequest.erase (iter); } };
if (iter->second == nullptr)
return;
iter->second (succeeded);
}
bool isContentSharingEnabled() const
{
auto* env = getEnv();
LocalRef<jobject> packageManager (env->CallObjectMethod (getAppContext().get(), AndroidContext.getPackageManager));
constexpr int getProviders = 8;
LocalRef<jobject> packageInfo (env->CallObjectMethod (packageManager,
AndroidPackageManager.getPackageInfo,
javaString (packageName).get(),
getProviders));
LocalRef<jobjectArray> providers ((jobjectArray) env->GetObjectField (packageInfo,
AndroidPackageInfo.providers));
if (providers == nullptr)
return false;
auto sharingContentProviderAuthority = packageName + ".sharingcontentprovider";
const int numProviders = env->GetArrayLength (providers.get());
for (int i = 0; i < numProviders; ++i)
{
LocalRef<jobject> providerInfo (env->GetObjectArrayElement (providers, i));
LocalRef<jstring> authority ((jstring) env->GetObjectField (providerInfo, AndroidProviderInfo.authority));
if (juceString (authority) == sharingContentProviderAuthority)
return true;
}
return false;
}
//==============================================================================
struct ContentUriElements
{
String filename;
File file;
};
ContentUriElements getContentUriElements (JNIEnv* env, const LocalRef<jobject>& uri) const
{
const auto fullUri = juceString ((jstring) env->CallObjectMethod (uri.get(), AndroidUri.toString));
const auto filename = fullUri.fromLastOccurrenceOf ("/", false, true);
const auto iter = fileForUri.find (fullUri);
const auto info = iter != fileForUri.end() ? iter->second : File{};
return { filename, info };
}
static StringArray getSupportedColumns()
{
return StringArray ("_display_name", "_size");
}
jobject getAssetFileDescriptor (JNIEnv* env, const LocalRef<jobject>& contentProvider, const File& filepath)
{
if (nonAssetFilePathsPendingShare.find (filepath) == nonAssetFilePathsPendingShare.end())
{
const auto onCloseCallback = [filepath]
{
ContentSharerGlobalImpl::getInstance().nonAssetFilePathsPendingShare.erase (filepath);
};
auto observer = rawToUniquePtr (new AndroidContentSharerFileObserver (env,
contentProvider,
filepath,
onCloseCallback));
nonAssetFilePathsPendingShare.emplace (filepath, std::move (observer));
}
const LocalRef<jobject> javaFile (env->NewObject (JavaFile,
JavaFile.constructor,
javaString (filepath.getFullPathName()).get()));
constexpr int modeReadOnly = 268435456;
LocalRef<jobject> parcelFileDescriptor (env->CallStaticObjectMethod (ParcelFileDescriptor,
ParcelFileDescriptor.open,
javaFile.get(),
modeReadOnly));
if (jniCheckHasExceptionOccurredAndClear())
{
// Failed to create file descriptor. Have you provided a valid file path/resource name?
jassertfalse;
return nullptr;
}
jlong startOffset = 0;
jlong unknownLength = -1;
assetFileDescriptors.add (GlobalRef (LocalRef<jobject> (env->NewObject (AssetFileDescriptor,
AssetFileDescriptor.constructor,
parcelFileDescriptor.get(),
startOffset,
unknownLength))));
return assetFileDescriptors.getReference (assetFileDescriptors.size() - 1).get();
}
StringArray filterMimeTypes (const StringArray& mimeTypes, const String& filter)
{
String filterToUse (filter.removeCharacters ("*"));
if (filterToUse.isEmpty() || filterToUse == "/")
return mimeTypes;
StringArray result;
for (const auto& type : mimeTypes)
if (String (type).contains (filterToUse))
result.add (type);
return result;
}
static String getCommonMimeType (const StringArray& mimeTypes)
{
if (mimeTypes.isEmpty())
return "*/*";
auto commonMime = mimeTypes[0];
bool lookForCommonGroup = false;
for (int i = 1; i < mimeTypes.size(); ++i)
{
if (mimeTypes[i] == commonMime)
continue;
if (! lookForCommonGroup)
{
lookForCommonGroup = true;
commonMime = commonMime.upToFirstOccurrenceOf ("/", true, false);
}
if (! mimeTypes[i].startsWith (commonMime))
return "*/*";
}
return lookForCommonGroup ? commonMime + "*" : commonMime;
}
CriticalSection mutex;
Array<GlobalRef> assetFileDescriptors;
std::map<File, std::unique_ptr<AndroidContentSharerFileObserver>> nonAssetFilePathsPendingShare;
std::set<std::unique_ptr<AndroidContentSharerCursor>> cursors;
std::map<String, File> fileForUri;
std::map<int, std::function<void (bool)>> callbackForRequest;
};
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
CALLBACK (ContentSharerGlobalImpl::contentSharerQuery, "contentSharerQuery", "(Landroid/net/Uri;[Ljava/lang/String;)Landroid/database/Cursor;") \
CALLBACK (ContentSharerGlobalImpl::contentSharerOpenFile, "contentSharerOpenFile", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;") \
CALLBACK (ContentSharerGlobalImpl::contentSharerGetStreamTypes, "contentSharerGetStreamTypes", "(Landroid/net/Uri;Ljava/lang/String;)[Ljava/lang/String;") \
DECLARE_JNI_CLASS (JuceSharingContentProvider, "com/rmsl/juce/JuceSharingContentProvider")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
CALLBACK (ContentSharerGlobalImpl::onBroadcastResultReceive, "onBroadcastResultNative", "(I)V")
DECLARE_JNI_CLASS (AndroidReceiver, "com/rmsl/juce/Receiver")
#undef JNI_CLASS_MEMBERS
//==============================================================================
class AndroidContentSharerPrepareFilesTask final : private AsyncUpdater
{
public:
AndroidContentSharerPrepareFilesTask (const Array<URL>& fileUrls,
std::function<void (const std::map<String, File>&, const StringArray&)> onCompletionIn)
: onCompletion (std::move (onCompletionIn)),
task (std::async (std::launch::async, [this, fileUrls]
{
run (fileUrls);
triggerAsyncUpdate();
})) {}
~AndroidContentSharerPrepareFilesTask() override
{
task.wait();
cancelPendingUpdate();
}
private:
const String packageName = ContentSharerGlobalImpl::getInstance().packageName;
const String uriBase = ContentSharerGlobalImpl::getInstance().uriBase;
struct StreamCloser
{
explicit StreamCloser (const LocalRef<jobject>& streamToUse)
: stream (GlobalRef (streamToUse))
{
}
~StreamCloser()
{
if (stream.get() != nullptr)
getEnv()->CallVoidMethod (stream, JavaCloseable.close);
}
GlobalRef stream;
};
void handleAsyncUpdate() override
{
onCompletion (infoForUri, mimeTypes);
}
void run (const Array<URL>& fileUrls)
{
auto* env = getEnv();
StringArray filePaths;
for (const auto& f : fileUrls)
{
const auto scheme = f.getScheme();
// Only "file://" scheme or no scheme (for files in app bundle) are allowed!
jassert (scheme.isEmpty() || scheme == "file");
const auto fileToPrepare = [&]
{
if (! scheme.isEmpty())
return f;
// Raw resource names need to be all lower case
jassert (f.toString (true).toLowerCase() == f.toString (true));
// This will get us a file with file:// URI
return copyAssetFileToTemporaryFile (env, f.toString (true));
}();
if (fileToPrepare.isEmpty())
continue;
const auto filepath = URL::removeEscapeChars (fileToPrepare.toString (true).fromFirstOccurrenceOf ("file://", false, false));
filePaths.add (filepath);
}
std::vector<String> extensions;
for (const auto& filepath : filePaths)
{
const auto filename = filepath.fromLastOccurrenceOf ("/", false, true);
extensions.push_back (filename.fromLastOccurrenceOf (".", false, true));
}
std::set<String> mimes;
if (std::none_of (extensions.begin(), extensions.end(), [] (const String& s) { return s.isEmpty(); }))
for (const auto& extension : extensions)
for (const auto& mime : detail::MimeTypeTable::getMimeTypesForFileExtension (extension))
mimes.insert (mime);
for (const auto& mime : mimes)
mimeTypes.add (mime);
for (auto it = filePaths.begin(); it != filePaths.end(); ++it)
{
const auto filename = it->fromLastOccurrenceOf ("/", false, true);
const auto contentString = uriBase + String (std::distance (filePaths.begin(), it)) + "/" + filename;
infoForUri.emplace (contentString, *it);
}
}
URL copyAssetFileToTemporaryFile (JNIEnv* env, const String& filename)
{
LocalRef<jobject> resources (env->CallObjectMethod (getAppContext().get(), AndroidContext.getResources));
int fileId = env->CallIntMethod (resources,
AndroidResources.getIdentifier,
javaString (filename).get(),
javaString ("raw").get(),
javaString (packageName).get());
// Raw resource not found. Please make sure that you include your file as a raw resource
// and that you specify just the file name, without an extension.
jassert (fileId != 0);
if (fileId == 0)
return {};
LocalRef<jobject> assetFd (env->CallObjectMethod (resources,
AndroidResources.openRawResourceFd,
fileId));
StreamCloser inputStream (LocalRef<jobject> (env->CallObjectMethod (assetFd, AssetFileDescriptor.createInputStream)));
if (jniCheckHasExceptionOccurredAndClear())
{
// Failed to open file stream for resource
jassertfalse;
return {};
}
auto tempFile = File::createTempFile ({});
tempFile.createDirectory();
tempFile = tempFile.getChildFile (filename);
StreamCloser outputStream (LocalRef<jobject> (env->NewObject (JavaFileOutputStream,
JavaFileOutputStream.constructor,
javaString (tempFile.getFullPathName()).get())));
if (jniCheckHasExceptionOccurredAndClear())
{
// Failed to open file stream for temporary file
jassertfalse;
return {};
}
LocalRef<jbyteArray> buffer (env->NewByteArray (1024));
int bytesRead = 0;
for (;;)
{
bytesRead = env->CallIntMethod (inputStream.stream, JavaFileInputStream.read, buffer.get());
if (jniCheckHasExceptionOccurredAndClear())
{
// Failed to read from resource file.
jassertfalse;
return {};
}
if (bytesRead < 0)
break;
env->CallVoidMethod (outputStream.stream, JavaFileOutputStream.write, buffer.get(), 0, bytesRead);
if (jniCheckHasExceptionOccurredAndClear())
{
// Failed to write to temporary file.
jassertfalse;
return {};
}
}
return URL (tempFile);
}
std::map<String, File> infoForUri;
StringArray mimeTypes;
std::function<void (const std::map<String, File>&, const StringArray&)> onCompletion;
// This task is obtained from std::async(). Its destructor will block until the asynchronous
// task has completed; as a result, we can guarantee that the async task will have finished
// before the lifetimes of the other data members and base class end.
std::future<void> task;
};
auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>& urls, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
class NativeScopedContentSharerInterface final : public detail::ScopedContentSharerInterface
{
public:
explicit NativeScopedContentSharerInterface (Array<URL> f)
: files (std::move (f)) {}
void runAsync (ContentSharer::Callback callback) override
{
// This lambda will only be called if the AndroidContentSharerPrepareFilesTask is still
// alive. We know that our lifetime will end after the
// AndroidContentSharerPrepareFilesTask, so there's no need to check that 'this' is
// still valid inside the lambda.
task.emplace (files, [this, callback] (const std::map<String, File>& infoForUri, const StringArray& mimeTypes)
{
launcher = ContentSharerGlobalImpl::getInstance().sharePreparedFiles (infoForUri, mimeTypes, [callback] (bool success)
{
callback (success, {});
});
});
}
void close() override
{
// dismiss() doesn't close the sharesheet, and there doesn't seem to be any alternative
// Maybe this will work in the future...
launcher.reset();
}
private:
Array<URL> files;
std::optional<AndroidContentSharerPrepareFilesTask> task;
std::unique_ptr<ActivityLauncher> launcher;
};
return std::make_unique<NativeScopedContentSharerInterface> (std::move (urls));
}
auto detail::ScopedContentSharerInterface::shareText (const String& text, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
class NativeScopedContentSharerInterface final : public detail::ScopedContentSharerInterface
{
public:
explicit NativeScopedContentSharerInterface (String t)
: text (std::move (t)) {}
void runAsync (ContentSharer::Callback callback) override
{
launcher = ContentSharerGlobalImpl::getInstance().shareText (text, [callback] (bool success)
{
callback (success, {});
});
}
void close() override
{
// dismiss() doesn't close the sharesheet, and there doesn't seem to be any alternative
// Maybe this will work in the future...
launcher.reset();
}
private:
String text;
std::unique_ptr<ActivityLauncher> launcher;
};
return std::make_unique<NativeScopedContentSharerInterface> (std::move (text));
}
} // namespace juce
@@ -0,0 +1,125 @@
/*
==============================================================================
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
{
class NativeScopedContentSharerInterface final : public detail::ScopedContentSharerInterface,
public detail::NativeModalWrapperComponent
{
public:
NativeScopedContentSharerInterface (Component* parentIn, NSUniquePtr<NSArray> itemsIn)
: parent (parentIn), items (std::move (itemsIn)) {}
void runAsync (std::function<void (bool, const String&)> callback) override
{
if ([items.get() count] == 0)
{
jassertfalse;
NullCheckedInvocation::invoke (callback, false, "No valid items found for sharing.");
return;
}
controller.reset ([[UIActivityViewController alloc] initWithActivityItems: items.get()
applicationActivities: nil]);
controller.get().excludedActivityTypes = nil;
controller.get().completionWithItemsHandler = ^([[maybe_unused]] UIActivityType type, BOOL completed,
[[maybe_unused]] NSArray* returnedItems, NSError* error)
{
const auto errorDescription = error != nil ? nsStringToJuce ([error localizedDescription])
: String();
exitModalState (0);
NullCheckedInvocation::invoke (callback, completed && errorDescription.isEmpty(), errorDescription);
};
displayNativeWindowModally (parent);
enterModalState (true, nullptr, false);
}
void close() override
{
[controller.get() dismissViewControllerAnimated: YES completion: nil];
}
private:
UIViewController* getViewController() const override { return controller.get(); }
Component* parent = nullptr;
NSUniquePtr<UIActivityViewController> controller;
NSUniquePtr<NSArray> items;
};
auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>& files, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
{
NSUniquePtr<NSMutableArray> urls ([[NSMutableArray arrayWithCapacity: (NSUInteger) files.size()] retain]);
for (const auto& f : files)
{
NSString* nativeFilePath = nil;
if (f.isLocalFile())
{
nativeFilePath = juceStringToNS (f.getLocalFile().getFullPathName());
}
else
{
auto filePath = f.toString (false);
auto* fileDirectory = filePath.contains ("/")
? juceStringToNS (filePath.upToLastOccurrenceOf ("/", false, false))
: [NSString string];
auto fileName = juceStringToNS (filePath.fromLastOccurrenceOf ("/", false, false)
.upToLastOccurrenceOf (".", false, false));
auto fileExt = juceStringToNS (filePath.fromLastOccurrenceOf (".", false, false));
if ([fileDirectory length] == NSUInteger (0))
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt];
else
nativeFilePath = [[NSBundle mainBundle] pathForResource: fileName
ofType: fileExt
inDirectory: fileDirectory];
}
if (nativeFilePath != nil)
[urls.get() addObject: [NSURL fileURLWithPath: nativeFilePath]];
}
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (urls));
}
auto detail::ScopedContentSharerInterface::shareText (const String& text, Component* parent) -> std::unique_ptr<ScopedContentSharerInterface>
{
NSUniquePtr<NSArray> array ([[NSArray arrayWithObject: juceStringToNS (text)] retain]);
return std::make_unique<NativeScopedContentSharerInterface> (parent, std::move (array));
}
} // namespace juce
@@ -0,0 +1,609 @@
/*
==============================================================================
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
{
static Cursor createDraggingHandCursor();
ComponentPeer* getPeerFor (::Window);
//==============================================================================
class X11DragState
{
public:
X11DragState() = default;
//==============================================================================
bool isDragging() const noexcept
{
return dragging;
}
//==============================================================================
void handleExternalSelectionClear()
{
if (dragging)
externalResetDragAndDrop();
}
void handleExternalSelectionRequest (const XEvent& evt)
{
auto targetType = evt.xselectionrequest.target;
XEvent s;
s.xselection.type = SelectionNotify;
s.xselection.requestor = evt.xselectionrequest.requestor;
s.xselection.selection = evt.xselectionrequest.selection;
s.xselection.target = targetType;
s.xselection.property = None;
s.xselection.time = evt.xselectionrequest.time;
auto* display = getDisplay();
if (allowedTypes.contains (targetType))
{
s.xselection.property = evt.xselectionrequest.property;
X11Symbols::getInstance()->xChangeProperty (display, evt.xselectionrequest.requestor, evt.xselectionrequest.property,
targetType, 8, PropModeReplace,
reinterpret_cast<const unsigned char*> (textOrFiles.toRawUTF8()),
(int) textOrFiles.getNumBytesAsUTF8());
}
X11Symbols::getInstance()->xSendEvent (display, evt.xselectionrequest.requestor, True, 0, &s);
}
void handleExternalDragAndDropStatus (const XClientMessageEvent& clientMsg)
{
if (expectingStatus)
{
expectingStatus = false;
canDrop = false;
silentRect = {};
const auto& atoms = getAtoms();
if ((clientMsg.data.l[1] & 1) != 0
&& ((Atom) clientMsg.data.l[4] == atoms.XdndActionCopy
|| (Atom) clientMsg.data.l[4] == atoms.XdndActionPrivate))
{
if ((clientMsg.data.l[1] & 2) == 0) // target requests silent rectangle
silentRect.setBounds ((int) clientMsg.data.l[2] >> 16, (int) clientMsg.data.l[2] & 0xffff,
(int) clientMsg.data.l[3] >> 16, (int) clientMsg.data.l[3] & 0xffff);
canDrop = true;
}
}
}
void handleExternalDragButtonReleaseEvent()
{
if (dragging)
X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime);
if (canDrop)
{
sendExternalDragAndDropDrop();
}
else
{
sendExternalDragAndDropLeave();
externalResetDragAndDrop();
}
}
void handleExternalDragMotionNotify()
{
auto* display = getDisplay();
auto newTargetWindow = externalFindDragTargetWindow (X11Symbols::getInstance()
->xRootWindow (display,
X11Symbols::getInstance()->xDefaultScreen (display)));
if (targetWindow != newTargetWindow)
{
if (targetWindow != None)
sendExternalDragAndDropLeave();
canDrop = false;
silentRect = {};
if (newTargetWindow == None)
return;
xdndVersion = getDnDVersionForWindow (newTargetWindow);
if (xdndVersion == -1)
return;
targetWindow = newTargetWindow;
sendExternalDragAndDropEnter();
}
if (! expectingStatus)
sendExternalDragAndDropPosition();
}
void handleDragAndDropPosition (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
{
if (dragAndDropSourceWindow == 0)
return;
dragAndDropSourceWindow = (::Window) clientMsg.data.l[0];
if (windowH == 0)
windowH = (::Window) peer->getNativeHandle();
const auto displays = Desktop::getInstance().getDisplays();
const auto logicalPos = displays.physicalToLogical (Point<int> ((int) clientMsg.data.l[2] >> 16,
(int) clientMsg.data.l[2] & 0xffff));
const auto dropPos = detail::ScalingHelpers::screenPosToLocalPos (peer->getComponent(), logicalPos.toFloat()).roundToInt();
const auto& atoms = getAtoms();
auto targetAction = atoms.XdndActionCopy;
for (int i = numElementsInArray (atoms.allowedActions); --i >= 0;)
{
if ((Atom) clientMsg.data.l[4] == atoms.allowedActions[i])
{
targetAction = atoms.allowedActions[i];
break;
}
}
sendDragAndDropStatus (true, targetAction);
if (dragInfo.position != dropPos)
{
dragInfo.position = dropPos;
if (dragInfo.isEmpty())
updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle());
if (! dragInfo.isEmpty())
peer->handleDragMove (dragInfo);
}
}
void handleDragAndDropDrop (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
{
if (dragInfo.isEmpty())
{
// no data, transaction finished in handleDragAndDropSelection()
finishAfterDropDataReceived = true;
updateDraggedFileList (clientMsg, (::Window) peer->getNativeHandle());
}
else
{
handleDragAndDropDataReceived(); // data was already received
}
}
void handleDragAndDropEnter (const XClientMessageEvent& clientMsg, ComponentPeer* peer)
{
dragInfo.clear();
srcMimeTypeAtomList.clear();
dragAndDropCurrentMimeType = 0;
auto dndCurrentVersion = (static_cast<unsigned long> (clientMsg.data.l[1]) & 0xff000000) >> 24;
if (dndCurrentVersion < 3 || dndCurrentVersion > XWindowSystemUtilities::Atoms::DndVersion)
{
dragAndDropSourceWindow = 0;
return;
}
const auto& atoms = getAtoms();
dragAndDropSourceWindow = (::Window) clientMsg.data.l[0];
if ((clientMsg.data.l[1] & 1) != 0)
{
XWindowSystemUtilities::ScopedXLock xLock;
XWindowSystemUtilities::GetXProperty prop (getDisplay(),
dragAndDropSourceWindow,
atoms.XdndTypeList,
0,
0x8000000L,
false,
XA_ATOM);
if (prop.success && prop.actualType == XA_ATOM && prop.actualFormat == 32 && prop.numItems != 0)
{
auto* types = prop.data;
for (unsigned long i = 0; i < prop.numItems; ++i)
{
unsigned long type;
memcpy (&type, types, sizeof (unsigned long));
if (type != None)
srcMimeTypeAtomList.add (type);
types += sizeof (unsigned long);
}
}
}
if (srcMimeTypeAtomList.isEmpty())
{
for (int i = 2; i < 5; ++i)
if (clientMsg.data.l[i] != None)
srcMimeTypeAtomList.add ((unsigned long) clientMsg.data.l[i]);
if (srcMimeTypeAtomList.isEmpty())
{
dragAndDropSourceWindow = 0;
return;
}
}
for (int i = 0; i < srcMimeTypeAtomList.size() && dragAndDropCurrentMimeType == 0; ++i)
for (int j = 0; j < numElementsInArray (atoms.allowedMimeTypes); ++j)
if (srcMimeTypeAtomList[i] == atoms.allowedMimeTypes[j])
dragAndDropCurrentMimeType = atoms.allowedMimeTypes[j];
handleDragAndDropPosition (clientMsg, peer);
}
void handleDragAndDropExit()
{
if (auto* peer = getPeerFor (windowH))
peer->handleDragExit (dragInfo);
resetDragAndDrop();
}
void handleDragAndDropSelection (const XEvent& evt)
{
dragInfo.clear();
if (evt.xselection.property != None)
{
StringArray lines;
{
MemoryBlock dropData;
for (;;)
{
XWindowSystemUtilities::GetXProperty prop (getDisplay(),
evt.xany.window,
evt.xselection.property,
(long) (dropData.getSize() / 4),
65536,
false,
AnyPropertyType);
if (! prop.success)
break;
dropData.append (prop.data, (size_t) (prop.actualFormat / 8) * prop.numItems);
if (prop.bytesLeft <= 0)
break;
}
lines.addLines (dropData.toString());
}
if (XWindowSystemUtilities::Atoms::isMimeTypeFile (getDisplay(), dragAndDropCurrentMimeType))
{
for (const auto& line : lines)
{
const auto escaped = line.replace ("+", "%2B").replace ("file://", String(), true);
dragInfo.files.add (URL::removeEscapeChars (escaped));
}
dragInfo.files.trim();
dragInfo.files.removeEmptyStrings();
}
else
{
dragInfo.text = lines.joinIntoString ("\n");
}
if (finishAfterDropDataReceived)
handleDragAndDropDataReceived();
}
}
void externalResetDragAndDrop()
{
if (dragging)
{
XWindowSystemUtilities::ScopedXLock xLock;
X11Symbols::getInstance()->xUngrabPointer (getDisplay(), CurrentTime);
}
NullCheckedInvocation::invoke (completionCallback);
dragging = false;
}
bool externalDragInit (::Window window, bool text, const String& str, std::function<void()>&& cb)
{
windowH = window;
isText = text;
textOrFiles = str;
targetWindow = windowH;
completionCallback = std::move (cb);
auto* display = getDisplay();
allowedTypes.add (XWindowSystemUtilities::Atoms::getCreating (display, isText ? "text/plain" : "text/uri-list"));
auto pointerGrabMask = (unsigned int) (Button1MotionMask | ButtonReleaseMask);
XWindowSystemUtilities::ScopedXLock xLock;
if (X11Symbols::getInstance()->xGrabPointer (display, windowH, True, pointerGrabMask,
GrabModeAsync, GrabModeAsync, None, None, CurrentTime) == GrabSuccess)
{
const auto& atoms = getAtoms();
// No other method of changing the pointer seems to work, this call is needed from this very context
X11Symbols::getInstance()->xChangeActivePointerGrab (display, pointerGrabMask, (Cursor) createDraggingHandCursor(), CurrentTime);
X11Symbols::getInstance()->xSetSelectionOwner (display, atoms.XdndSelection, windowH, CurrentTime);
// save the available types to XdndTypeList
X11Symbols::getInstance()->xChangeProperty (display, windowH, atoms.XdndTypeList, XA_ATOM, 32, PropModeReplace,
reinterpret_cast<const unsigned char*> (allowedTypes.getRawDataPointer()), allowedTypes.size());
dragging = true;
xdndVersion = getDnDVersionForWindow (targetWindow);
sendExternalDragAndDropEnter();
handleExternalDragMotionNotify();
return true;
}
return false;
}
private:
//==============================================================================
const XWindowSystemUtilities::Atoms& getAtoms() const noexcept { return XWindowSystem::getInstance()->getAtoms(); }
::Display* getDisplay() const noexcept { return XWindowSystem::getInstance()->getDisplay(); }
//==============================================================================
void sendDragAndDropMessage (XClientMessageEvent& msg)
{
auto* display = getDisplay();
msg.type = ClientMessage;
msg.display = display;
msg.window = dragAndDropSourceWindow;
msg.format = 32;
msg.data.l[0] = (long) windowH;
XWindowSystemUtilities::ScopedXLock xLock;
X11Symbols::getInstance()->xSendEvent (display, dragAndDropSourceWindow, False, 0, (XEvent*) &msg);
}
bool sendExternalDragAndDropMessage (XClientMessageEvent& msg)
{
auto* display = getDisplay();
msg.type = ClientMessage;
msg.display = display;
msg.window = targetWindow;
msg.format = 32;
msg.data.l[0] = (long) windowH;
XWindowSystemUtilities::ScopedXLock xLock;
return X11Symbols::getInstance()->xSendEvent (display, targetWindow, False, 0, (XEvent*) &msg) != 0;
}
void sendExternalDragAndDropDrop()
{
XClientMessageEvent msg;
zerostruct (msg);
msg.message_type = getAtoms().XdndDrop;
msg.data.l[2] = CurrentTime;
sendExternalDragAndDropMessage (msg);
}
void sendExternalDragAndDropEnter()
{
XClientMessageEvent msg;
zerostruct (msg);
msg.message_type = getAtoms().XdndEnter;
msg.data.l[1] = (xdndVersion << 24);
for (int i = 0; i < 3; ++i)
msg.data.l[i + 2] = (long) allowedTypes[i];
sendExternalDragAndDropMessage (msg);
}
void sendExternalDragAndDropPosition()
{
XClientMessageEvent msg;
zerostruct (msg);
const auto& atoms = getAtoms();
msg.message_type = atoms.XdndPosition;
auto mousePos = Desktop::getInstance().getMousePosition();
if (silentRect.contains (mousePos)) // we've been asked to keep silent
return;
mousePos = Desktop::getInstance().getDisplays().logicalToPhysical (mousePos);
msg.data.l[1] = 0;
msg.data.l[2] = (mousePos.x << 16) | mousePos.y;
msg.data.l[3] = CurrentTime;
msg.data.l[4] = (long) atoms.XdndActionCopy; // this is all JUCE currently supports
expectingStatus = sendExternalDragAndDropMessage (msg);
}
void sendDragAndDropStatus (bool acceptDrop, Atom dropAction)
{
XClientMessageEvent msg;
zerostruct (msg);
msg.message_type = getAtoms().XdndStatus;
msg.data.l[1] = (acceptDrop ? 1 : 0) | 2; // 2 indicates that we want to receive position messages
msg.data.l[4] = (long) dropAction;
sendDragAndDropMessage (msg);
}
void sendExternalDragAndDropLeave()
{
XClientMessageEvent msg;
zerostruct (msg);
msg.message_type = getAtoms().XdndLeave;
sendExternalDragAndDropMessage (msg);
}
void sendDragAndDropFinish()
{
XClientMessageEvent msg;
zerostruct (msg);
msg.message_type = getAtoms().XdndFinished;
sendDragAndDropMessage (msg);
}
void updateDraggedFileList (const XClientMessageEvent& clientMsg, ::Window requestor)
{
jassert (dragInfo.isEmpty());
if (dragAndDropSourceWindow != None && dragAndDropCurrentMimeType != None)
{
auto* display = getDisplay();
XWindowSystemUtilities::ScopedXLock xLock;
X11Symbols::getInstance()->xConvertSelection (display, getAtoms().XdndSelection, dragAndDropCurrentMimeType,
XWindowSystemUtilities::Atoms::getCreating (display, "JXSelectionWindowProperty"),
requestor, (::Time) clientMsg.data.l[2]);
}
}
bool isWindowDnDAware (::Window w) const
{
int numProperties = 0;
auto* properties = X11Symbols::getInstance()->xListProperties (getDisplay(), w, &numProperties);
bool dndAwarePropFound = false;
for (int i = 0; i < numProperties; ++i)
if (properties[i] == getAtoms().XdndAware)
dndAwarePropFound = true;
if (properties != nullptr)
X11Symbols::getInstance()->xFree (properties);
return dndAwarePropFound;
}
int getDnDVersionForWindow (::Window target)
{
XWindowSystemUtilities::GetXProperty prop (getDisplay(),
target,
getAtoms().XdndAware,
0,
2,
false,
AnyPropertyType);
if (prop.success && prop.data != nullptr && prop.actualFormat == 32 && prop.numItems == 1)
return jmin ((int) prop.data[0], (int) XWindowSystemUtilities::Atoms::DndVersion);
return -1;
}
::Window externalFindDragTargetWindow (::Window target)
{
if (target == None)
return None;
if (isWindowDnDAware (target))
return target;
::Window child, phonyWin;
int phony;
unsigned int uphony;
X11Symbols::getInstance()->xQueryPointer (getDisplay(), target, &phonyWin, &child, &phony, &phony, &phony, &phony, &uphony);
return externalFindDragTargetWindow (child);
}
void handleDragAndDropDataReceived()
{
ComponentPeer::DragInfo dragInfoCopy (dragInfo);
sendDragAndDropFinish();
resetDragAndDrop();
if (! dragInfoCopy.isEmpty())
if (auto* peer = getPeerFor (windowH))
peer->handleDragDrop (dragInfoCopy);
}
void resetDragAndDrop()
{
dragInfo.clear();
dragInfo.position = Point<int> (-1, -1);
dragAndDropCurrentMimeType = 0;
dragAndDropSourceWindow = 0;
srcMimeTypeAtomList.clear();
finishAfterDropDataReceived = false;
}
//==============================================================================
::Window windowH = 0, targetWindow = 0, dragAndDropSourceWindow = 0;
int xdndVersion = -1;
bool isText = false, dragging = false, expectingStatus = false, canDrop = false, finishAfterDropDataReceived = false;
Atom dragAndDropCurrentMimeType;
Array<Atom> allowedTypes, srcMimeTypeAtomList;
ComponentPeer::DragInfo dragInfo;
Rectangle<int> silentRect;
String textOrFiles;
std::function<void()> completionCallback = nullptr;
//==============================================================================
JUCE_LEAK_DETECTOR (X11DragState)
};
} // namespace juce
@@ -0,0 +1,368 @@
/*
==============================================================================
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
{
namespace DragAndDropHelpers
{
//==============================================================================
struct JuceDropSource final : public ComBaseClassHelper<IDropSource>
{
JuceDropSource() = default;
JUCE_COMRESULT QueryContinueDrag (BOOL escapePressed, DWORD keys) override
{
if (escapePressed)
return DRAGDROP_S_CANCEL;
if ((keys & (MK_LBUTTON | MK_RBUTTON)) == 0)
return DRAGDROP_S_DROP;
return S_OK;
}
JUCE_COMRESULT GiveFeedback (DWORD) override
{
return DRAGDROP_S_USEDEFAULTCURSORS;
}
};
//==============================================================================
struct JuceEnumFormatEtc final : public ComBaseClassHelper<IEnumFORMATETC>
{
JuceEnumFormatEtc (const FORMATETC* f) : format (f) {}
JUCE_COMRESULT Clone (IEnumFORMATETC** result) override
{
if (result == nullptr)
return E_POINTER;
auto newOne = new JuceEnumFormatEtc (format);
newOne->index = index;
*result = newOne;
return S_OK;
}
JUCE_COMRESULT Next (ULONG celt, LPFORMATETC lpFormatEtc, ULONG* pceltFetched) override
{
if (pceltFetched != nullptr)
*pceltFetched = 0;
else if (celt != 1)
return S_FALSE;
if (index == 0 && celt > 0 && lpFormatEtc != nullptr)
{
copyFormatEtc (lpFormatEtc [0], *format);
++index;
if (pceltFetched != nullptr)
*pceltFetched = 1;
return S_OK;
}
return S_FALSE;
}
JUCE_COMRESULT Skip (ULONG celt) override
{
if (index + (int) celt >= 1)
return S_FALSE;
index += (int) celt;
return S_OK;
}
JUCE_COMRESULT Reset() override
{
index = 0;
return S_OK;
}
private:
const FORMATETC* const format;
int index = 0;
static void copyFormatEtc (FORMATETC& dest, const FORMATETC& source)
{
dest = source;
if (source.ptd != nullptr)
{
dest.ptd = (DVTARGETDEVICE*) CoTaskMemAlloc (sizeof (DVTARGETDEVICE));
if (dest.ptd != nullptr)
*(dest.ptd) = *(source.ptd);
}
}
JUCE_DECLARE_NON_COPYABLE (JuceEnumFormatEtc)
};
//==============================================================================
class JuceDataObject final : public ComBaseClassHelper<IDataObject>
{
public:
JuceDataObject (const FORMATETC* f, const STGMEDIUM* m)
: format (f), medium (m)
{
}
~JuceDataObject() override
{
jassert (refCount == 0);
}
JUCE_COMRESULT GetData (FORMATETC* pFormatEtc, STGMEDIUM* pMedium) override
{
if ((pFormatEtc->tymed & format->tymed) != 0
&& pFormatEtc->cfFormat == format->cfFormat
&& pFormatEtc->dwAspect == format->dwAspect)
{
pMedium->tymed = format->tymed;
pMedium->pUnkForRelease = nullptr;
if (format->tymed == TYMED_HGLOBAL)
{
auto len = GlobalSize (medium->hGlobal);
void* const src = GlobalLock (medium->hGlobal);
void* const dst = GlobalAlloc (GMEM_FIXED, len);
if (src != nullptr && dst != nullptr)
memcpy (dst, src, len);
GlobalUnlock (medium->hGlobal);
pMedium->hGlobal = dst;
return S_OK;
}
}
return DV_E_FORMATETC;
}
JUCE_COMRESULT QueryGetData (FORMATETC* f) override
{
if (f == nullptr)
return E_INVALIDARG;
if (f->tymed == format->tymed
&& f->cfFormat == format->cfFormat
&& f->dwAspect == format->dwAspect)
return S_OK;
return DV_E_FORMATETC;
}
JUCE_COMRESULT GetCanonicalFormatEtc (FORMATETC*, FORMATETC* pFormatEtcOut) override
{
pFormatEtcOut->ptd = nullptr;
return E_NOTIMPL;
}
JUCE_COMRESULT EnumFormatEtc (DWORD direction, IEnumFORMATETC** result) override
{
if (result == nullptr)
return E_POINTER;
if (direction == DATADIR_GET)
{
*result = new JuceEnumFormatEtc (format);
return S_OK;
}
*result = nullptr;
return E_NOTIMPL;
}
JUCE_COMRESULT GetDataHere (FORMATETC*, STGMEDIUM*) override { return DATA_E_FORMATETC; }
JUCE_COMRESULT SetData (FORMATETC*, STGMEDIUM*, BOOL) override { return E_NOTIMPL; }
JUCE_COMRESULT DAdvise (FORMATETC*, DWORD, IAdviseSink*, DWORD*) override { return OLE_E_ADVISENOTSUPPORTED; }
JUCE_COMRESULT DUnadvise (DWORD) override { return E_NOTIMPL; }
JUCE_COMRESULT EnumDAdvise (IEnumSTATDATA**) override { return OLE_E_ADVISENOTSUPPORTED; }
private:
const FORMATETC* const format;
const STGMEDIUM* const medium;
JUCE_DECLARE_NON_COPYABLE (JuceDataObject)
};
//==============================================================================
static HDROP createHDrop (const StringArray& fileNames)
{
size_t totalBytes = 0;
for (int i = fileNames.size(); --i >= 0;)
totalBytes += CharPointer_UTF16::getBytesRequiredFor (fileNames[i].getCharPointer()) + sizeof (WCHAR);
struct Deleter
{
void operator() (void* ptr) const noexcept { GlobalFree (ptr); }
};
auto hDrop = std::unique_ptr<void, Deleter> ((HDROP) GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, sizeof (DROPFILES) + totalBytes + 4));
if (hDrop != nullptr)
{
auto pDropFiles = (LPDROPFILES) GlobalLock (hDrop.get());
if (pDropFiles == nullptr)
return nullptr;
pDropFiles->pFiles = sizeof (DROPFILES);
pDropFiles->fWide = true;
auto* fname = reinterpret_cast<WCHAR*> (addBytesToPointer (pDropFiles, sizeof (DROPFILES)));
for (int i = 0; i < fileNames.size(); ++i)
{
auto bytesWritten = fileNames[i].copyToUTF16 (fname, 2048);
fname = reinterpret_cast<WCHAR*> (addBytesToPointer (fname, bytesWritten));
}
*fname = 0;
GlobalUnlock (hDrop.get());
}
return static_cast<HDROP> (hDrop.release());
}
struct DragAndDropJob final : public ThreadPoolJob
{
DragAndDropJob (FORMATETC f, STGMEDIUM m, DWORD d, std::function<void()>&& cb)
: ThreadPoolJob ("DragAndDrop"),
format (f), medium (m), whatToDo (d),
completionCallback (std::move (cb))
{
}
JobStatus runJob() override
{
[[maybe_unused]] const auto result = OleInitialize (nullptr);
auto* source = new JuceDropSource();
auto* data = new JuceDataObject (&format, &medium);
DWORD effect;
DoDragDrop (data, source, whatToDo, &effect);
data->Release();
source->Release();
OleUninitialize();
if (completionCallback != nullptr)
MessageManager::callAsync (std::move (completionCallback));
return jobHasFinished;
}
FORMATETC format;
STGMEDIUM medium;
DWORD whatToDo;
std::function<void()> completionCallback;
};
class ThreadPoolHolder final : private DeletedAtShutdown
{
public:
ThreadPoolHolder() = default;
~ThreadPoolHolder()
{
// Wait forever if there's a job running. The user needs to cancel the transfer
// in the GUI.
pool.removeAllJobs (true, -1);
clearSingletonInstance();
}
JUCE_DECLARE_SINGLETON_SINGLETHREADED (ThreadPoolHolder, false)
// We need to make sure we don't do simultaneous text and file drag and drops,
// so use a pool that can only run a single job.
ThreadPool pool { ThreadPoolOptions{}.withNumberOfThreads (1) };
};
JUCE_IMPLEMENT_SINGLETON (ThreadPoolHolder)
}
//==============================================================================
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, const bool canMove,
Component*, std::function<void()> callback)
{
if (files.isEmpty())
return false;
FORMATETC format = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM medium = { TYMED_HGLOBAL, { nullptr }, nullptr };
medium.hGlobal = DragAndDropHelpers::createHDrop (files);
auto& pool = DragAndDropHelpers::ThreadPoolHolder::getInstance()->pool;
pool.addJob (new DragAndDropHelpers::DragAndDropJob (format, medium,
canMove ? (DROPEFFECT_COPY | DROPEFFECT_MOVE) : DROPEFFECT_COPY,
std::move (callback)),
true);
return true;
}
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component*, std::function<void()> callback)
{
if (text.isEmpty())
return false;
FORMATETC format = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM medium = { TYMED_HGLOBAL, { nullptr }, nullptr };
auto numBytes = CharPointer_UTF16::getBytesRequiredFor (text.getCharPointer());
medium.hGlobal = GlobalAlloc (GMEM_MOVEABLE | GMEM_ZEROINIT, numBytes + 2);
if (medium.hGlobal == nullptr)
return false;
auto* data = static_cast<WCHAR*> (GlobalLock (medium.hGlobal));
text.copyToUTF16 (data, numBytes + 2);
format.cfFormat = CF_UNICODETEXT;
GlobalUnlock (medium.hGlobal);
auto& pool = DragAndDropHelpers::ThreadPoolHolder::getInstance()->pool;
pool.addJob (new DragAndDropHelpers::DragAndDropJob (format,
medium,
DROPEFFECT_COPY | DROPEFFECT_MOVE,
std::move (callback)),
true);
return true;
}
} // namespace juce
@@ -0,0 +1,286 @@
/*
==============================================================================
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
{
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getItemCount, "getItemCount", "()I") \
METHOD (getItemAt, "getItemAt", "(I)Landroid/content/ClipData$Item;")
DECLARE_JNI_CLASS (ClipData, "android/content/ClipData")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getUri, "getUri", "()Landroid/net/Uri;")
DECLARE_JNI_CLASS (ClipDataItem, "android/content/ClipData$Item")
#undef JNI_CLASS_MEMBERS
class FileChooser::Native final : public FileChooser::Pimpl
{
public:
//==============================================================================
Native (FileChooser& fileChooser, int flags) : owner (fileChooser)
{
if (currentFileChooser == nullptr)
{
currentFileChooser = this;
auto* env = getEnv();
auto sdkVersion = getAndroidSDKVersion();
auto saveMode = ((flags & FileBrowserComponent::saveMode) != 0);
auto selectsDirectories = ((flags & FileBrowserComponent::canSelectDirectories) != 0);
auto canSelectMultiple = ((flags & FileBrowserComponent::canSelectMultipleItems) != 0);
// You cannot save a directory
jassert (! (saveMode && selectsDirectories));
if (sdkVersion < 19)
{
// native save dialogs are only supported in Android versions >= 19
jassert (! saveMode);
saveMode = false;
}
if (sdkVersion < 21)
{
// native directory chooser dialogs are only supported in Android versions >= 21
jassert (! selectsDirectories);
selectsDirectories = false;
}
const char* action = (selectsDirectories ? "android.intent.action.OPEN_DOCUMENT_TREE"
: (saveMode ? "android.intent.action.CREATE_DOCUMENT"
: (sdkVersion >= 19 ? "android.intent.action.OPEN_DOCUMENT"
: "android.intent.action.GET_CONTENT")));
intent = GlobalRef (LocalRef<jobject> (env->NewObject (AndroidIntent, AndroidIntent.constructWithString,
javaString (action).get())));
if (owner.startingFile != File())
{
if (saveMode && (! owner.startingFile.isDirectory()))
env->CallObjectMethod (intent.get(), AndroidIntent.putExtraString,
javaString ("android.intent.extra.TITLE").get(),
javaString (owner.startingFile.getFileName()).get());
URL url (owner.startingFile);
LocalRef<jobject> uri (env->CallStaticObjectMethod (AndroidUri, AndroidUri.parse,
javaString (url.toString (true)).get()));
if (uri)
env->CallObjectMethod (intent.get(), AndroidIntent.putExtraParcelable,
javaString ("android.provider.extra.INITIAL_URI").get(),
uri.get());
}
if (canSelectMultiple && sdkVersion >= 18)
{
env->CallObjectMethod (intent.get(),
AndroidIntent.putExtraBool,
javaString ("android.intent.extra.ALLOW_MULTIPLE").get(),
true);
}
if (! selectsDirectories)
{
env->CallObjectMethod (intent.get(), AndroidIntent.addCategory,
javaString ("android.intent.category.OPENABLE").get());
auto mimeTypes = convertFiltersToMimeTypes (owner.filters);
if (mimeTypes.size() == 1)
{
env->CallObjectMethod (intent.get(), AndroidIntent.setType, javaString (mimeTypes[0]).get());
}
else
{
String mimeGroup = "*";
if (mimeTypes.size() > 0)
{
mimeGroup = mimeTypes[0].upToFirstOccurrenceOf ("/", false, false);
auto allMimeTypesHaveSameGroup = true;
LocalRef<jobjectArray> jMimeTypes (env->NewObjectArray (mimeTypes.size(), JavaString,
javaString ("").get()));
for (int i = 0; i < mimeTypes.size(); ++i)
{
env->SetObjectArrayElement (jMimeTypes.get(), i, javaString (mimeTypes[i]).get());
if (mimeGroup != mimeTypes[i].upToFirstOccurrenceOf ("/", false, false))
allMimeTypesHaveSameGroup = false;
}
env->CallObjectMethod (intent.get(), AndroidIntent.putExtraStrings,
javaString ("android.intent.extra.MIME_TYPES").get(),
jMimeTypes.get());
if (! allMimeTypesHaveSameGroup)
mimeGroup = "*";
}
env->CallObjectMethod (intent.get(), AndroidIntent.setType, javaString (mimeGroup + "/*").get());
}
}
}
else
jassertfalse; // there can only be a single file chooser
}
~Native() override
{
masterReference.clear();
currentFileChooser = nullptr;
}
void runModally() override
{
// Android does not support modal file choosers
jassertfalse;
}
void launch() override
{
auto* env = getEnv();
if (currentFileChooser != nullptr)
{
startAndroidActivityForResult (LocalRef<jobject> (env->NewLocalRef (intent.get())), /*READ_REQUEST_CODE*/ 42,
[myself = WeakReference<Native> { this }] (int requestCode, int resultCode, LocalRef<jobject> intentData) mutable
{
if (myself != nullptr)
myself->onActivityResult (requestCode, resultCode, intentData);
});
}
else
{
jassertfalse; // There is already a file chooser running
}
}
void onActivityResult (int /*requestCode*/, int resultCode, const LocalRef<jobject>& intentData)
{
currentFileChooser = nullptr;
auto* env = getEnv();
const auto getUrls = [&]() -> Array<URL>
{
if (resultCode != /*Activity.RESULT_OK*/ -1 || intentData == nullptr)
return {};
Array<URL> chosenURLs;
const auto addUrl = [env, &chosenURLs] (jobject uri)
{
if (auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString))
chosenURLs.add (URL (juceString (env, jStr)));
};
if (LocalRef<jobject> clipData { env->CallObjectMethod (intentData.get(), AndroidIntent.getClipData) })
{
const auto count = env->CallIntMethod (clipData.get(), ClipData.getItemCount);
for (auto i = 0; i < count; ++i)
{
if (LocalRef<jobject> item { env->CallObjectMethod (clipData.get(), ClipData.getItemAt, i) })
{
if (LocalRef<jobject> itemUri { env->CallObjectMethod (item.get(), ClipDataItem.getUri) })
addUrl (itemUri.get());
}
}
}
else if (LocalRef<jobject> uri { env->CallObjectMethod (intentData.get(), AndroidIntent.getData )})
{
addUrl (uri.get());
}
return chosenURLs;
};
owner.finished (getUrls());
}
static Native* currentFileChooser;
static StringArray convertFiltersToMimeTypes (const String& fileFilters)
{
StringArray result;
auto wildcards = StringArray::fromTokens (fileFilters, ";", "");
for (auto wildcard : wildcards)
{
if (wildcard.upToLastOccurrenceOf (".", false, false) == "*")
{
auto extension = wildcard.fromLastOccurrenceOf (".", false, false);
result.addArray (detail::MimeTypeTable::getMimeTypesForFileExtension (extension));
}
}
result.removeDuplicates (false);
return result;
}
private:
JUCE_DECLARE_WEAK_REFERENCEABLE (Native)
FileChooser& owner;
GlobalRef intent;
};
FileChooser::Native* FileChooser::Native::currentFileChooser = nullptr;
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
FilePreviewComponent*)
{
if (FileChooser::Native::currentFileChooser == nullptr)
return std::make_shared<FileChooser::Native> (owner, flags);
// there can only be one file chooser on Android at a once
jassertfalse;
return nullptr;
}
bool FileChooser::isPlatformDialogAvailable()
{
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
return false;
#else
return true;
#endif
}
void FileChooser::registerCustomMimeTypeForFileExtension (const String& mimeType,
const String& fileExtension)
{
detail::MimeTypeTable::registerCustomMimeTypeForFileExtension (mimeType, fileExtension);
}
} // namespace juce
@@ -0,0 +1,388 @@
/*
==============================================================================
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.
==============================================================================
*/
@interface FileChooserControllerClass : UIDocumentPickerViewController
- (void) setParent: (FileChooser::Native*) ptr;
@end
@interface FileChooserDelegateClass : NSObject<UIDocumentPickerDelegate, UIAdaptivePresentationControllerDelegate>
- (id) initWithOwner: (FileChooser::Native*) owner;
@end
namespace juce
{
#if ! (defined (__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0)
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
#define JUCE_DEPRECATION_IGNORED 1
#endif
//==============================================================================
class FileChooser::Native final : public FileChooser::Pimpl,
public detail::NativeModalWrapperComponent,
public std::enable_shared_from_this<Native>
{
public:
static std::shared_ptr<Native> make (FileChooser& fileChooser, int flags)
{
std::shared_ptr<Native> result { new Native (fileChooser, flags) };
/* Must be called after forming a shared_ptr to an instance of this class.
Note that we can't call this directly inside the class constructor, because
the owning shared_ptr might not yet exist.
*/
[result->controller.get() setParent: result.get()];
return result;
}
void launch() override
{
jassert (shared_from_this() != nullptr);
/* Normally, when deleteWhenDismissed is true, the modal component manager will keep a copy of a raw pointer
to our component and delete it when the modal state has ended. However, this is incompatible with
our class being tracked by shared_ptr as it will force delete our class regardless of the current
reference count. On the other hand, it's important that the modal manager keeps a reference as it can
sometimes be the only reference to our class.
To do this, we set deleteWhenDismissed to false so that the modal component manager does not delete
our class. Instead, we pass in a lambda which captures a shared_ptr to ourselves to increase the
reference count while the component is modal.
*/
enterModalState (true,
ModalCallbackFunction::create ([_self = shared_from_this()] (int) {}),
false);
}
void runModally() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
launch();
runModalLoop();
#else
jassertfalse;
#endif
}
//==============================================================================
void didPickDocumentsAtURLs (NSArray<NSURL*>* urls)
{
const auto isWriting = controller.get().documentPickerMode == UIDocumentPickerModeExportToService
|| controller.get().documentPickerMode == UIDocumentPickerModeMoveToService;
const auto accessOptions = isWriting ? 0 : NSFileCoordinatorReadingWithoutChanges;
auto* fileCoordinator = [[[NSFileCoordinator alloc] initWithFilePresenter: nil] autorelease];
auto* intents = [[[NSMutableArray alloc] init] autorelease];
for (NSURL* url in urls)
{
auto* fileAccessIntent = isWriting
? [NSFileAccessIntent writingIntentWithURL: url options: accessOptions]
: [NSFileAccessIntent readingIntentWithURL: url options: accessOptions];
[intents addObject: fileAccessIntent];
}
[fileCoordinator coordinateAccessWithIntents: intents queue: [NSOperationQueue mainQueue] byAccessor: ^(NSError* err)
{
if (err != nil)
{
[[maybe_unused]] auto desc = [err localizedDescription];
jassertfalse;
return;
}
Array<URL> result;
for (NSURL* url in urls)
{
[url startAccessingSecurityScopedResource];
NSError* error = nil;
auto* bookmark = [url bookmarkDataWithOptions: 0
includingResourceValuesForKeys: nil
relativeToURL: nil
error: &error];
[bookmark retain];
[url stopAccessingSecurityScopedResource];
URL juceUrl (nsStringToJuce ([url absoluteString]));
if (error == nil)
{
setURLBookmark (juceUrl, (void*) bookmark);
}
else
{
[[maybe_unused]] auto desc = [error localizedDescription];
jassertfalse;
}
result.add (std::move (juceUrl));
}
passResultsToInitiator (std::move (result));
}];
}
void didPickDocumentAtURL (NSURL* url)
{
didPickDocumentsAtURLs (@[url]);
}
void pickerWasCancelled()
{
passResultsToInitiator ({});
}
private:
UIViewController* getViewController() const override { return controller.get(); }
Native (FileChooser& fileChooser, int flags)
: owner (fileChooser)
{
delegate.reset ([[FileChooserDelegateClass alloc] initWithOwner: this]);
const auto validExtensions = getValidExtensionsForWildcards (owner.filters);
const auto utTypeArray = (flags & FileBrowserComponent::canSelectDirectories) != 0
? @[@"public.folder"]
: createNSArrayFromStringArray (getUTTypesForExtensions (validExtensions));
if ((flags & FileBrowserComponent::saveMode) != 0)
{
auto currentFileOrDirectory = owner.startingFile;
UIDocumentPickerMode pickerMode = currentFileOrDirectory.existsAsFile()
? UIDocumentPickerModeExportToService
: UIDocumentPickerModeMoveToService;
if (! currentFileOrDirectory.existsAsFile())
{
const auto extension = validExtensions.isEmpty() ? String()
: validExtensions.getReference (0);
const auto filename = getFilename (currentFileOrDirectory, extension);
const auto tmpDirectory = File::createTempFile ("JUCE-filepath");
if (tmpDirectory.createDirectory().wasOk())
{
currentFileOrDirectory = tmpDirectory.getChildFile (filename);
currentFileOrDirectory.replaceWithText ("");
}
else
{
// Temporary directory creation failed! You need to specify a
// path you have write access to. Saving will not work for
// current path.
jassertfalse;
}
}
auto url = [[NSURL alloc] initFileURLWithPath: juceStringToNS (currentFileOrDirectory.getFullPathName())];
controller.reset ([[FileChooserControllerClass alloc] initWithURL: url inMode: pickerMode]);
[url release];
}
else
{
controller.reset ([[FileChooserControllerClass alloc] initWithDocumentTypes: utTypeArray inMode: UIDocumentPickerModeOpen]);
if (@available (iOS 11.0, *))
[controller.get() setAllowsMultipleSelection: (flags & FileBrowserComponent::canSelectMultipleItems) != 0];
}
[controller.get() setDelegate: delegate.get()];
if (auto* pc = [controller.get() presentationController])
[pc setDelegate: delegate.get()];
displayNativeWindowModally (fileChooser.parent);
}
void passResultsToInitiator (Array<URL> urls)
{
exitModalState (0);
// If the caller attempts to show a platform-native dialog box inside the results callback (e.g. in the DialogsDemo)
// then the original peer must already have focus. Otherwise, there's a danger that either the invisible FileChooser
// components will display the popup, locking the application, or maybe no component will have focus, and the
// dialog won't show at all.
for (auto i = 0; i < ComponentPeer::getNumPeers(); ++i)
if (auto* p = ComponentPeer::getPeer (i))
if (p != getPeer())
if (auto* view = (UIView*) p->getNativeHandle())
if ([view becomeFirstResponder] && [view isFirstResponder])
break;
// Calling owner.finished will delete this Pimpl instance, so don't call any more member functions here!
owner.finished (std::move (urls));
}
//==============================================================================
static StringArray getValidExtensionsForWildcards (const String& filterWildcards)
{
const auto filters = StringArray::fromTokens (filterWildcards, ";", "");
if (filters.contains ("*") || filters.isEmpty())
return {};
StringArray result;
for (const auto& filter : filters)
{
if (filter.isEmpty())
continue;
// iOS only supports file extension wild cards
jassert (filter.upToLastOccurrenceOf (".", true, false) == "*.");
result.add (filter.fromLastOccurrenceOf (".", false, false));
}
return result;
}
static StringArray getUTTypesForExtensions (const StringArray& extensions)
{
if (extensions.isEmpty())
return { "public.data" };
StringArray result;
for (const auto& extension : extensions)
{
if (extension.isEmpty())
continue;
CFUniquePtr<CFStringRef> fileExtensionCF (extension.toCFString());
if (const auto tag = CFUniquePtr<CFStringRef> (UTTypeCreatePreferredIdentifierForTag (kUTTagClassFilenameExtension, fileExtensionCF.get(), nullptr)))
result.add (String::fromCFString (tag.get()));
}
return result;
}
static String getFilename (const File& path, const String& fallbackExtension)
{
auto filename = path.getFileNameWithoutExtension();
auto extension = path.getFileExtension().substring (1);
if (filename.isEmpty())
filename = "Untitled";
if (extension.isEmpty())
extension = fallbackExtension;
if (extension.isNotEmpty())
filename += "." + extension;
return filename;
}
//==============================================================================
FileChooser& owner;
NSUniquePtr<NSObject<UIDocumentPickerDelegate, UIAdaptivePresentationControllerDelegate>> delegate;
NSUniquePtr<FileChooserControllerClass> controller;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
};
//==============================================================================
bool FileChooser::isPlatformDialogAvailable()
{
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
return false;
#else
return true;
#endif
}
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
FilePreviewComponent*)
{
return Native::make (owner, flags);
}
#if JUCE_DEPRECATION_IGNORED
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
#endif
} // namespace juce
@implementation FileChooserControllerClass
{
std::weak_ptr<FileChooser::Native> ptr;
}
- (void) setParent: (FileChooser::Native*) parent
{
jassert (parent != nullptr);
jassert (parent->shared_from_this() != nullptr);
ptr = parent->weak_from_this();
}
@end
@implementation FileChooserDelegateClass
{
FileChooser::Native* owner;
}
- (id) initWithOwner: (FileChooser::Native*) o
{
self = [super init];
owner = o;
return self;
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-implementations")
- (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentAtURL: (NSURL*) url
{
if (owner != nullptr)
owner->didPickDocumentAtURL (url);
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
- (void) documentPicker: (UIDocumentPickerViewController*) controller didPickDocumentsAtURLs: (NSArray<NSURL*>*) urls
{
if (owner != nullptr)
owner->didPickDocumentsAtURLs (urls);
}
- (void) documentPickerWasCancelled: (UIDocumentPickerViewController*) controller
{
if (owner != nullptr)
owner->pickerWasCancelled();
}
- (void) presentationControllerDidDismiss: (UIPresentationController *) presentationController
{
if (owner != nullptr)
owner->pickerWasCancelled();
}
@end
@@ -0,0 +1,287 @@
/*
==============================================================================
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
{
static bool exeIsAvailable (String executable)
{
ChildProcess child;
if (child.start ("which " + executable))
{
child.waitForProcessToFinish (60 * 1000);
return (child.getExitCode() == 0);
}
return false;
}
static bool isSet (int flags, int toCheck)
{
return (flags & toCheck) != 0;
}
class FileChooser::Native final : public FileChooser::Pimpl,
private Timer
{
public:
Native (FileChooser& fileChooser, int flags)
: owner (fileChooser),
// kdialog/zenity only support opening either files or directories.
// Files should take precedence, if requested.
isDirectory (isSet (flags, FileBrowserComponent::canSelectDirectories) && ! isSet (flags, FileBrowserComponent::canSelectFiles)),
isSave (isSet (flags, FileBrowserComponent::saveMode)),
selectMultipleFiles (isSet (flags, FileBrowserComponent::canSelectMultipleItems)),
warnAboutOverwrite (isSet (flags, FileBrowserComponent::warnAboutOverwriting))
{
const File previousWorkingDirectory (File::getCurrentWorkingDirectory());
// use kdialog for KDE sessions or if zenity is missing
if (exeIsAvailable ("kdialog") && (isKdeFullSession() || ! exeIsAvailable ("zenity")))
addKDialogArgs();
else
addZenityArgs();
}
~Native() override
{
finish (true);
}
void runModally() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
child.start (args, ChildProcess::wantStdOut);
while (child.isRunning())
if (! MessageManager::getInstance()->runDispatchLoopUntil (20))
break;
finish (false);
#else
jassertfalse;
#endif
}
void launch() override
{
child.start (args, ChildProcess::wantStdOut);
startTimer (100);
}
private:
FileChooser& owner;
bool isDirectory, isSave, selectMultipleFiles, warnAboutOverwrite;
ChildProcess child;
StringArray args;
String separator;
void timerCallback() override
{
if (! child.isRunning())
{
stopTimer();
finish (false);
}
}
void finish (bool shouldKill)
{
String result;
Array<URL> selection;
if (shouldKill)
child.kill();
else
result = child.readAllProcessOutput().trim();
if (result.isNotEmpty())
{
StringArray tokens;
if (selectMultipleFiles)
tokens.addTokens (result, separator, "\"");
else
tokens.add (result);
for (auto& token : tokens)
selection.add (URL (File::getCurrentWorkingDirectory().getChildFile (token)));
}
if (! shouldKill)
{
child.waitForProcessToFinish (60 * 1000);
owner.finished (selection);
}
}
static uint64 getTopWindowID() noexcept
{
if (TopLevelWindow* top = TopLevelWindow::getActiveTopLevelWindow())
return (uint64) (pointer_sized_uint) top->getWindowHandle();
return 0;
}
static bool isKdeFullSession()
{
return SystemStats::getEnvironmentVariable ("KDE_FULL_SESSION", String())
.equalsIgnoreCase ("true");
}
void addKDialogArgs()
{
args.add ("kdialog");
if (owner.title.isNotEmpty())
args.add ("--title=" + owner.title);
if (uint64 topWindowID = getTopWindowID())
{
args.add ("--attach");
args.add (String (topWindowID));
}
if (selectMultipleFiles)
{
separator = "\n";
args.add ("--multiple");
args.add ("--separate-output");
args.add ("--getopenfilename");
}
else
{
if (isSave) args.add ("--getsavefilename");
else if (isDirectory) args.add ("--getexistingdirectory");
else args.add ("--getopenfilename");
}
File startPath;
if (owner.startingFile.exists())
{
startPath = owner.startingFile;
}
else if (owner.startingFile.getParentDirectory().exists())
{
startPath = owner.startingFile.getParentDirectory();
}
else
{
startPath = File::getSpecialLocation (File::userHomeDirectory);
if (isSave)
startPath = startPath.getChildFile (owner.startingFile.getFileName());
}
args.add (startPath.getFullPathName());
args.add ("(" + owner.filters.replaceCharacter (';', ' ') + ")");
}
void addZenityArgs()
{
args.add ("zenity");
args.add ("--file-selection");
const auto getUnderstandsConfirmOverwrite = []
{
// --confirm-overwrite is deprecated in zenity 3.91 and higher
ChildProcess process;
process.start ("zenity --version");
process.waitForProcessToFinish (1000);
const auto versionString = process.readAllProcessOutput();
const auto version = StringArray::fromTokens (versionString.trim(), ".", "");
return version.size() >= 2
&& (version[0].getIntValue() < 3
|| (version[0].getIntValue() == 3 && version[1].getIntValue() < 91));
};
if (warnAboutOverwrite && getUnderstandsConfirmOverwrite())
args.add ("--confirm-overwrite");
if (owner.title.isNotEmpty())
args.add ("--title=" + owner.title);
if (selectMultipleFiles)
{
separator = ":";
args.add ("--multiple");
args.add ("--separator=" + separator);
}
else
{
if (isSave)
args.add ("--save");
}
if (isDirectory)
args.add ("--directory");
if (owner.filters.isNotEmpty() && owner.filters != "*" && owner.filters != "*.*")
{
StringArray tokens;
tokens.addTokens (owner.filters, ";,|", "\"");
args.add ("--file-filter=" + tokens.joinIntoString (" "));
}
if (owner.startingFile.isDirectory())
owner.startingFile.setAsCurrentWorkingDirectory();
else if (owner.startingFile.getParentDirectory().exists())
owner.startingFile.getParentDirectory().setAsCurrentWorkingDirectory();
else
File::getSpecialLocation (File::userHomeDirectory).setAsCurrentWorkingDirectory();
auto filename = owner.startingFile.getFileName();
if (! filename.isEmpty())
args.add ("--filename=" + filename);
// supplying the window ID of the topmost window makes sure that Zenity pops up..
if (uint64 topWindowID = getTopWindowID())
setenv ("WINDOWID", String (topWindowID).toRawUTF8(), true);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
};
bool FileChooser::isPlatformDialogAvailable()
{
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
return false;
#else
static bool canUseNativeBox = exeIsAvailable ("zenity") || exeIsAvailable ("kdialog");
return canUseNativeBox;
#endif
}
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags, FilePreviewComponent*)
{
return std::make_shared<Native> (owner, flags);
}
} // namespace juce
@@ -0,0 +1,418 @@
/*
==============================================================================
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
{
//==============================================================================
static NSMutableArray* createAllowedTypesArray (const StringArray& filters)
{
if (filters.size() == 0)
return nil;
NSMutableArray* filterArray = [[[NSMutableArray alloc] init] autorelease];
for (int i = 0; i < filters.size(); ++i)
{
// From OS X 10.6 you can only specify allowed extensions, so any filters containing wildcards
// must be of the form "*.extension"
jassert (filters[i] == "*"
|| (filters[i].startsWith ("*.") && filters[i].lastIndexOfChar ('*') == 0));
const String f (filters[i].replace ("*.", ""));
if (f == "*")
return nil;
[filterArray addObject: juceStringToNS (f)];
}
return filterArray;
}
//==============================================================================
class FileChooser::Native final : public Component,
public FileChooser::Pimpl
{
public:
Native (FileChooser& fileChooser, int flags, FilePreviewComponent* previewComponent)
: owner (fileChooser), preview (previewComponent),
selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0),
selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0),
isSave ((flags & FileBrowserComponent::saveMode) != 0),
selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0)
{
setBounds (0, 0, 0, 0);
setOpaque (true);
panel = [&]
{
if (SystemStats::isAppSandboxEnabled())
return isSave ? [[NSSavePanel alloc] init]
: [[NSOpenPanel alloc] init];
static SafeSavePanel safeSavePanel;
static SafeOpenPanel safeOpenPanel;
return isSave ? [safeSavePanel.createInstance() init]
: [safeOpenPanel.createInstance() init];
}();
static DelegateClass delegateClass;
delegate = [delegateClass.createInstance() init];
object_setInstanceVariable (delegate, "cppObject", this);
[panel setDelegate: delegate];
filters.addTokens (owner.filters.replaceCharacters (",:", ";;"), ";", String());
filters.trim();
filters.removeEmptyStrings();
auto* nsTitle = juceStringToNS (owner.title);
[panel setTitle: nsTitle];
[panel setReleasedWhenClosed: YES];
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
[panel setAllowedFileTypes: createAllowedTypesArray (filters)];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
if (! isSave)
{
auto* openPanel = static_cast<NSOpenPanel*> (panel);
[openPanel setCanChooseDirectories: selectsDirectories];
[openPanel setCanChooseFiles: selectsFiles];
[openPanel setAllowsMultipleSelection: selectMultiple];
[openPanel setResolvesAliases: YES];
[openPanel setMessage: nsTitle]; // equivalent to the title bar since 10.11
if (owner.treatFilePackagesAsDirs)
[openPanel setTreatsFilePackagesAsDirectories: YES];
}
if (preview != nullptr)
{
nsViewPreview = [[NSView alloc] initWithFrame: makeNSRect (preview->getLocalBounds())];
[panel setAccessoryView: nsViewPreview];
preview->addToDesktop (0, (void*) nsViewPreview);
preview->setVisible (true);
if (@available (macOS 10.11, *))
{
if (! isSave)
{
auto* openPanel = static_cast<NSOpenPanel*> (panel);
[openPanel setAccessoryViewDisclosed: YES];
}
}
}
if (isSave || selectsDirectories)
[panel setCanCreateDirectories: YES];
[panel setLevel: NSModalPanelWindowLevel];
if (owner.startingFile.isDirectory())
{
startingDirectory = owner.startingFile.getFullPathName();
}
else
{
startingDirectory = owner.startingFile.getParentDirectory().getFullPathName();
filename = owner.startingFile.getFileName();
}
[panel setDirectoryURL: createNSURLFromFile (startingDirectory)];
[panel setNameFieldStringValue: juceStringToNS (filename)];
}
~Native() override
{
exitModalState (0);
if (preview != nullptr)
preview->removeFromDesktop();
removeFromDesktop();
if (panel != nil)
{
[panel setDelegate: nil];
if (nsViewPreview != nil)
{
[panel setAccessoryView: nil];
[nsViewPreview release];
}
[panel close];
}
if (delegate != nil)
[delegate release];
}
void launch() override
{
if (panel != nil)
{
setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows());
addToDesktop (0);
enterModalState (true);
MessageManager::callAsync ([ref = SafePointer<Native> (this)]
{
if (ref == nullptr)
return;
[ref->panel beginWithCompletionHandler: ^(NSInteger result)
{
if (auto* ptr = ref.getComponent())
ptr->finished (result);
}];
if (ref->preview != nullptr)
ref->preview->toFront (true);
});
}
}
void runModally() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
ensurePanelSafe();
std::unique_ptr<TemporaryMainMenuWithStandardCommands> tempMenu;
if (JUCEApplicationBase::isStandaloneApp())
tempMenu = std::make_unique<TemporaryMainMenuWithStandardCommands> (preview);
jassert (panel != nil);
auto result = [panel runModal];
finished (result);
#else
jassertfalse;
#endif
}
bool canModalEventBeSentToComponent (const Component* targetComponent) override
{
return TemporaryMainMenuWithStandardCommands::checkModalEvent (preview, targetComponent);
}
private:
//==============================================================================
typedef NSObject<NSOpenSavePanelDelegate> DelegateType;
static URL urlFromNSURL (NSURL* url)
{
const auto scheme = nsStringToJuce ([url scheme]);
auto pathComponents = StringArray::fromTokens (nsStringToJuce ([url path]), "/", {});
for (auto& component : pathComponents)
component = URL::addEscapeChars (component, false);
return { scheme + "://" + pathComponents.joinIntoString ("/") };
}
void finished (NSInteger result)
{
Array<URL> chooserResults;
exitModalState (0);
const auto okResult = []() -> NSInteger
{
if (@available (macOS 10.9, *))
return NSModalResponseOK;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
return NSFileHandlingPanelOKButton;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}();
if (panel != nil && result == okResult)
{
auto addURLResult = [&chooserResults] (NSURL* urlToAdd)
{
chooserResults.add (urlFromNSURL (urlToAdd));
};
if (isSave)
{
addURLResult ([panel URL]);
}
else
{
auto* openPanel = static_cast<NSOpenPanel*> (panel);
auto urls = [openPanel URLs];
for (unsigned int i = 0; i < [urls count]; ++i)
addURLResult ([urls objectAtIndex: i]);
}
}
owner.finished (chooserResults);
}
BOOL shouldShowURL (const URL& urlToTest)
{
for (int i = filters.size(); --i >= 0;)
if (urlToTest.getFileName().matchesWildcard (filters[i], true))
return YES;
const auto f = urlToTest.getLocalFile();
return f.isDirectory()
&& ! [[NSWorkspace sharedWorkspace] isFilePackageAtPath: juceStringToNS (f.getFullPathName())];
}
void panelSelectionDidChange ([[maybe_unused]] id sender)
{
jassert (sender == panel);
// NB: would need to extend FilePreviewComponent to handle the full list rather than just the first one
if (preview != nullptr)
preview->selectedFileChanged (File (getSelectedPaths()[0]));
}
StringArray getSelectedPaths() const
{
if (panel == nullptr)
return {};
StringArray paths;
if (isSave)
{
paths.add (nsStringToJuce ([[panel URL] path]));
}
else
{
auto* urls = [static_cast<NSOpenPanel*> (panel) URLs];
for (NSUInteger i = 0; i < [urls count]; ++i)
paths.add (nsStringToJuce ([[urls objectAtIndex: i] path]));
}
return paths;
}
//==============================================================================
FileChooser& owner;
FilePreviewComponent* preview;
NSView* nsViewPreview = nullptr;
bool selectsDirectories, selectsFiles, isSave, selectMultiple;
NSSavePanel* panel;
DelegateType* delegate;
StringArray filters;
String startingDirectory, filename;
void ensurePanelSafe()
{
// If you hit this, something (probably the plugin host) has modified the panel,
// allowing the application to terminate while the panel's modal loop is running.
// This is a very bad idea! Quitting from within the panel's modal loop may cause
// your plugin/app destructor to run directly from within `runModally`, which will
// dispose all app resources while they're still in use.
// A safer alternative is to invoke the FileChooser with `launchAsync`, rather than
// using the modal launchers.
jassert ([panel preventsApplicationTerminationWhenModal]);
}
static BOOL preventsApplicationTerminationWhenModal (id, SEL) { return YES; }
template <typename Base>
struct SafeModalPanel : public ObjCClass<Base>
{
explicit SafeModalPanel (const char* name) : ObjCClass<Base> (name)
{
this->addMethod (@selector (preventsApplicationTerminationWhenModal),
preventsApplicationTerminationWhenModal);
this->registerClass();
}
};
struct SafeSavePanel : SafeModalPanel<NSSavePanel>
{
SafeSavePanel() : SafeModalPanel ("SafeSavePanel_") {}
};
struct SafeOpenPanel : SafeModalPanel<NSOpenPanel>
{
SafeOpenPanel() : SafeModalPanel ("SafeOpenPanel_") {}
};
//==============================================================================
struct DelegateClass final : public ObjCClass<DelegateType>
{
DelegateClass() : ObjCClass<DelegateType> ("JUCEFileChooser_")
{
addIvar<Native*> ("cppObject");
addMethod (@selector (panel:shouldEnableURL:), shouldEnableURL);
addMethod (@selector (panelSelectionDidChange:), panelSelectionDidChange);
addProtocol (@protocol (NSOpenSavePanelDelegate));
registerClass();
}
private:
static BOOL shouldEnableURL (id self, SEL, id /*sender*/, NSURL* url)
{
return getIvar<Native*> (self, "cppObject")->shouldShowURL (urlFromNSURL (url));
}
static void panelSelectionDidChange (id self, SEL, id sender)
{
getIvar<Native*> (self, "cppObject")->panelSelectionDidChange (sender);
}
};
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Native)
};
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
FilePreviewComponent* preview)
{
return std::make_shared<FileChooser::Native> (owner, flags, preview);
}
bool FileChooser::isPlatformDialogAvailable()
{
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
return false;
#else
return true;
#endif
}
} // namespace juce
@@ -0,0 +1,902 @@
/*
==============================================================================
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
{
// Implemented in juce_Messaging_windows.cpp
namespace detail
{
bool dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages);
} // namespace detail
class Win32NativeFileChooser final : private Thread
{
public:
enum { charsAvailableForResult = 32768 };
Win32NativeFileChooser (Component* parent, int flags, FilePreviewComponent* previewComp,
const File& startingFile, const String& titleToUse,
const String& filtersToUse)
: Thread ("Native Win32 FileChooser"),
owner (parent),
title (titleToUse),
filtersString (filtersToUse.replaceCharacter (',', ';')),
selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0),
// When dealing with directories, it is not possible to 'Save' them. However, one can 'Open' a directory in order to save into it.
// If the 'saveMode' and 'canSelectDirectories' flags are both present, create an FileOpenDialog instead of an FileSaveDialog.
isSave ((flags & FileBrowserComponent::saveMode) != 0 && ! selectsDirectories),
warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0),
selectMultiple ((flags & FileBrowserComponent::canSelectMultipleItems) != 0)
{
auto parentDirectory = startingFile.getParentDirectory();
// Handle nonexistent root directories in the same way as existing ones
files.calloc (static_cast<size_t> (charsAvailableForResult) + 1);
if (startingFile.isDirectory() || startingFile.isRoot())
{
initialPath = startingFile.getFullPathName();
}
else
{
startingFile.getFileName().copyToUTF16 (files,
static_cast<size_t> (charsAvailableForResult) * sizeof (WCHAR));
initialPath = parentDirectory.getFullPathName();
}
if (! selectsDirectories)
{
if (previewComp != nullptr)
customComponent.reset (new CustomComponentHolder (previewComp));
setupFilters();
}
}
~Win32NativeFileChooser() override
{
signalThreadShouldExit();
while (isThreadRunning())
{
if (! detail::dispatchNextMessageOnSystemQueue (true))
Thread::sleep (1);
}
}
void open (bool async)
{
results.clear();
// the thread should not be running
nativeDialogRef.set (nullptr);
if (async)
{
jassert (! isThreadRunning());
startThread();
}
else
{
results = openDialog (false);
owner->exitModalState (results.size() > 0 ? 1 : 0);
}
}
void cancel()
{
ScopedLock lock (deletingDialog);
customComponent = nullptr;
shouldCancel = true;
if (auto hwnd = nativeDialogRef.get())
PostMessage (hwnd, WM_CLOSE, 0, 0);
}
Component* getCustomComponent() { return customComponent.get(); }
Array<URL> results;
private:
//==============================================================================
class CustomComponentHolder final : public Component
{
public:
CustomComponentHolder (Component* const customComp)
{
setVisible (true);
setOpaque (true);
addAndMakeVisible (customComp);
setSize (jlimit (20, 800, customComp->getWidth()), customComp->getHeight());
}
void paint (Graphics& g) override
{
g.fillAll (Colours::lightgrey);
}
void resized() override
{
if (Component* const c = getChildComponent (0))
c->setBounds (getLocalBounds());
}
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomComponentHolder)
};
//==============================================================================
const Component::SafePointer<Component> owner;
String title, filtersString;
std::unique_ptr<CustomComponentHolder> customComponent;
String initialPath, returnedString;
CriticalSection deletingDialog;
bool selectsDirectories, isSave, warnAboutOverwrite, selectMultiple;
HeapBlock<WCHAR> files;
HeapBlock<WCHAR> filters;
Atomic<HWND> nativeDialogRef { nullptr };
bool shouldCancel = false;
struct FreeLPWSTR
{
void operator() (LPWSTR ptr) const noexcept { CoTaskMemFree (ptr); }
};
bool showDialog (IFileDialog& dialog)
{
FILEOPENDIALOGOPTIONS flags = {};
if (FAILED (dialog.GetOptions (&flags)))
return false;
const auto setBit = [] (FILEOPENDIALOGOPTIONS& field, bool value, FILEOPENDIALOGOPTIONS option)
{
if (value)
field |= option;
else
field &= ~option;
};
setBit (flags, selectsDirectories, FOS_PICKFOLDERS);
setBit (flags, warnAboutOverwrite, FOS_OVERWRITEPROMPT);
setBit (flags, selectMultiple, FOS_ALLOWMULTISELECT);
setBit (flags, customComponent != nullptr, FOS_FORCEPREVIEWPANEON);
if (FAILED (dialog.SetOptions (flags)) || FAILED (dialog.SetTitle (title.toUTF16())))
return false;
PIDLIST_ABSOLUTE pidl = {};
if (FAILED (SHParseDisplayName (initialPath.toWideCharPointer(), nullptr, &pidl, SFGAO_FOLDER, nullptr)))
{
LPWSTR ptr = nullptr;
auto result = SHGetKnownFolderPath (FOLDERID_Desktop, 0, nullptr, &ptr);
std::unique_ptr<WCHAR, FreeLPWSTR> desktopPath (ptr);
if (FAILED (result))
return false;
if (FAILED (SHParseDisplayName (desktopPath.get(), nullptr, &pidl, SFGAO_FOLDER, nullptr)))
return false;
}
const auto item = [&]
{
ComSmartPtr<IShellItem> ptr;
SHCreateShellItem (nullptr, nullptr, pidl, ptr.resetAndGetPointerAddress());
return ptr;
}();
if (item != nullptr)
{
dialog.SetDefaultFolder (item);
if (! initialPath.isEmpty())
dialog.SetFolder (item);
}
String filename (files.getData());
if (FAILED (dialog.SetFileName (filename.toWideCharPointer())))
return false;
auto extension = getDefaultFileExtension (filename);
if (extension.isNotEmpty() && FAILED (dialog.SetDefaultExtension (extension.toWideCharPointer())))
return false;
const COMDLG_FILTERSPEC spec[] { { filtersString.toWideCharPointer(), filtersString.toWideCharPointer() } };
if (! selectsDirectories && FAILED (dialog.SetFileTypes (numElementsInArray (spec), spec)))
return false;
struct Events final : public ComBaseClassHelper<IFileDialogEvents>
{
explicit Events (Win32NativeFileChooser& o) : owner (o) {}
JUCE_COMRESULT OnTypeChange (IFileDialog* d) override { return updateHwnd (d); }
JUCE_COMRESULT OnFolderChanging (IFileDialog* d, IShellItem*) override { return updateHwnd (d); }
JUCE_COMRESULT OnFileOk (IFileDialog* d) override { return updateHwnd (d); }
JUCE_COMRESULT OnFolderChange (IFileDialog* d) override { return updateHwnd (d); }
JUCE_COMRESULT OnSelectionChange (IFileDialog* d) override { focusWorkaround(); return updateHwnd (d); }
JUCE_COMRESULT OnShareViolation (IFileDialog* d, IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) override { return updateHwnd (d); }
JUCE_COMRESULT OnOverwrite (IFileDialog* d, IShellItem*, FDE_OVERWRITE_RESPONSE*) override { return updateHwnd (d); }
/* Workaround for a bug in Vista+, OpenFileDialog will truncate the initialFile text.
Moving the caret along the full length of the text and back will reveal the full string.
*/
void focusWorkaround()
{
if (! defaultFileNameTextCaretMoved)
{
auto makeKeyInput = [] (WORD vk, bool pressed)
{
INPUT i;
ZeroMemory (&i, sizeof (INPUT));
i.type = INPUT_KEYBOARD;
i.ki.wVk = vk;
i.ki.dwFlags = pressed ? 0 : KEYEVENTF_KEYUP;
return i;
};
INPUT inputs[] = {
makeKeyInput (VK_HOME, true),
makeKeyInput (VK_HOME, false),
makeKeyInput (VK_END, true),
makeKeyInput (VK_END, false),
};
SendInput ((UINT) std::size (inputs), inputs, sizeof (INPUT));
defaultFileNameTextCaretMoved = true;
}
}
JUCE_COMRESULT updateHwnd (IFileDialog* d)
{
HWND hwnd = nullptr;
if (auto window = ComSmartPtr<IFileDialog> { d }.getInterface<IOleWindow>())
window->GetWindow (&hwnd);
ScopedLock lock (owner.deletingDialog);
if (owner.shouldCancel)
d->Close (S_FALSE);
else if (hwnd != nullptr)
owner.nativeDialogRef = hwnd;
return S_OK;
}
bool defaultFileNameTextCaretMoved = false;
Win32NativeFileChooser& owner;
};
{
ScopedLock lock (deletingDialog);
if (shouldCancel)
return false;
}
const auto result = [&]
{
struct ScopedAdvise
{
ScopedAdvise (IFileDialog& d, Events& events) : dialog (d) { dialog.Advise (&events, &cookie); }
~ScopedAdvise() { dialog.Unadvise (cookie); }
IFileDialog& dialog;
DWORD cookie = 0;
};
Events events { *this };
ScopedAdvise scope { dialog, events };
return dialog.Show (GetActiveWindow()) == S_OK;
}();
ScopedLock lock (deletingDialog);
nativeDialogRef = nullptr;
return result;
}
//==============================================================================
Array<URL> openDialogVistaAndUp()
{
const auto getUrl = [] (IShellItem& item)
{
LPWSTR ptr = nullptr;
if (item.GetDisplayName (SIGDN_FILESYSPATH, &ptr) != S_OK)
return URL();
const auto path = std::unique_ptr<WCHAR, FreeLPWSTR> { ptr };
return URL (File (String (path.get())));
};
if (isSave)
{
const auto dialog = [&]
{
ComSmartPtr<IFileDialog> ptr;
ptr.CoCreateInstance (CLSID_FileSaveDialog, CLSCTX_INPROC_SERVER);
return ptr;
}();
if (dialog == nullptr)
return {};
showDialog (*dialog);
const auto item = [&]
{
ComSmartPtr<IShellItem> ptr;
dialog->GetResult (ptr.resetAndGetPointerAddress());
return ptr;
}();
if (item == nullptr)
return {};
const auto url = getUrl (*item);
if (url.isEmpty())
return {};
return { url };
}
const auto dialog = [&]
{
ComSmartPtr<IFileOpenDialog> ptr;
ptr.CoCreateInstance (CLSID_FileOpenDialog, CLSCTX_INPROC_SERVER);
return ptr;
}();
if (dialog == nullptr)
return {};
showDialog (*dialog);
const auto items = [&]
{
ComSmartPtr<IShellItemArray> ptr;
dialog->GetResults (ptr.resetAndGetPointerAddress());
return ptr;
}();
if (items == nullptr)
return {};
Array<URL> result;
DWORD numItems = 0;
items->GetCount (&numItems);
for (DWORD i = 0; i < numItems; ++i)
{
ComSmartPtr<IShellItem> scope;
items->GetItemAt (i, scope.resetAndGetPointerAddress());
if (scope != nullptr)
{
const auto url = getUrl (*scope);
if (! url.isEmpty())
result.add (url);
}
}
return result;
}
Array<URL> openDialogPreVista (bool async)
{
Array<URL> selections;
if (selectsDirectories)
{
BROWSEINFO bi = {};
bi.hwndOwner = GetActiveWindow();
bi.pszDisplayName = files;
bi.lpszTitle = title.toWideCharPointer();
bi.lParam = (LPARAM) this;
bi.lpfn = browseCallbackProc;
#ifdef BIF_USENEWUI
bi.ulFlags = BIF_USENEWUI | BIF_VALIDATE;
#else
bi.ulFlags = 0x50;
#endif
LPITEMIDLIST list = SHBrowseForFolder (&bi);
if (! SHGetPathFromIDListW (list, files))
{
files[0] = 0;
returnedString.clear();
}
LPMALLOC al;
if (list != nullptr && SUCCEEDED (SHGetMalloc (&al)))
al->Free (list);
if (files[0] != 0)
{
File result (String (files.get()));
if (returnedString.isNotEmpty())
result = result.getSiblingFile (returnedString);
selections.add (URL (result));
}
}
else
{
OPENFILENAMEW of = {};
#ifdef OPENFILENAME_SIZE_VERSION_400W
of.lStructSize = OPENFILENAME_SIZE_VERSION_400W;
#else
of.lStructSize = sizeof (of);
#endif
if (files[0] != 0)
{
auto startingFile = File (initialPath).getChildFile (String (files.get()));
startingFile.getFullPathName().copyToUTF16 (files, charsAvailableForResult * sizeof (WCHAR));
}
of.hwndOwner = GetActiveWindow();
of.lpstrFilter = filters.getData();
of.nFilterIndex = 1;
of.lpstrFile = files;
of.nMaxFile = (DWORD) charsAvailableForResult;
of.lpstrInitialDir = initialPath.toWideCharPointer();
of.lpstrTitle = title.toWideCharPointer();
of.Flags = getOpenFilenameFlags (async);
of.lCustData = (LPARAM) this;
of.lpfnHook = &openCallback;
if (isSave)
{
auto extension = getDefaultFileExtension (files.getData());
if (extension.isNotEmpty())
of.lpstrDefExt = extension.toWideCharPointer();
if (! GetSaveFileName (&of))
return {};
}
else
{
if (! GetOpenFileName (&of))
return {};
}
if (selectMultiple && of.nFileOffset > 0 && files[of.nFileOffset - 1] == 0)
{
const WCHAR* filename = files + of.nFileOffset;
while (*filename != 0)
{
selections.add (URL (File (String (files.get())).getChildFile (String (filename))));
filename += wcslen (filename) + 1;
}
}
else if (files[0] != 0)
{
selections.add (URL (File (String (files.get()))));
}
}
return selections;
}
Array<URL> openDialog (bool async)
{
struct Remover
{
explicit Remover (Win32NativeFileChooser& chooser) : item (chooser) {}
~Remover() { getNativeDialogList().removeValue (&item); }
Win32NativeFileChooser& item;
};
const Remover remover (*this);
if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista
&& customComponent == nullptr)
{
return openDialogVistaAndUp();
}
return openDialogPreVista (async);
}
void run() override
{
results = [&]
{
struct ScopedCoInitialize
{
// IUnknown_GetWindow will only succeed when instantiated in a single-thread apartment
ScopedCoInitialize() { [[maybe_unused]] const auto result = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); }
~ScopedCoInitialize() { CoUninitialize(); }
};
ScopedCoInitialize scope;
return openDialog (true);
}();
auto safeOwner = owner;
auto resultCode = results.size() > 0 ? 1 : 0;
MessageManager::callAsync ([resultCode, safeOwner]
{
if (safeOwner != nullptr)
safeOwner->exitModalState (resultCode);
});
}
static HashMap<HWND, Win32NativeFileChooser*>& getNativeDialogList()
{
static HashMap<HWND, Win32NativeFileChooser*> dialogs;
return dialogs;
}
static Win32NativeFileChooser* getNativePointerForDialog (HWND hwnd)
{
return getNativeDialogList()[hwnd];
}
//==============================================================================
void setupFilters()
{
const size_t filterSpaceNumChars = 2048;
filters.calloc (filterSpaceNumChars);
const size_t bytesWritten = filtersString.copyToUTF16 (filters.getData(), filterSpaceNumChars * sizeof (WCHAR));
filtersString.copyToUTF16 (filters + (bytesWritten / sizeof (WCHAR)),
((filterSpaceNumChars - 1) * sizeof (WCHAR) - bytesWritten));
for (size_t i = 0; i < filterSpaceNumChars; ++i)
if (filters[i] == '|')
filters[i] = 0;
}
DWORD getOpenFilenameFlags (bool async)
{
DWORD ofFlags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING;
if (warnAboutOverwrite)
ofFlags |= OFN_OVERWRITEPROMPT;
if (selectMultiple)
ofFlags |= OFN_ALLOWMULTISELECT;
if (async || customComponent != nullptr)
ofFlags |= OFN_ENABLEHOOK;
return ofFlags;
}
String getDefaultFileExtension (const String& filename) const
{
const auto extension = filename.contains (".") ? filename.fromLastOccurrenceOf (".", false, false)
: String();
if (! extension.isEmpty())
return extension;
auto tokens = StringArray::fromTokens (filtersString, ";,", "\"'");
tokens.trim();
tokens.removeEmptyStrings();
if (tokens.size() == 1 && tokens[0].removeCharacters ("*.").isNotEmpty())
return tokens[0].fromFirstOccurrenceOf (".", false, false);
return {};
}
//==============================================================================
void initialised (HWND hWnd)
{
SendMessage (hWnd, BFFM_SETSELECTIONW, TRUE, (LPARAM) initialPath.toWideCharPointer());
initDialog (hWnd);
}
void validateFailed (const String& path)
{
returnedString = path;
}
void initDialog (HWND hdlg)
{
ScopedLock lock (deletingDialog);
getNativeDialogList().set (hdlg, this);
if (shouldCancel)
{
EndDialog (hdlg, 0);
}
else
{
nativeDialogRef.set (hdlg);
if (customComponent != nullptr)
{
Component::SafePointer<Component> safeCustomComponent (customComponent.get());
RECT dialogScreenRect, dialogClientRect;
GetWindowRect (hdlg, &dialogScreenRect);
GetClientRect (hdlg, &dialogClientRect);
auto screenRectangle = Rectangle<int>::leftTopRightBottom (dialogScreenRect.left, dialogScreenRect.top,
dialogScreenRect.right, dialogScreenRect.bottom);
auto scale = Desktop::getInstance().getDisplays().getDisplayForRect (screenRectangle, true)->scale;
auto physicalComponentWidth = roundToInt (safeCustomComponent->getWidth() * scale);
SetWindowPos (hdlg, nullptr, screenRectangle.getX(), screenRectangle.getY(),
physicalComponentWidth + jmax (150, screenRectangle.getWidth()),
jmax (150, screenRectangle.getHeight()),
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
auto appendCustomComponent = [safeCustomComponent, dialogClientRect, scale, hdlg]() mutable
{
if (safeCustomComponent != nullptr)
{
auto scaledClientRectangle = Rectangle<int>::leftTopRightBottom (dialogClientRect.left, dialogClientRect.top,
dialogClientRect.right, dialogClientRect.bottom) / scale;
safeCustomComponent->setBounds (scaledClientRectangle.getRight(), scaledClientRectangle.getY(),
safeCustomComponent->getWidth(), scaledClientRectangle.getHeight());
safeCustomComponent->addToDesktop (0, hdlg);
}
};
if (MessageManager::getInstance()->isThisTheMessageThread())
appendCustomComponent();
else
MessageManager::callAsync (appendCustomComponent);
}
}
}
void destroyDialog (HWND hdlg)
{
ScopedLock exiting (deletingDialog);
getNativeDialogList().remove (hdlg);
nativeDialogRef.set (nullptr);
if (MessageManager::getInstance()->isThisTheMessageThread())
customComponent = nullptr;
else
MessageManager::callAsync ([this] { customComponent = nullptr; });
}
void selectionChanged (HWND hdlg)
{
ScopedLock lock (deletingDialog);
if (customComponent != nullptr && ! shouldCancel)
{
if (FilePreviewComponent* comp = dynamic_cast<FilePreviewComponent*> (customComponent->getChildComponent (0)))
{
WCHAR path [MAX_PATH * 2] = { 0 };
CommDlg_OpenSave_GetFilePath (hdlg, (LPARAM) &path, MAX_PATH);
if (MessageManager::getInstance()->isThisTheMessageThread())
{
comp->selectedFileChanged (File (path));
}
else
{
MessageManager::callAsync ([safeComp = Component::SafePointer<FilePreviewComponent> { comp },
selectedFile = File { path }]() mutable
{
if (safeComp != nullptr)
safeComp->selectedFileChanged (selectedFile);
});
}
}
}
}
//==============================================================================
static int CALLBACK browseCallbackProc (HWND hWnd, UINT msg, LPARAM lParam, LPARAM lpData)
{
auto* self = reinterpret_cast<Win32NativeFileChooser*> (lpData);
switch (msg)
{
case BFFM_INITIALIZED: self->initialised (hWnd); break;
case BFFM_VALIDATEFAILEDW: self->validateFailed (String ((LPCWSTR) lParam)); break;
case BFFM_VALIDATEFAILEDA: self->validateFailed (String ((const char*) lParam)); break;
default: break;
}
return 0;
}
static UINT_PTR CALLBACK openCallback (HWND hwnd, UINT uiMsg, WPARAM /*wParam*/, LPARAM lParam)
{
auto hdlg = getDialogFromHWND (hwnd);
switch (uiMsg)
{
case WM_INITDIALOG:
{
if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (((OPENFILENAMEW*) lParam)->lCustData))
self->initDialog (hdlg);
break;
}
case WM_DESTROY:
{
if (auto* self = getNativeDialogList()[hdlg])
self->destroyDialog (hdlg);
break;
}
case WM_NOTIFY:
{
auto ofn = reinterpret_cast<LPOFNOTIFY> (lParam);
if (ofn->hdr.code == CDN_SELCHANGE)
if (auto* self = reinterpret_cast<Win32NativeFileChooser*> (ofn->lpOFN->lCustData))
self->selectionChanged (hdlg);
break;
}
default:
break;
}
return 0;
}
static HWND getDialogFromHWND (HWND hwnd)
{
if (hwnd == nullptr)
return nullptr;
HWND dialogH = GetParent (hwnd);
if (dialogH == nullptr)
dialogH = hwnd;
return dialogH;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Win32NativeFileChooser)
};
class FileChooser::Native final : public std::enable_shared_from_this<Native>,
public Component,
public FileChooser::Pimpl
{
public:
Native (FileChooser& fileChooser, int flagsIn, FilePreviewComponent* previewComp)
: owner (fileChooser),
nativeFileChooser (std::make_unique<Win32NativeFileChooser> (this, flagsIn, previewComp, fileChooser.startingFile,
fileChooser.title, fileChooser.filters))
{
auto mainMon = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
setBounds (mainMon.getX() + mainMon.getWidth() / 4,
mainMon.getY() + mainMon.getHeight() / 4,
0, 0);
setOpaque (true);
setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows());
addToDesktop (0);
}
~Native() override
{
exitModalState (0);
nativeFileChooser->cancel();
}
void launch() override
{
std::weak_ptr<Native> safeThis = shared_from_this();
enterModalState (true, ModalCallbackFunction::create ([safeThis] (int)
{
if (auto locked = safeThis.lock())
locked->owner.finished (locked->nativeFileChooser->results);
}));
nativeFileChooser->open (true);
}
void runModally() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
enterModalState (true);
nativeFileChooser->open (false);
exitModalState (nativeFileChooser->results.size() > 0 ? 1 : 0);
nativeFileChooser->cancel();
owner.finished (nativeFileChooser->results);
#else
jassertfalse;
#endif
}
bool canModalEventBeSentToComponent (const Component* targetComponent) override
{
if (targetComponent == nullptr)
return false;
if (targetComponent == nativeFileChooser->getCustomComponent())
return true;
return targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr;
}
void inputAttemptWhenModal() override {}
private:
FileChooser& owner;
std::shared_ptr<Win32NativeFileChooser> nativeFileChooser;
};
//==============================================================================
bool FileChooser::isPlatformDialogAvailable()
{
#if JUCE_DISABLE_NATIVE_FILECHOOSERS
return false;
#else
return true;
#endif
}
std::shared_ptr<FileChooser::Pimpl> FileChooser::showPlatformDialog (FileChooser& owner, int flags,
FilePreviewComponent* preview)
{
return std::make_shared<FileChooser::Native> (owner, flags, preview);
}
} // namespace juce
@@ -0,0 +1,849 @@
/*
==============================================================================
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
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
const auto menuItemInvokedSelector = @selector (menuItemInvoked:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
//==============================================================================
struct JuceMainMenuBarHolder final : private DeletedAtShutdown
{
JuceMainMenuBarHolder()
: mainMenuBar ([[NSMenu alloc] initWithTitle: nsStringLiteral ("MainMenu")])
{
auto item = [mainMenuBar addItemWithTitle: nsStringLiteral ("Apple")
action: nil
keyEquivalent: nsEmptyString()];
auto appMenu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Apple")];
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
[NSApp performSelector: @selector (setAppleMenu:) withObject: appMenu];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
[mainMenuBar setSubmenu: appMenu forItem: item];
[appMenu release];
[NSApp setMainMenu: mainMenuBar];
}
~JuceMainMenuBarHolder()
{
clearSingletonInstance();
[NSApp setMainMenu: nil];
[mainMenuBar release];
}
NSMenu* mainMenuBar = nil;
JUCE_DECLARE_SINGLETON_SINGLETHREADED (JuceMainMenuBarHolder, true)
};
JUCE_IMPLEMENT_SINGLETON (JuceMainMenuBarHolder)
//==============================================================================
class JuceMainMenuHandler final : private MenuBarModel::Listener,
private DeletedAtShutdown
{
public:
JuceMainMenuHandler()
{
static JuceMenuCallbackClass cls;
callback = [cls.createInstance() init];
JuceMenuCallbackClass::setOwner (callback, this);
}
~JuceMainMenuHandler() override
{
setMenu (nullptr, nullptr, String());
jassert (instance == this);
instance = nullptr;
[callback release];
}
void setMenu (MenuBarModel* const newMenuBarModel,
const PopupMenu* newExtraAppleMenuItems,
const String& recentItemsName)
{
recentItemsMenuName = recentItemsName;
if (currentModel != newMenuBarModel)
{
if (currentModel != nullptr)
currentModel->removeListener (this);
currentModel = newMenuBarModel;
if (currentModel != nullptr)
currentModel->addListener (this);
menuBarItemsChanged (nullptr);
}
extraAppleMenuItems.reset (createCopyIfNotNull (newExtraAppleMenuItems));
}
void addTopLevelMenu (NSMenu* parent, const PopupMenu& child, const String& name, int menuId, int topLevelIndex)
{
NSMenuItem* item = [parent addItemWithTitle: juceStringToNS (name)
action: nil
keyEquivalent: nsEmptyString()];
NSMenu* sub = createMenu (child, name, menuId, topLevelIndex, true);
[parent setSubmenu: sub forItem: item];
[sub release];
}
void updateTopLevelMenu (NSMenuItem* parentItem, const PopupMenu& menuToCopy, const String& name, int menuId, int topLevelIndex)
{
// Note: This method used to update the contents of the existing menu in-place, but that caused
// weird side-effects which messed-up keyboard focus when switching between windows. By creating
// a new menu and replacing the old one with it, that problem seems to be avoided..
NSMenu* menu = [[NSMenu alloc] initWithTitle: juceStringToNS (name)];
for (PopupMenu::MenuItemIterator iter (menuToCopy); iter.next();)
addMenuItem (iter, menu, menuId, topLevelIndex);
[menu update];
removeItemRecursive ([parentItem submenu]);
[parentItem setSubmenu: menu];
[menu release];
}
void updateTopLevelMenu (NSMenu* menu)
{
NSMenu* superMenu = [menu supermenu];
auto menuNames = currentModel->getMenuBarNames();
auto indexOfMenu = (int) [superMenu indexOfItemWithSubmenu: menu] - 1;
if (indexOfMenu >= 0)
{
removeItemRecursive (menu);
auto updatedPopup = currentModel->getMenuForIndex (indexOfMenu, menuNames[indexOfMenu]);
for (PopupMenu::MenuItemIterator iter (updatedPopup); iter.next();)
addMenuItem (iter, menu, 1, indexOfMenu);
[menu update];
}
}
void menuBarItemsChanged (MenuBarModel*) override
{
if (isOpen)
{
defferedUpdateRequested = true;
return;
}
lastUpdateTime = Time::getMillisecondCounter();
StringArray menuNames;
if (currentModel != nullptr)
menuNames = currentModel->getMenuBarNames();
auto* menuBar = getMainMenuBar();
while ([menuBar numberOfItems] > 1 + menuNames.size())
removeItemRecursive (menuBar, static_cast<int> ([menuBar numberOfItems] - 1));
int menuId = 1;
for (int i = 0; i < menuNames.size(); ++i)
{
const PopupMenu menu (currentModel->getMenuForIndex (i, menuNames[i]));
if (i >= [menuBar numberOfItems] - 1)
addTopLevelMenu (menuBar, menu, menuNames[i], menuId, i);
else
updateTopLevelMenu ([menuBar itemAtIndex: 1 + i], menu, menuNames[i], menuId, i);
}
}
void menuCommandInvoked (MenuBarModel*, const ApplicationCommandTarget::InvocationInfo& info) override
{
if ((info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0
&& info.invocationMethod != ApplicationCommandTarget::InvocationInfo::fromKeyPress)
if (auto* item = findMenuItemWithCommandID (getMainMenuBar(), info.commandID))
flashMenuBar ([item menu]);
}
void invoke (const PopupMenu::Item& item, int topLevelIndex) const
{
if (currentModel != nullptr)
{
if (item.action != nullptr)
{
MessageManager::callAsync (item.action);
return;
}
if (item.customCallback != nullptr)
if (! item.customCallback->menuItemTriggered())
return;
if (item.commandManager != nullptr)
{
ApplicationCommandTarget::InvocationInfo info (item.itemID);
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromMenu;
item.commandManager->invoke (info, true);
}
MessageManager::callAsync ([=]
{
if (instance != nullptr)
instance->invokeDirectly (item.itemID, topLevelIndex);
});
}
}
void invokeDirectly (int commandId, int topLevelIndex)
{
if (currentModel != nullptr)
currentModel->menuItemSelected (commandId, topLevelIndex);
}
void addMenuItem (PopupMenu::MenuItemIterator& iter, NSMenu* menuToAddTo,
const int topLevelMenuId, const int topLevelIndex)
{
const PopupMenu::Item& i = iter.getItem();
NSString* text = juceStringToNS (i.text);
if (text == nil)
text = nsEmptyString();
if (i.isSeparator)
{
[menuToAddTo addItem: [NSMenuItem separatorItem]];
}
else if (i.isSectionHeader)
{
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
action: nil
keyEquivalent: nsEmptyString()];
[item setEnabled: false];
}
else if (i.subMenu != nullptr)
{
if (recentItemsMenuName.isNotEmpty() && i.text == recentItemsMenuName)
{
if (recent == nullptr)
recent = std::make_unique<RecentFilesMenuItem>();
if (recent->recentItem != nil)
{
if (NSMenu* parent = [recent->recentItem menu])
[parent removeItem: recent->recentItem];
[menuToAddTo addItem: recent->recentItem];
return;
}
}
NSMenuItem* item = [menuToAddTo addItemWithTitle: text
action: nil
keyEquivalent: nsEmptyString()];
[item setTag: i.itemID];
[item setEnabled: i.isEnabled];
NSMenu* sub = createMenu (*i.subMenu, i.text, topLevelMenuId, topLevelIndex, false);
[menuToAddTo setSubmenu: sub forItem: item];
[sub release];
}
else
{
auto item = [[NSMenuItem alloc] initWithTitle: text
action: menuItemInvokedSelector
keyEquivalent: nsEmptyString()];
[item setTag: topLevelIndex];
[item setEnabled: i.isEnabled];
[item setState: i.isTicked ? NSControlStateValueOn : NSControlStateValueOff];
[item setTarget: (id) callback];
auto* juceItem = new PopupMenu::Item (i);
juceItem->customComponent = nullptr;
[item setRepresentedObject: [createNSObjectFromJuceClass (juceItem) autorelease]];
if (i.commandManager != nullptr)
{
for (auto& kp : i.commandManager->getKeyMappings()->getKeyPressesAssignedToCommand (i.itemID))
{
if (kp != KeyPress::backspaceKey // (adding these is annoying because it flashes the menu bar
&& kp != KeyPress::deleteKey) // every time you press the key while editing text)
{
juce_wchar key = kp.getTextCharacter();
if (key == 0)
key = (juce_wchar) kp.getKeyCode();
[item setKeyEquivalent: juceStringToNS (String::charToString (key).toLowerCase())];
[item setKeyEquivalentModifierMask: juceModsToNSMods (kp.getModifiers())];
}
break;
}
}
[menuToAddTo addItem: item];
[item release];
}
}
NSMenu* createMenu (const PopupMenu menu,
const String& menuName,
const int topLevelMenuId,
const int topLevelIndex,
const bool addDelegate)
{
NSMenu* m = [[NSMenu alloc] initWithTitle: juceStringToNS (menuName)];
if (addDelegate)
[m setDelegate: (id<NSMenuDelegate>) callback];
for (PopupMenu::MenuItemIterator iter (menu); iter.next();)
addMenuItem (iter, m, topLevelMenuId, topLevelIndex);
[m update];
return m;
}
static JuceMainMenuHandler* instance;
MenuBarModel* currentModel = nullptr;
std::unique_ptr<PopupMenu> extraAppleMenuItems;
uint32 lastUpdateTime = 0;
NSObject* callback = nil;
String recentItemsMenuName;
bool isOpen = false, defferedUpdateRequested = false;
private:
struct RecentFilesMenuItem
{
RecentFilesMenuItem() : recentItem (nil)
{
if (NSNib* menuNib = [[[NSNib alloc] initWithNibNamed: @"RecentFilesMenuTemplate" bundle: nil] autorelease])
{
NSArray* array = nil;
if (@available (macOS 10.11, *))
{
[menuNib instantiateWithOwner: NSApp
topLevelObjects: &array];
}
else
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
[menuNib instantiateNibWithOwner: NSApp
topLevelObjects: &array];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
for (id object in array)
{
if ([object isKindOfClass: [NSMenu class]])
{
if (NSArray* items = [object itemArray])
{
if (NSMenuItem* item = findRecentFilesItem (items))
{
recentItem = [item retain];
break;
}
}
}
}
}
}
~RecentFilesMenuItem()
{
[recentItem release];
}
static NSMenuItem* findRecentFilesItem (NSArray* const items)
{
for (id object in items)
if (NSArray* subMenuItems = [[object submenu] itemArray])
for (id subObject in subMenuItems)
if ([subObject isKindOfClass: [NSMenuItem class]])
return subObject;
return nil;
}
NSMenuItem* recentItem;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecentFilesMenuItem)
};
std::unique_ptr<RecentFilesMenuItem> recent;
//==============================================================================
static NSMenuItem* findMenuItemWithCommandID (NSMenu* const menu, int commandID)
{
for (NSInteger i = [menu numberOfItems]; --i >= 0;)
{
NSMenuItem* m = [menu itemAtIndex: i];
if (auto* menuItem = getJuceClassFromNSObject<PopupMenu::Item> ([m representedObject]))
if (menuItem->itemID == commandID)
return m;
if (NSMenu* sub = [m submenu])
if (NSMenuItem* found = findMenuItemWithCommandID (sub, commandID))
return found;
}
return nil;
}
static void flashMenuBar (NSMenu* menu)
{
if ([[menu title] isEqualToString: nsStringLiteral ("Apple")])
return;
[menu retain];
const unichar f35Key = NSF35FunctionKey;
NSString* f35String = [NSString stringWithCharacters: &f35Key length: 1];
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle: nsStringLiteral ("x")
action: menuItemInvokedSelector
keyEquivalent: f35String];
// When the f35Event is invoked, the item's enablement is checked and a
// NSBeep is triggered if the item appears to be disabled.
// This ValidatorClass exists solely to return YES from validateMenuItem.
struct ValidatorClass final : public ObjCClass<NSObject>
{
ValidatorClass() : ObjCClass ("JUCEMenuValidator_")
{
addMethod (menuItemInvokedSelector, [] (id, SEL, NSMenuItem*) {});
addMethod (@selector (validateMenuItem:), [] (id, SEL, NSMenuItem*) { return YES; });
addProtocol (@protocol (NSMenuItemValidation));
registerClass();
}
};
static ValidatorClass validatorClass;
static auto* vcInstance = validatorClass.createInstance();
[item setTarget: vcInstance];
[menu insertItem: item atIndex: [menu numberOfItems]];
[item release];
if ([menu indexOfItem: item] >= 0)
{
NSEvent* f35Event = [NSEvent keyEventWithType: NSEventTypeKeyDown
location: NSZeroPoint
modifierFlags: NSEventModifierFlagCommand
timestamp: 0
windowNumber: 0
context: [NSGraphicsContext currentContext]
characters: f35String
charactersIgnoringModifiers: f35String
isARepeat: NO
keyCode: 0];
[menu performKeyEquivalent: f35Event];
if ([menu indexOfItem: item] >= 0)
[menu removeItem: item]; // (this throws if the item isn't actually in the menu)
}
[menu release];
}
static unsigned int juceModsToNSMods (const ModifierKeys mods)
{
unsigned int m = 0;
if (mods.isShiftDown()) m |= NSEventModifierFlagShift;
if (mods.isCtrlDown()) m |= NSEventModifierFlagControl;
if (mods.isAltDown()) m |= NSEventModifierFlagOption;
if (mods.isCommandDown()) m |= NSEventModifierFlagCommand;
return m;
}
// Apple Bug: For some reason [NSMenu removeAllItems] seems to leak its objects
// on shutdown, so we need this method to release the items one-by-one manually
static void removeItemRecursive (NSMenu* parentMenu, int menuItemIndex)
{
if (isPositiveAndBelow (menuItemIndex, (int) [parentMenu numberOfItems]))
{
if (auto menuItem = [parentMenu itemAtIndex:menuItemIndex])
{
if (auto submenu = [menuItem submenu])
removeItemRecursive (submenu);
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
[parentMenu removeItem: menuItem];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
}
else
jassertfalse;
}
static void removeItemRecursive (NSMenu* menu)
{
if (menu != nullptr)
{
auto n = static_cast<int> ([menu numberOfItems]);
for (auto i = n; --i >= 0;)
removeItemRecursive (menu, i);
}
}
static NSMenu* getMainMenuBar()
{
return JuceMainMenuBarHolder::getInstance()->mainMenuBar;
}
//==============================================================================
struct JuceMenuCallbackClass final : public ObjCClass<NSObject>
{
JuceMenuCallbackClass() : ObjCClass ("JUCEMainMenu_")
{
addIvar<JuceMainMenuHandler*> ("owner");
addMethod (menuItemInvokedSelector, [] (id self, SEL, NSMenuItem* item)
{
if (auto* juceItem = getPopupMenuItem (item))
getOwner (self)->invoke (*juceItem, static_cast<int> ([item tag]));
});
addMethod (@selector (menuNeedsUpdate:), [] (id self, SEL, NSMenu* menu)
{
getOwner (self)->updateTopLevelMenu (menu);
});
addMethod (@selector (validateMenuItem:), [] (id, SEL, NSMenuItem* item) -> BOOL
{
if (auto* juceItem = getPopupMenuItem (item))
return juceItem->isEnabled;
return YES;
});
addProtocol (@protocol (NSMenuDelegate));
addProtocol (@protocol (NSMenuItemValidation));
registerClass();
}
static void setOwner (id self, JuceMainMenuHandler* owner)
{
object_setInstanceVariable (self, "owner", owner);
}
private:
static PopupMenu::Item* getPopupMenuItem (NSMenuItem* item)
{
return getJuceClassFromNSObject<PopupMenu::Item> ([item representedObject]);
}
static JuceMainMenuHandler* getOwner (id self)
{
return getIvar<JuceMainMenuHandler*> (self, "owner");
}
};
};
JuceMainMenuHandler* JuceMainMenuHandler::instance = nullptr;
//==============================================================================
class TemporaryMainMenuWithStandardCommands
{
public:
explicit TemporaryMainMenuWithStandardCommands (FilePreviewComponent* filePreviewComponent)
: oldMenu (MenuBarModel::getMacMainMenu()), dummyModalComponent (filePreviewComponent)
{
if (auto* appleMenu = MenuBarModel::getMacExtraAppleItemsMenu())
oldAppleMenu = std::make_unique<PopupMenu> (*appleMenu);
if (auto* handler = JuceMainMenuHandler::instance)
oldRecentItems = handler->recentItemsMenuName;
MenuBarModel::setMacMainMenu (nullptr);
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
{
NSMenu* menu = [[NSMenu alloc] initWithTitle: nsStringLiteral ("Edit")];
NSMenuItem* item;
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Cut"), nil)
action: @selector (cut:) keyEquivalent: nsStringLiteral ("x")];
[menu addItem: item];
[item release];
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Copy"), nil)
action: @selector (copy:) keyEquivalent: nsStringLiteral ("c")];
[menu addItem: item];
[item release];
item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString (nsStringLiteral ("Paste"), nil)
action: @selector (paste:) keyEquivalent: nsStringLiteral ("v")];
[menu addItem: item];
[item release];
editMenuIndex = [mainMenu numberOfItems];
item = [mainMenu addItemWithTitle: NSLocalizedString (nsStringLiteral ("Edit"), nil)
action: nil keyEquivalent: nsEmptyString()];
[mainMenu setSubmenu: menu forItem: item];
[menu release];
}
// use a dummy modal component so that apps can tell that something is currently modal.
dummyModalComponent.enterModalState (false);
}
~TemporaryMainMenuWithStandardCommands()
{
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
[mainMenu removeItemAtIndex:editMenuIndex];
MenuBarModel::setMacMainMenu (oldMenu, oldAppleMenu.get(), oldRecentItems);
}
static bool checkModalEvent (FilePreviewComponent* preview, const Component* targetComponent)
{
if (targetComponent == nullptr)
return false;
return (targetComponent == preview
|| targetComponent->findParentComponentOfClass<FilePreviewComponent>() != nullptr);
}
private:
MenuBarModel* const oldMenu = nullptr;
std::unique_ptr<PopupMenu> oldAppleMenu;
String oldRecentItems;
NSInteger editMenuIndex;
// The OS view already plays an alert when clicking outside
// the modal comp, so this override avoids adding extra
// inappropriate noises when the cancel button is pressed.
// This override is also important because it stops the base class
// calling ModalComponentManager::bringToFront, which can get
// recursive when file dialogs are involved
struct SilentDummyModalComp final : public Component
{
explicit SilentDummyModalComp (FilePreviewComponent* p)
: preview (p) {}
void inputAttemptWhenModal() override {}
bool canModalEventBeSentToComponent (const Component* targetComponent) override
{
return checkModalEvent (preview, targetComponent);
}
FilePreviewComponent* preview = nullptr;
};
SilentDummyModalComp dummyModalComponent;
};
//==============================================================================
namespace MainMenuHelpers
{
static NSString* translateMenuName (const String& name)
{
return NSLocalizedString (juceStringToNS (TRANS (name)), nil);
}
static NSMenuItem* createMenuItem (NSMenu* menu, const String& name, SEL sel, NSString* key)
{
NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle: translateMenuName (name)
action: sel
keyEquivalent: key] autorelease];
[item setTarget: NSApp];
[menu addItem: item];
return item;
}
static void createStandardAppMenu (NSMenu* menu, const String& appName, const PopupMenu* extraItems)
{
if (extraItems != nullptr && JuceMainMenuHandler::instance != nullptr && extraItems->getNumItems() > 0)
{
for (PopupMenu::MenuItemIterator iter (*extraItems); iter.next();)
JuceMainMenuHandler::instance->addMenuItem (iter, menu, 0, -1);
[menu addItem: [NSMenuItem separatorItem]];
}
// Services...
NSMenuItem* services = [[[NSMenuItem alloc] initWithTitle: translateMenuName ("Services")
action: nil keyEquivalent: nsEmptyString()] autorelease];
[menu addItem: services];
NSMenu* servicesMenu = [[[NSMenu alloc] initWithTitle: translateMenuName ("Services")] autorelease];
[menu setSubmenu: servicesMenu forItem: services];
[NSApp setServicesMenu: servicesMenu];
[menu addItem: [NSMenuItem separatorItem]];
createMenuItem (menu, TRANS ("Hide") + String (" ") + appName, @selector (hide:), nsStringLiteral ("h"));
[createMenuItem (menu, TRANS ("Hide Others"), @selector (hideOtherApplications:), nsStringLiteral ("h"))
setKeyEquivalentModifierMask: NSEventModifierFlagCommand | NSEventModifierFlagOption];
createMenuItem (menu, TRANS ("Show All"), @selector (unhideAllApplications:), nsEmptyString());
[menu addItem: [NSMenuItem separatorItem]];
createMenuItem (menu, TRANS ("Quit") + String (" ") + appName, @selector (terminate:), nsStringLiteral ("q"));
}
// Since our app has no NIB, this initialises a standard app menu...
static void rebuildMainMenu (const PopupMenu* extraItems)
{
// this can't be used in a plugin!
jassert (JUCEApplicationBase::isStandaloneApp());
if (auto* app = JUCEApplicationBase::getInstance())
{
if (auto* mainMenu = JuceMainMenuBarHolder::getInstance()->mainMenuBar)
{
if ([mainMenu numberOfItems] > 0)
{
if (auto appMenu = [[mainMenu itemAtIndex: 0] submenu])
{
[appMenu removeAllItems];
MainMenuHelpers::createStandardAppMenu (appMenu, app->getApplicationName(), extraItems);
}
}
}
}
}
}
void MenuBarModel::setMacMainMenu (MenuBarModel* newMenuBarModel,
const PopupMenu* extraAppleMenuItems,
const String& recentItemsMenuName)
{
if (getMacMainMenu() != newMenuBarModel)
{
JUCE_AUTORELEASEPOOL
{
if (newMenuBarModel == nullptr)
{
delete JuceMainMenuHandler::instance;
jassert (JuceMainMenuHandler::instance == nullptr); // should be zeroed in the destructor
jassert (extraAppleMenuItems == nullptr); // you can't specify some extra items without also supplying a model
extraAppleMenuItems = nullptr;
}
else
{
if (JuceMainMenuHandler::instance == nullptr)
JuceMainMenuHandler::instance = new JuceMainMenuHandler();
JuceMainMenuHandler::instance->setMenu (newMenuBarModel, extraAppleMenuItems, recentItemsMenuName);
}
}
}
MainMenuHelpers::rebuildMainMenu (extraAppleMenuItems);
if (newMenuBarModel != nullptr)
newMenuBarModel->menuItemsChanged();
}
MenuBarModel* MenuBarModel::getMacMainMenu()
{
if (auto* mm = JuceMainMenuHandler::instance)
return mm->currentModel;
return nullptr;
}
const PopupMenu* MenuBarModel::getMacExtraAppleItemsMenu()
{
if (auto* mm = JuceMainMenuHandler::instance)
return mm->extraAppleMenuItems.get();
return nullptr;
}
using MenuTrackingChangedCallback = void (*)(bool);
extern MenuTrackingChangedCallback menuTrackingChangedCallback;
static void mainMenuTrackingChanged (bool isTracking)
{
PopupMenu::dismissAllActiveMenus();
if (auto* menuHandler = JuceMainMenuHandler::instance)
{
menuHandler->isOpen = isTracking;
if (auto* model = menuHandler->currentModel)
model->handleMenuBarActivate (isTracking);
if (menuHandler->defferedUpdateRequested && ! isTracking)
{
menuHandler->defferedUpdateRequested = false;
menuHandler->menuBarItemsChanged (menuHandler->currentModel);
}
}
}
static void initialiseMacMainMenu()
{
menuTrackingChangedCallback = mainMenuTrackingChanged;
if (JuceMainMenuHandler::instance == nullptr)
MainMenuHelpers::rebuildMainMenu (nullptr);
}
// (used from other modules that need to create an NSMenu)
NSMenu* createNSMenu (const PopupMenu&, const String&, int, int, bool);
NSMenu* createNSMenu (const PopupMenu& menu, const String& name, int topLevelMenuId, int topLevelIndex, bool addDelegate)
{
initialiseMacMainMenu();
if (auto* mm = JuceMainMenuHandler::instance)
return mm->createMenu (menu, name, topLevelMenuId, topLevelIndex, addDelegate);
jassertfalse; // calling this before making sure the OSX main menu stuff was initialised?
return nil;
}
} // namespace juce
@@ -0,0 +1,197 @@
/*
==============================================================================
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
{
#if JUCE_MAC
//==============================================================================
class MouseCursor::PlatformSpecificHandle
{
public:
PlatformSpecificHandle (const MouseCursor::StandardCursorType type)
: cursorHandle (createCursor (type)) {}
PlatformSpecificHandle (const detail::CustomMouseCursorInfo& info)
: cursorHandle (createCursor (info)) {}
~PlatformSpecificHandle()
{
[cursorHandle release];
}
static void showInWindow (PlatformSpecificHandle* handle, ComponentPeer*)
{
auto c = [&]
{
if (handle == nullptr || handle->cursorHandle == nullptr)
return [NSCursor arrowCursor];
return handle->cursorHandle;
}();
[c set];
}
private:
static NSCursor* fromNSImage (NSImage* im, NSPoint hotspot)
{
NSCursor* c = [[NSCursor alloc] initWithImage: im
hotSpot: hotspot];
[im release];
return c;
}
static NSCursor* fromHIServices (const char* filename)
{
JUCE_AUTORELEASEPOOL
{
auto cursorPath = String ("/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/"
"HIServices.framework/Versions/A/Resources/cursors/")
+ filename;
NSImage* originalImage = [[NSImage alloc] initByReferencingFile: juceStringToNS (cursorPath + "/cursor.pdf")];
NSSize originalSize = [originalImage size];
NSImage* resultImage = [[NSImage alloc] initWithSize: originalSize];
for (int scale = 1; scale <= 4; ++scale)
{
NSAffineTransform* scaleTransform = [NSAffineTransform transform];
[scaleTransform scaleBy: (float) scale];
if (CGImageRef rasterCGImage = [originalImage CGImageForProposedRect: nil
context: nil
hints: [NSDictionary dictionaryWithObjectsAndKeys:
NSImageHintCTM, scaleTransform, nil]])
{
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage: rasterCGImage];
[imageRep setSize: originalSize];
[resultImage addRepresentation: imageRep];
[imageRep release];
}
else
{
return nil;
}
}
[originalImage release];
NSDictionary* info = [NSDictionary dictionaryWithContentsOfFile: juceStringToNS (cursorPath + "/info.plist")];
auto hotspotX = (float) [[info valueForKey: nsStringLiteral ("hotx")] doubleValue];
auto hotspotY = (float) [[info valueForKey: nsStringLiteral ("hoty")] doubleValue];
return fromNSImage (resultImage, NSMakePoint (hotspotX, hotspotY));
}
}
static NSCursor* createCursor (const detail::CustomMouseCursorInfo& info)
{
return fromNSImage (imageToNSImage (info.image),
NSMakePoint (info.hotspot.x, info.hotspot.y));
}
static NSCursor* createCursor (const MouseCursor::StandardCursorType type)
{
JUCE_AUTORELEASEPOOL
{
NSCursor* c = nil;
switch (type)
{
case NormalCursor:
case ParentCursor: c = [NSCursor arrowCursor]; break;
case NoCursor: return createCursor ({ ScaledImage (Image (Image::ARGB, 8, 8, true)), {} });
case DraggingHandCursor: c = [NSCursor openHandCursor]; break;
case WaitCursor: c = [NSCursor arrowCursor]; break; // avoid this on the mac, let the OS provide the beachball
case IBeamCursor: c = [NSCursor IBeamCursor]; break;
case PointingHandCursor: c = [NSCursor pointingHandCursor]; break;
case LeftEdgeResizeCursor: c = [NSCursor resizeLeftCursor]; break;
case RightEdgeResizeCursor: c = [NSCursor resizeRightCursor]; break;
case CrosshairCursor: c = [NSCursor crosshairCursor]; break;
case CopyingCursor:
{
c = [NSCursor dragCopyCursor];
break;
}
case UpDownResizeCursor:
case TopEdgeResizeCursor:
case BottomEdgeResizeCursor:
if (NSCursor* m = fromHIServices ("resizenorthsouth"))
return m;
c = [NSCursor resizeUpDownCursor];
break;
case LeftRightResizeCursor:
if (NSCursor* m = fromHIServices ("resizeeastwest"))
return m;
c = [NSCursor resizeLeftRightCursor];
break;
case TopLeftCornerResizeCursor:
case BottomRightCornerResizeCursor:
return fromHIServices ("resizenorthwestsoutheast");
case TopRightCornerResizeCursor:
case BottomLeftCornerResizeCursor:
return fromHIServices ("resizenortheastsouthwest");
case UpDownLeftRightResizeCursor:
return fromHIServices ("move");
case NumStandardCursorTypes:
default:
jassertfalse;
break;
}
[c retain];
return c;
}
}
NSCursor* cursorHandle;
};
#else
class MouseCursor::PlatformSpecificHandle
{
public:
PlatformSpecificHandle (const MouseCursor::StandardCursorType) {}
PlatformSpecificHandle (const detail::CustomMouseCursorInfo&) {}
static void showInWindow (PlatformSpecificHandle*, ComponentPeer*) {}
};
#endif
} // namespace juce
@@ -0,0 +1,107 @@
/*
==============================================================================
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.
==============================================================================
*/
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-as-null-pointer-constant")
namespace juce
{
template <typename IDType>
class MultiTouchMapper
{
public:
MultiTouchMapper() {}
int getIndexOfTouch (ComponentPeer* peer, IDType touchID)
{
jassert (touchID != 0); // need to rethink this if IDs can be 0!
TouchInfo info {touchID, peer};
int touchIndex = currentTouches.indexOf (info);
if (touchIndex < 0)
{
auto emptyTouchIndex = currentTouches.indexOf ({});
touchIndex = (emptyTouchIndex >= 0 ? emptyTouchIndex : currentTouches.size());
currentTouches.set (touchIndex, info);
}
return touchIndex;
}
void clear()
{
currentTouches.clear();
}
void clearTouch (int index)
{
currentTouches.set (index, {});
}
bool areAnyTouchesActive() const noexcept
{
for (auto& t : currentTouches)
if (t.touchId != 0)
return true;
return false;
}
void deleteAllTouchesForPeer (ComponentPeer* peer)
{
for (auto& t : currentTouches)
if (t.owner == peer)
t.touchId = 0;
}
private:
//==============================================================================
struct TouchInfo
{
TouchInfo() noexcept : touchId (0), owner (nullptr) {}
TouchInfo (IDType idToUse, ComponentPeer* peer) noexcept : touchId (idToUse), owner (peer) {}
TouchInfo (const TouchInfo&) = default;
TouchInfo& operator= (const TouchInfo&) = default;
TouchInfo (TouchInfo&&) noexcept = default;
TouchInfo& operator= (TouchInfo&&) noexcept = default;
IDType touchId;
ComponentPeer* owner;
bool operator== (const TouchInfo& o) const noexcept { return (touchId == o.touchId); }
};
//==============================================================================
Array<TouchInfo> currentTouches;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiTouchMapper)
};
} // namespace juce
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
/*
==============================================================================
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::detail
{
std::unique_ptr<ScopedMessageBoxInterface> ScopedMessageBoxInterface::create (const MessageBoxOptions& options)
{
class AndroidMessageBox final : public ScopedMessageBoxInterface
{
public:
explicit AndroidMessageBox (const MessageBoxOptions& o) : opts (o) {}
void runAsync (std::function<void (int)> recipient) override
{
const auto makeDialogListener = [&recipient] (int result)
{
return new DialogListener ([recipient, result] { recipient (result); });
};
auto* env = getEnv();
LocalRef<jobject> builder (env->NewObject (AndroidAlertDialogBuilder, AndroidAlertDialogBuilder.construct, getMainActivity().get()));
const auto setText = [&] (auto method, const String& text)
{
builder = LocalRef<jobject> (env->CallObjectMethod (builder, method, javaString (text).get()));
};
setText (AndroidAlertDialogBuilder.setTitle, opts.getTitle());
setText (AndroidAlertDialogBuilder.setMessage, opts.getMessage());
builder = LocalRef<jobject> (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.setCancelable, true));
builder = LocalRef<jobject> (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.setOnCancelListener,
CreateJavaInterface (makeDialogListener (0),
"android/content/DialogInterface$OnCancelListener").get()));
const auto addButton = [&] (auto method, int index)
{
builder = LocalRef<jobject> (env->CallObjectMethod (builder,
method,
javaString (opts.getButtonText (index)).get(),
CreateJavaInterface (makeDialogListener (index),
"android/content/DialogInterface$OnClickListener").get()));
};
addButton (AndroidAlertDialogBuilder.setPositiveButton, 0);
if (opts.getButtonText (1).isNotEmpty())
addButton (AndroidAlertDialogBuilder.setNegativeButton, 1);
if (opts.getButtonText (2).isNotEmpty())
addButton (AndroidAlertDialogBuilder.setNeutralButton, 2);
dialog = GlobalRef (LocalRef<jobject> (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.create)));
LocalRef<jobject> window (env->CallObjectMethod (dialog, AndroidDialog.getWindow));
if (Desktop::getInstance().getKioskModeComponent() != nullptr)
{
env->CallVoidMethod (window, AndroidWindow.setFlags, FLAG_NOT_FOCUSABLE, FLAG_NOT_FOCUSABLE);
LocalRef<jobject> decorView (env->CallObjectMethod (window, AndroidWindow.getDecorView));
env->CallVoidMethod (decorView, AndroidView.setSystemUiVisibility, fullScreenFlags);
}
env->CallVoidMethod (dialog, AndroidDialog.show);
if (Desktop::getInstance().getKioskModeComponent() != nullptr)
env->CallVoidMethod (window, AndroidWindow.clearFlags, FLAG_NOT_FOCUSABLE);
}
int runSync() override
{
// Not implemented on this platform.
jassertfalse;
return 0;
}
void close() override
{
if (dialog != nullptr)
getEnv()->CallVoidMethod (dialog, AndroidDialogInterface.dismiss);
}
private:
const MessageBoxOptions opts;
GlobalRef dialog;
};
return std::make_unique<AndroidMessageBox> (options);
}
} // namespace juce::detail
@@ -0,0 +1,110 @@
/*
==============================================================================
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::detail
{
std::unique_ptr<ScopedMessageBoxInterface> ScopedMessageBoxInterface::create (const MessageBoxOptions& options)
{
class MessageBox final : public ScopedMessageBoxInterface
{
public:
explicit MessageBox (const MessageBoxOptions& opts) : options (opts) {}
void runAsync (std::function<void (int)> recipient) override
{
if (iOSGlobals::currentlyFocusedPeer == nullptr)
{
// Since iOS8, alert windows need to be associated with a window, so you need to
// have at least one window on screen when you use this
jassertfalse;
return;
}
alert.reset ([[UIAlertController alertControllerWithTitle: juceStringToNS (options.getTitle())
message: juceStringToNS (options.getMessage())
preferredStyle: UIAlertControllerStyleAlert] retain]);
for (auto i = 0; i < options.getNumButtons(); ++i)
{
const auto text = options.getButtonText (i);
if (text.isEmpty())
continue;
auto* action = [UIAlertAction actionWithTitle: juceStringToNS (text)
style: UIAlertActionStyleDefault
handler: ^(UIAlertAction*)
{
MessageManager::callAsync ([recipient, i] { NullCheckedInvocation::invoke (recipient, i); });
}];
[alert.get() addAction: action];
if (i == 0)
[alert.get() setPreferredAction: action];
}
[iOSGlobals::currentlyFocusedPeer->controller presentViewController: alert.get()
animated: YES
completion: nil];
}
int runSync() override
{
int result = -1;
JUCE_AUTORELEASEPOOL
{
runAsync ([&result] (int r) { result = r; });
while (result < 0)
{
JUCE_AUTORELEASEPOOL
{
[[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]];
}
}
}
return result;
}
void close() override
{
if (auto* alertViewController = alert.get())
[alertViewController dismissViewControllerAnimated: YES completion: nil];
}
private:
const MessageBoxOptions options;
NSUniquePtr<UIAlertController> alert;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageBox)
};
return std::make_unique<MessageBox> (options);
}
} // namespace juce::detail
@@ -0,0 +1,69 @@
/*
==============================================================================
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::detail
{
std::unique_ptr<ScopedMessageBoxInterface> ScopedMessageBoxInterface::create (const MessageBoxOptions& options)
{
// On Linux, we re-use the AlertWindow rather than using a platform-specific dialog.
// For consistency with the NativeMessageBox on other platforms, the result code must
// match the button index, hence this adapter.
class MessageBox final : public ScopedMessageBoxInterface
{
public:
explicit MessageBox (const MessageBoxOptions& options)
: inner (detail::AlertWindowHelpers::create (options)),
numButtons (options.getNumButtons()) {}
void runAsync (std::function<void (int)> fn) override
{
inner->runAsync ([fn, n = numButtons] (int result)
{
fn (map (result, n));
});
}
int runSync() override
{
return map (inner->runSync(), numButtons);
}
void close() override
{
inner->close();
}
private:
static int map (int button, int numButtons) { return (button + numButtons - 1) % numButtons; }
std::unique_ptr<ScopedMessageBoxInterface> inner;
int numButtons = 0;
};
return std::make_unique<MessageBox> (options);
}
} // namespace juce::detail
@@ -0,0 +1,134 @@
/*
==============================================================================
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::detail
{
std::unique_ptr<ScopedMessageBoxInterface> ScopedMessageBoxInterface::create (const MessageBoxOptions& options)
{
class OSXMessageBox final : public ScopedMessageBoxInterface
{
public:
explicit OSXMessageBox (const MessageBoxOptions& opts)
: options (opts) {}
void runAsync (std::function<void (int)> recipient) override
{
makeAlert();
const auto onDone = [recipient] (NSModalResponse result)
{
recipient (convertResult (result));
};
if (auto* comp = options.getAssociatedComponent())
{
if (auto* peer = comp->getPeer())
{
if (auto* view = static_cast<NSView*> (peer->getNativeHandle()))
{
if (auto* window = [view window])
{
if (@available (macOS 10.9, *))
{
[alertWindow.get() beginSheetModalForWindow: window completionHandler: ^(NSModalResponse result)
{
onDone (result);
}];
return;
}
}
}
}
}
const auto result = [alertWindow.get() runModal];
onDone (result);
}
int runSync() override
{
makeAlert();
return convertResult ([alertWindow.get() runModal]);
}
void close() override
{
if (auto* alert = alertWindow.get())
[[alert window] close];
}
private:
static int convertResult (NSModalResponse response)
{
switch (response)
{
case NSAlertFirstButtonReturn: return 0;
case NSAlertSecondButtonReturn: return 1;
case NSAlertThirdButtonReturn: return 2;
default: break;
}
jassertfalse;
return 0;
}
static void addButton (NSAlert* alert, const String& button)
{
if (! button.isEmpty())
[alert addButtonWithTitle: juceStringToNS (button)];
}
void makeAlert()
{
NSAlert* alert = [[NSAlert alloc] init];
[alert setMessageText: juceStringToNS (options.getTitle())];
[alert setInformativeText: juceStringToNS (options.getMessage())];
[alert setAlertStyle: options.getIconType() == MessageBoxIconType::WarningIcon ? NSAlertStyleCritical
: NSAlertStyleInformational];
const auto button1Text = options.getButtonText (0);
addButton (alert, button1Text.isEmpty() ? "OK" : button1Text);
addButton (alert, options.getButtonText (1));
addButton (alert, options.getButtonText (2));
alertWindow.reset (alert);
}
NSUniquePtr<NSAlert> alertWindow;
MessageBoxOptions options;
std::unique_ptr<ModalComponentManager::Callback> callback;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSXMessageBox)
};
return std::make_unique<OSXMessageBox> (options);
}
} // namespace juce::detail
@@ -0,0 +1,348 @@
/*
==============================================================================
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::detail
{
#if JUCE_MSVC
// required to enable the newer dialog box on vista and above
#pragma comment(linker, \
"\"/MANIFESTDEPENDENCY:type='Win32' " \
"name='Microsoft.Windows.Common-Controls' " \
"version='6.0.0.0' " \
"processorArchitecture='*' " \
"publicKeyToken='6595b64144ccf1df' " \
"language='*'\"" \
)
#endif
std::unique_ptr<ScopedMessageBoxInterface> ScopedMessageBoxInterface::create (const MessageBoxOptions& options)
{
class WindowsMessageBoxBase : public ScopedMessageBoxInterface
{
public:
explicit WindowsMessageBoxBase (Component* comp)
: associatedComponent (comp) {}
void runAsync (std::function<void (int)> recipient) override
{
future = std::async (std::launch::async, [showMessageBox = getShowMessageBox(), recipient]
{
const auto initComResult = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (initComResult != S_OK)
return;
const ScopeGuard scope { [] { CoUninitialize(); } };
const auto messageResult = showMessageBox != nullptr ? showMessageBox() : 0;
NullCheckedInvocation::invoke (recipient, messageResult);
});
}
int runSync() override
{
if (auto showMessageBox = getShowMessageBox())
return showMessageBox();
return 0;
}
void close() override
{
if (auto* toClose = windowHandle.exchange (nullptr))
EndDialog (toClose, 0);
}
void setDialogWindowHandle (HWND dialogHandle)
{
windowHandle = dialogHandle;
}
private:
std::function<int()> getShowMessageBox()
{
const auto parent = associatedComponent != nullptr ? (HWND) associatedComponent->getWindowHandle() : nullptr;
return getShowMessageBoxForParent (parent);
}
/* Returns a function that should display a message box and return the result.
getShowMessageBoxForParent() will be called on the message thread.
The returned function will be called on a separate thread, in order to avoid blocking the
message thread.
'this' is guaranteed to be alive when the returned function is called.
*/
virtual std::function<int()> getShowMessageBoxForParent (HWND parent) = 0;
Component::SafePointer<Component> associatedComponent;
std::atomic<HWND> windowHandle { nullptr };
std::future<void> future;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsMessageBoxBase)
};
class PreVistaMessageBox final : public WindowsMessageBoxBase
{
public:
PreVistaMessageBox (const MessageBoxOptions& opts, UINT extraFlags)
: WindowsMessageBoxBase (opts.getAssociatedComponent()),
flags (extraFlags | getMessageBoxFlags (opts.getIconType())),
title (opts.getTitle()), message (opts.getMessage()) {}
private:
std::function<int()> getShowMessageBoxForParent (const HWND parent) override
{
JUCE_ASSERT_MESSAGE_THREAD
static std::map<DWORD, PreVistaMessageBox*> map;
static std::mutex mapMutex;
return [this, parent]
{
const auto threadId = GetCurrentThreadId();
{
const std::scoped_lock scope { mapMutex };
map.emplace (threadId, this);
}
const ScopeGuard eraseFromMap { [threadId]
{
const std::scoped_lock scope { mapMutex };
map.erase (threadId);
} };
const auto hookCallback = [] (int nCode, const WPARAM wParam, const LPARAM lParam)
{
auto* params = reinterpret_cast<CWPSTRUCT*> (lParam);
if (nCode >= 0
&& params != nullptr
&& (params->message == WM_INITDIALOG || params->message == WM_DESTROY))
{
const auto callbackThreadId = GetCurrentThreadId();
const std::scoped_lock scope { mapMutex };
if (const auto iter = map.find (callbackThreadId); iter != map.cend())
iter->second->setDialogWindowHandle (params->message == WM_INITDIALOG ? params->hwnd : nullptr);
}
return CallNextHookEx ({}, nCode, wParam, lParam);
};
const auto hook = SetWindowsHookEx (WH_CALLWNDPROC,
hookCallback,
(HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(),
threadId);
const ScopeGuard removeHook { [hook] { UnhookWindowsHookEx (hook); } };
const auto result = MessageBox (parent, message.toWideCharPointer(), title.toWideCharPointer(), flags);
if (result == IDYES || result == IDOK) return 0;
if (result == IDNO && ((flags & 1) != 0)) return 1;
return 2;
};
}
static UINT getMessageBoxFlags (MessageBoxIconType iconType) noexcept
{
// this window can get lost behind JUCE windows which are set to be alwaysOnTop
// so if there are any set it to be topmost
const auto topmostFlag = WindowUtils::areThereAnyAlwaysOnTopWindows() ? MB_TOPMOST : 0;
const auto iconFlags = [&]() -> decltype (topmostFlag)
{
switch (iconType)
{
case MessageBoxIconType::QuestionIcon: return MB_ICONQUESTION;
case MessageBoxIconType::WarningIcon: return MB_ICONWARNING;
case MessageBoxIconType::InfoIcon: return MB_ICONINFORMATION;
case MessageBoxIconType::NoIcon: break;
}
return 0;
}();
return static_cast<UINT> (MB_TASKMODAL | MB_SETFOREGROUND | topmostFlag | iconFlags);
}
const UINT flags;
const String title, message;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PreVistaMessageBox)
};
class WindowsTaskDialog final : public WindowsMessageBoxBase
{
static auto getTaskDialogFunc()
{
using TaskDialogIndirectFunc = HRESULT (WINAPI*) (const TASKDIALOGCONFIG*, INT*, INT*, BOOL*);
static const auto result = [&]() -> TaskDialogIndirectFunc
{
if (SystemStats::getOperatingSystemType() < SystemStats::WinVista)
return nullptr;
const auto comctl = "Comctl32.dll";
LoadLibraryA (comctl);
const auto comctlModule = GetModuleHandleA (comctl);
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wcast-function-type")
if (comctlModule != nullptr)
return (TaskDialogIndirectFunc) GetProcAddress (comctlModule, "TaskDialogIndirect");
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
return nullptr;
}();
return result;
}
public:
explicit WindowsTaskDialog (const MessageBoxOptions& opts)
: WindowsMessageBoxBase (opts.getAssociatedComponent()),
iconType (opts.getIconType()),
title (opts.getTitle()), message (opts.getMessage()),
buttons { opts.getButtonText (0), opts.getButtonText (1), opts.getButtonText (2) } {}
static bool isAvailable()
{
return getTaskDialogFunc() != nullptr;
}
private:
std::function<int()> getShowMessageBoxForParent (const HWND parent) override
{
JUCE_ASSERT_MESSAGE_THREAD
return [this, parent]
{
TASKDIALOGCONFIG config{};
config.cbSize = sizeof (config);
config.hwndParent = parent;
config.pszWindowTitle = title.toWideCharPointer();
config.pszContent = message.toWideCharPointer();
config.hInstance = (HINSTANCE) Process::getCurrentModuleInstanceHandle();
config.lpCallbackData = reinterpret_cast<LONG_PTR> (this);
config.pfCallback = [] (HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR lpRefData)
{
if (auto* t = reinterpret_cast<WindowsTaskDialog*> (lpRefData))
{
switch (msg)
{
case TDN_CREATED:
case TDN_DIALOG_CONSTRUCTED:
t->setDialogWindowHandle (hwnd);
break;
case TDN_DESTROYED:
t->setDialogWindowHandle (nullptr);
break;
}
}
return S_OK;
};
if (iconType == MessageBoxIconType::QuestionIcon)
{
if (auto* questionIcon = LoadIcon (nullptr, IDI_QUESTION))
{
config.hMainIcon = questionIcon;
config.dwFlags |= TDF_USE_HICON_MAIN;
}
}
else
{
config.pszMainIcon = [&]() -> LPWSTR
{
switch (iconType)
{
case MessageBoxIconType::WarningIcon: return TD_WARNING_ICON;
case MessageBoxIconType::InfoIcon: return TD_INFORMATION_ICON;
case MessageBoxIconType::QuestionIcon: JUCE_FALLTHROUGH
case MessageBoxIconType::NoIcon:
break;
}
return nullptr;
}();
}
std::vector<TASKDIALOG_BUTTON> buttonLabels;
for (const auto& buttonText : buttons)
if (buttonText.isNotEmpty())
buttonLabels.push_back ({ (int) buttonLabels.size(), buttonText.toWideCharPointer() });
config.pButtons = buttonLabels.data();
config.cButtons = (UINT) buttonLabels.size();
int buttonIndex = 0;
if (auto* func = getTaskDialogFunc())
func (&config, &buttonIndex, nullptr, nullptr);
else
jassertfalse;
return buttonIndex;
};
}
const MessageBoxIconType iconType;
const String title, message;
const std::array<String, 3> buttons;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsTaskDialog)
};
if (WindowsTaskDialog::isAvailable())
return std::make_unique<WindowsTaskDialog> (options);
const auto extraFlags = [&options]
{
const auto numButtons = options.getNumButtons();
if (numButtons == 3)
return MB_YESNOCANCEL;
if (numButtons == 2)
return options.getButtonText (0) == "OK" ? MB_OKCANCEL
: MB_YESNO;
return MB_OK;
}();
return std::make_unique<PreVistaMessageBox> (options, (UINT) extraFlags);
}
} // namespace juce::detail
@@ -0,0 +1,132 @@
/*
==============================================================================
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::detail
{
/**
Sets up a native control to be hosted on top of a JUCE component.
*/
class NativeModalWrapperComponent : public Component
{
public:
void parentHierarchyChanged() final
{
auto* newPeer = dynamic_cast<UIViewComponentPeer*> (getPeer());
if (std::exchange (peer, newPeer) == newPeer)
return;
if (peer == nullptr)
return;
if (isIPad())
{
getViewController().preferredContentSize = peer->view.frame.size;
if (auto* popoverController = getViewController().popoverPresentationController)
{
popoverController.sourceView = peer->view;
popoverController.sourceRect = CGRectMake (0.0f, (float) getHeight() - 10.0f, (float) getWidth(), 10.0f);
popoverController.canOverlapSourceViewRect = YES;
popoverController.delegate = popoverDelegate.get();
}
}
if (auto* parentController = peer->controller)
[parentController showViewController: getViewController() sender: parentController];
peer->toFront (false);
}
void displayNativeWindowModally (Component* parent)
{
setOpaque (false);
if (parent != nullptr)
{
[getViewController() setModalPresentationStyle: UIModalPresentationPageSheet];
setBounds (parent->getLocalBounds());
setAlwaysOnTop (true);
parent->addAndMakeVisible (this);
}
else
{
if (SystemStats::isRunningInAppExtensionSandbox())
{
// Opening a native top-level window in an AUv3 is not allowed (sandboxing). You need to specify a
// parent component (for example your editor) to parent the native file chooser window. To do this
// specify a parent component in the FileChooser's constructor!
jassertfalse;
return;
}
auto chooserBounds = Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
setBounds (chooserBounds);
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (0);
}
}
private:
virtual UIViewController* getViewController() const = 0;
static bool isIPad()
{
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad;
}
struct PopoverDelegateClass : public ObjCClass<NSObject<UIPopoverPresentationControllerDelegate>>
{
PopoverDelegateClass()
: ObjCClass ("PopoverDelegateClass_")
{
addMethod (@selector (popoverPresentationController:willRepositionPopoverToRect:inView:), [] (id, SEL, UIPopoverPresentationController*, CGRect* rect, UIView*)
{
auto screenBounds = [UIScreen mainScreen].bounds;
rect->origin.x = 0.f;
rect->origin.y = screenBounds.size.height - 10.f;
rect->size.width = screenBounds.size.width;
rect->size.height = 10.f;
});
registerClass();
}
};
UIViewComponentPeer* peer = nullptr;
NSUniquePtr<NSObject<UIPopoverPresentationControllerDelegate>> popoverDelegate { []
{
static PopoverDelegateClass cls;
return cls.createInstance();
}() };
};
} // namespace juce::detail
@@ -0,0 +1,304 @@
/*
==============================================================================
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
{
//==============================================================================
/*
Forwards NSNotificationCenter callbacks to a std::function<void()>.
*/
class FunctionNotificationCenterObserver
{
public:
FunctionNotificationCenterObserver (NSNotificationName notificationName,
id objectToObserve,
std::function<void()> callback)
: onNotification (std::move (callback)),
observer (observerObject.get(), getSelector(), notificationName, objectToObserve)
{}
private:
struct ObserverClass
{
ObserverClass()
{
klass.addIvar<FunctionNotificationCenterObserver*> ("owner");
klass.addMethod (getSelector(), [] (id self, SEL, NSNotification*)
{
getIvar<FunctionNotificationCenterObserver*> (self, "owner")->onNotification();
});
klass.registerClass();
}
NSObject* createInstance() const { return klass.createInstance(); }
private:
ObjCClass<NSObject> klass { "JUCEObserverClass_" };
};
static SEL getSelector()
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
return @selector (notificationFired:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
std::function<void()> onNotification;
NSUniquePtr<NSObject> observerObject
{
[this]
{
static ObserverClass observerClass;
auto* result = observerClass.createInstance();
object_setInstanceVariable (result, "owner", this);
return result;
}()
};
ScopedNotificationCenterObserver observer;
// Instances can't be copied or moved, because 'this' is stored as a member of the ObserverClass
// object.
JUCE_DECLARE_NON_COPYABLE (FunctionNotificationCenterObserver)
JUCE_DECLARE_NON_MOVEABLE (FunctionNotificationCenterObserver)
};
//==============================================================================
/*
Manages the lifetime of a CVDisplayLinkRef for a single display, and automatically starts and
stops it.
*/
class ScopedDisplayLink
{
public:
static CGDirectDisplayID getDisplayIdForScreen (NSScreen* screen)
{
return (CGDirectDisplayID) [[screen.deviceDescription objectForKey: @"NSScreenNumber"] unsignedIntegerValue];
}
ScopedDisplayLink (NSScreen* screenIn, std::function<void()> onCallbackIn)
: displayId (getDisplayIdForScreen (screenIn)),
link ([display = displayId]
{
CVDisplayLinkRef ptr = nullptr;
[[maybe_unused]] const auto result = CVDisplayLinkCreateWithCGDisplay (display, &ptr);
jassert (result == kCVReturnSuccess);
jassert (ptr != nullptr);
return ptr;
}()),
onCallback (std::move (onCallbackIn))
{
const auto callback = [] (CVDisplayLinkRef,
const CVTimeStamp*,
const CVTimeStamp*,
CVOptionFlags,
CVOptionFlags*,
void* context) -> int
{
static_cast<const ScopedDisplayLink*> (context)->onCallback();
return kCVReturnSuccess;
};
[[maybe_unused]] const auto callbackResult = CVDisplayLinkSetOutputCallback (link.get(), callback, this);
jassert (callbackResult == kCVReturnSuccess);
[[maybe_unused]] const auto startResult = CVDisplayLinkStart (link.get());
jassert (startResult == kCVReturnSuccess);
}
~ScopedDisplayLink() noexcept
{
if (link != nullptr)
CVDisplayLinkStop (link.get());
}
CGDirectDisplayID getDisplayId() const { return displayId; }
double getNominalVideoRefreshPeriodS() const
{
const auto nominalVideoRefreshPeriod = CVDisplayLinkGetNominalOutputVideoRefreshPeriod (link.get());
if ((nominalVideoRefreshPeriod.flags & kCVTimeIsIndefinite) == 0)
return (double) nominalVideoRefreshPeriod.timeValue / (double) nominalVideoRefreshPeriod.timeScale;
return 0.0;
}
private:
struct DisplayLinkDestructor
{
void operator() (CVDisplayLinkRef ptr) const
{
if (ptr != nullptr)
CVDisplayLinkRelease (ptr);
}
};
CGDirectDisplayID displayId;
std::unique_ptr<std::remove_pointer_t<CVDisplayLinkRef>, DisplayLinkDestructor> link;
std::function<void()> onCallback;
// Instances can't be copied or moved, because 'this' is passed as context to
// CVDisplayLinkSetOutputCallback
JUCE_DECLARE_NON_COPYABLE (ScopedDisplayLink)
JUCE_DECLARE_NON_MOVEABLE (ScopedDisplayLink)
};
//==============================================================================
/*
Holds a ScopedDisplayLink for each screen. When the screen configuration changes, the
ScopedDisplayLinks will be recreated automatically to match the new configuration.
*/
class PerScreenDisplayLinks
{
public:
PerScreenDisplayLinks()
{
refreshScreens();
}
using RefreshCallback = std::function<void()>;
using Factory = std::function<RefreshCallback (CGDirectDisplayID)>;
/*
Automatically unregisters a CVDisplayLink callback factory when ~Connection() is called.
*/
class Connection
{
public:
Connection() = default;
Connection (PerScreenDisplayLinks& linksIn, std::list<Factory>::const_iterator it)
: links (&linksIn), iter (it) {}
~Connection() noexcept
{
if (links != nullptr)
links->unregisterFactory (iter);
}
Connection (const Connection&) = delete;
Connection& operator= (const Connection&) = delete;
Connection (Connection&& other) noexcept
: links (std::exchange (other.links, nullptr)), iter (other.iter) {}
Connection& operator= (Connection&& other) noexcept
{
Connection { std::move (other) }.swap (*this);
return *this;
}
private:
void swap (Connection& other) noexcept
{
std::swap (other.links, links);
std::swap (other.iter, iter);
}
PerScreenDisplayLinks* links = nullptr;
std::list<Factory>::const_iterator iter;
};
/* Stores the provided factory for as long as the returned Connection remains alive.
Whenever the screen configuration changes, the factory function will be called for each
screen. The RefreshCallback returned by the factory will be called every time that screen's
display link callback fires.
*/
[[nodiscard]] Connection registerFactory (Factory factory)
{
const ScopedLock lock (mutex);
factories.push_front (std::move (factory));
refreshScreens();
return { *this, factories.begin() };
}
double getNominalVideoRefreshPeriodSForScreen (CGDirectDisplayID display) const
{
const ScopedLock lock (mutex);
for (const auto& link : links)
if (link.getDisplayId() == display)
return link.getNominalVideoRefreshPeriodS();
return 0.0;
}
private:
void unregisterFactory (std::list<Factory>::const_iterator iter)
{
const ScopedLock lock (mutex);
factories.erase (iter);
refreshScreens();
}
void refreshScreens()
{
auto newLinks = [&]
{
std::list<ScopedDisplayLink> result;
for (NSScreen* screen in [NSScreen screens])
{
std::vector<RefreshCallback> callbacks;
for (auto& factory : factories)
callbacks.push_back (factory (ScopedDisplayLink::getDisplayIdForScreen (screen)));
// This is the callback that will actually fire in response to this screen's display
// link callback.
result.emplace_back (screen, [cbs = std::move (callbacks)]
{
for (const auto& callback : cbs)
callback();
});
}
return result;
}();
const ScopedLock lock (mutex);
links = std::move (newLinks);
}
CriticalSection mutex;
// This is a list rather than a vector so that the iterators are stable, even when items are
// added/removed from the list. This is important because Connection objects store an iterator
// internally, and may be created/destroyed arbitrarily.
std::list<Factory> factories;
// This is a list rather than a vector because ScopedDisplayLink is non-moveable.
std::list<ScopedDisplayLink> links;
FunctionNotificationCenterObserver screenParamsObserver { NSApplicationDidChangeScreenParametersNotification,
nullptr,
[this] { refreshScreens(); } };
};
} // namespace juce
@@ -0,0 +1,34 @@
/*
==============================================================================
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
{
#if ! JUCE_WINDOWS
ScopedDPIAwarenessDisabler::ScopedDPIAwarenessDisabler() { ignoreUnused (previousContext); }
ScopedDPIAwarenessDisabler::~ScopedDPIAwarenessDisabler() {}
#endif
} // namespace juce
@@ -0,0 +1,54 @@
/*
==============================================================================
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
{
//==============================================================================
/**
A Windows-specific class that temporarily sets the DPI awareness context of
the current thread to be DPI unaware and resets it to the previous context
when it goes out of scope.
If you create one of these before creating a top-level window, the window
will be DPI unaware and bitmap stretched by the OS on a display with >100%
scaling.
You shouldn't use this unless you really know what you are doing and
are dealing with native HWNDs.
@tags{GUI}
*/
class JUCE_API ScopedDPIAwarenessDisabler
{
public:
ScopedDPIAwarenessDisabler();
~ScopedDPIAwarenessDisabler();
private:
void* previousContext = nullptr;
};
} // namespace juce
@@ -0,0 +1,43 @@
/*
==============================================================================
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
{
//==============================================================================
class ScopedThreadDPIAwarenessSetter
{
public:
explicit ScopedThreadDPIAwarenessSetter (void* nativeWindow);
~ScopedThreadDPIAwarenessSetter();
private:
class NativeImpl;
std::unique_ptr<NativeImpl> pimpl;
JUCE_LEAK_DETECTOR (ScopedThreadDPIAwarenessSetter)
};
} // namespace juce
@@ -0,0 +1,118 @@
/*
==============================================================================
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
{
extern XContext windowHandleXContext;
/* Attaches a pointer to a given window, so that it can be retrieved with XFindContext on
the windowHandleXContext.
*/
class ScopedWindowAssociation
{
public:
ScopedWindowAssociation() = default;
ScopedWindowAssociation (void* associatedIn, Window windowIn)
: associatedPointer ([&]() -> void*
{
if (associatedIn == nullptr)
return nullptr;
// If you hit this, there's already a pointer associated with this window.
const auto display = XWindowSystem::getInstance()->getDisplay();
jassert (! getAssociatedPointer (display, windowIn).has_value());
if (X11Symbols::getInstance()->xSaveContext (display,
static_cast<XID> (windowIn),
windowHandleXContext,
unalignedPointerCast<XPointer> (associatedIn)) != 0)
{
jassertfalse;
return nullptr;
}
return associatedIn;
}()),
window (static_cast<XID> (windowIn)) {}
ScopedWindowAssociation (const ScopedWindowAssociation&) = delete;
ScopedWindowAssociation& operator= (const ScopedWindowAssociation&) = delete;
ScopedWindowAssociation (ScopedWindowAssociation&& other) noexcept
: associatedPointer (std::exchange (other.associatedPointer, nullptr)), window (other.window) {}
ScopedWindowAssociation& operator= (ScopedWindowAssociation&& other) noexcept
{
ScopedWindowAssociation { std::move (other) }.swap (*this);
return *this;
}
~ScopedWindowAssociation() noexcept
{
if (associatedPointer == nullptr)
return;
const auto display = XWindowSystem::getInstance()->getDisplay();
const auto ptr = getAssociatedPointer (display, window);
if (! ptr.has_value())
{
// If you hit this, something else has cleared this association before we were able to.
jassertfalse;
return;
}
jassert (unalignedPointerCast<XPointer> (associatedPointer) == *ptr);
if (X11Symbols::getInstance()->xDeleteContext (display, window, windowHandleXContext) != 0)
jassertfalse;
}
bool isValid() const { return associatedPointer != nullptr; }
private:
static std::optional<XPointer> getAssociatedPointer (Display* display, Window window)
{
XPointer ptr{};
if (X11Symbols::getInstance()->xFindContext (display, window, windowHandleXContext, &ptr) != 0)
return std::nullopt;
return ptr;
}
void swap (ScopedWindowAssociation& other) noexcept
{
std::swap (other.associatedPointer, associatedPointer);
std::swap (other.window, window);
}
void* associatedPointer = nullptr;
XID window{};
};
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,34 @@
/*
==============================================================================
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 WindowUtils::areThereAnyAlwaysOnTopWindows()
{
return false;
}
} // namespace juce
@@ -0,0 +1,34 @@
/*
==============================================================================
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 WindowUtils::areThereAnyAlwaysOnTopWindows()
{
return false;
}
} // namespace juce
@@ -0,0 +1,39 @@
/*
==============================================================================
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
{
struct WindowUtilsInternal
{
inline static int numAlwaysOnTopPeers = 0;
};
bool WindowUtils::areThereAnyAlwaysOnTopWindows()
{
return WindowUtilsInternal::numAlwaysOnTopPeers > 0;
}
} // namespace juce
@@ -0,0 +1,38 @@
/*
==============================================================================
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 WindowUtils::areThereAnyAlwaysOnTopWindows()
{
for (NSWindow* window in [NSApp windows])
if ([window level] > NSNormalWindowLevel)
return true;
return false;
}
} // namespace juce
@@ -0,0 +1,59 @@
/*
==============================================================================
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
{
static BOOL CALLBACK enumAlwaysOnTopWindows (HWND hwnd, LPARAM lParam)
{
if (IsWindowVisible (hwnd))
{
DWORD processID = 0;
GetWindowThreadProcessId (hwnd, &processID);
if (processID == GetCurrentProcessId())
{
WINDOWINFO info{};
if (GetWindowInfo (hwnd, &info)
&& (info.dwExStyle & WS_EX_TOPMOST) != 0)
{
*reinterpret_cast<bool*> (lParam) = true;
return FALSE;
}
}
}
return TRUE;
}
bool WindowUtils::areThereAnyAlwaysOnTopWindows()
{
bool anyAlwaysOnTopFound = false;
EnumWindows (&enumAlwaysOnTopWindows, (LPARAM) &anyAlwaysOnTopFound);
return anyAlwaysOnTopFound;
}
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,741 @@
/*
==============================================================================
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
{
extern bool isIOSAppActive;
struct AppInactivityCallback // NB: careful, this declaration is duplicated in other modules
{
virtual ~AppInactivityCallback() = default;
virtual void appBecomingInactive() = 0;
};
// This is an internal list of callbacks (but currently used between modules)
Array<AppInactivityCallback*> appBecomingInactiveCallbacks;
} // namespace juce
#if JUCE_PUSH_NOTIFICATIONS
@interface JuceAppStartupDelegate : NSObject <UIApplicationDelegate, UNUserNotificationCenterDelegate>
#else
@interface JuceAppStartupDelegate : NSObject <UIApplicationDelegate>
#endif
{
UIBackgroundTaskIdentifier appSuspendTask;
}
@property (strong, nonatomic) UIWindow *window;
- (id) init;
- (void) dealloc;
- (void) applicationDidFinishLaunching: (UIApplication*) application;
- (void) applicationWillTerminate: (UIApplication*) application;
- (void) applicationDidEnterBackground: (UIApplication*) application;
- (void) applicationWillEnterForeground: (UIApplication*) application;
- (void) applicationDidBecomeActive: (UIApplication*) application;
- (void) applicationWillResignActive: (UIApplication*) application;
- (void) application: (UIApplication*) application handleEventsForBackgroundURLSession: (NSString*) identifier
completionHandler: (void (^)(void)) completionHandler;
- (void) applicationDidReceiveMemoryWarning: (UIApplication *) application;
#if JUCE_PUSH_NOTIFICATIONS
- (void) application: (UIApplication*) application
didRegisterForRemoteNotificationsWithDeviceToken: (NSData*) deviceToken;
- (void) application: (UIApplication*) application
didFailToRegisterForRemoteNotificationsWithError: (NSError*) error;
- (void) application: (UIApplication*) application
didReceiveRemoteNotification: (NSDictionary*) userInfo;
- (void) application: (UIApplication*) application
didReceiveRemoteNotification: (NSDictionary*) userInfo
fetchCompletionHandler: (void (^)(UIBackgroundFetchResult result)) completionHandler;
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forRemoteNotification: (NSDictionary*) userInfo
withResponseInfo: (NSDictionary*) responseInfo
completionHandler: (void(^)()) completionHandler;
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
- (void) application: (UIApplication*) application
didRegisterUserNotificationSettings: (UIUserNotificationSettings*) notificationSettings;
- (void) application: (UIApplication*) application
didReceiveLocalNotification: (UILocalNotification*) notification;
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forLocalNotification: (UILocalNotification*) notification
completionHandler: (void(^)()) completionHandler;
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forLocalNotification: (UILocalNotification*) notification
withResponseInfo: (NSDictionary*) responseInfo
completionHandler: (void(^)()) completionHandler;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
- (void) userNotificationCenter: (UNUserNotificationCenter*) center
willPresentNotification: (UNNotification*) notification
withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler;
- (void) userNotificationCenter: (UNUserNotificationCenter*) center
didReceiveNotificationResponse: (UNNotificationResponse*) response
withCompletionHandler: (void(^)())completionHandler;
#endif
@end
@implementation JuceAppStartupDelegate
NSObject* _pushNotificationsDelegate;
- (id) init
{
self = [super init];
appSuspendTask = UIBackgroundTaskInvalid;
#if JUCE_PUSH_NOTIFICATIONS
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
#endif
return self;
}
- (void) dealloc
{
[super dealloc];
}
- (void) applicationDidFinishLaunching: (UIApplication*) application
{
ignoreUnused (application);
initialiseJuce_GUI();
if (auto* app = JUCEApplicationBase::createInstance())
{
if (! app->initialiseApp())
exit (app->shutdownApp());
}
else
{
jassertfalse; // you must supply an application object for an iOS app!
}
}
- (void) applicationWillTerminate: (UIApplication*) application
{
ignoreUnused (application);
JUCEApplicationBase::appWillTerminateByForce();
}
- (void) applicationDidEnterBackground: (UIApplication*) application
{
if (auto* app = JUCEApplicationBase::getInstance())
{
#if JUCE_EXECUTE_APP_SUSPEND_ON_BACKGROUND_TASK
appSuspendTask = [application beginBackgroundTaskWithName:@"JUCE Suspend Task" expirationHandler:^{
if (appSuspendTask != UIBackgroundTaskInvalid)
{
[application endBackgroundTask:appSuspendTask];
appSuspendTask = UIBackgroundTaskInvalid;
}
}];
MessageManager::callAsync ([app] { app->suspended(); });
#else
ignoreUnused (application);
app->suspended();
#endif
}
}
- (void) applicationWillEnterForeground: (UIApplication*) application
{
ignoreUnused (application);
if (auto* app = JUCEApplicationBase::getInstance())
app->resumed();
}
- (void) applicationDidBecomeActive: (UIApplication*) application
{
application.applicationIconBadgeNumber = 0;
isIOSAppActive = true;
}
- (void) applicationWillResignActive: (UIApplication*) application
{
ignoreUnused (application);
isIOSAppActive = false;
for (int i = appBecomingInactiveCallbacks.size(); --i >= 0;)
appBecomingInactiveCallbacks.getReference (i)->appBecomingInactive();
}
- (void) application: (UIApplication*) application handleEventsForBackgroundURLSession: (NSString*)identifier
completionHandler: (void (^)(void))completionHandler
{
ignoreUnused (application);
URL::DownloadTask::juce_iosURLSessionNotify (nsStringToJuce (identifier));
completionHandler();
}
- (void) applicationDidReceiveMemoryWarning: (UIApplication*) application
{
ignoreUnused (application);
if (auto* app = JUCEApplicationBase::getInstance())
app->memoryWarningReceived();
}
- (void) setPushNotificationsDelegateToUse: (NSObject*) delegate
{
_pushNotificationsDelegate = delegate;
}
#if JUCE_PUSH_NOTIFICATIONS
- (void) application: (UIApplication*) application
didRegisterForRemoteNotificationsWithDeviceToken: (NSData*) deviceToken
{
ignoreUnused (application);
SEL selector = @selector (application:didRegisterForRemoteNotificationsWithDeviceToken:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &deviceToken atIndex:3];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
didFailToRegisterForRemoteNotificationsWithError: (NSError*) error
{
ignoreUnused (application);
SEL selector = @selector (application:didFailToRegisterForRemoteNotificationsWithError:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &error atIndex:3];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
didReceiveRemoteNotification: (NSDictionary*) userInfo
{
ignoreUnused (application);
SEL selector = @selector (application:didReceiveRemoteNotification:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &userInfo atIndex:3];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
didReceiveRemoteNotification: (NSDictionary*) userInfo
fetchCompletionHandler: (void (^)(UIBackgroundFetchResult result)) completionHandler
{
ignoreUnused (application);
SEL selector = @selector (application:didReceiveRemoteNotification:fetchCompletionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &userInfo atIndex:3];
[invocation setArgument: &completionHandler atIndex:4];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forRemoteNotification: (NSDictionary*) userInfo
withResponseInfo: (NSDictionary*) responseInfo
completionHandler: (void(^)()) completionHandler
{
ignoreUnused (application);
SEL selector = @selector (application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &identifier atIndex:3];
[invocation setArgument: &userInfo atIndex:4];
[invocation setArgument: &responseInfo atIndex:5];
[invocation setArgument: &completionHandler atIndex:6];
[invocation invoke];
}
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
- (void) application: (UIApplication*) application
didRegisterUserNotificationSettings: (UIUserNotificationSettings*) notificationSettings
{
ignoreUnused (application);
SEL selector = @selector (application:didRegisterUserNotificationSettings:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector:selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex: 2];
[invocation setArgument: &notificationSettings atIndex: 3];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
didReceiveLocalNotification: (UILocalNotification*) notification
{
ignoreUnused (application);
SEL selector = @selector (application:didReceiveLocalNotification:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex: 2];
[invocation setArgument: &notification atIndex: 3];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forLocalNotification: (UILocalNotification*) notification
completionHandler: (void(^)()) completionHandler
{
ignoreUnused (application);
SEL selector = @selector (application:handleActionWithIdentifier:forLocalNotification:completionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &identifier atIndex:3];
[invocation setArgument: &notification atIndex:4];
[invocation setArgument: &completionHandler atIndex:5];
[invocation invoke];
}
}
- (void) application: (UIApplication*) application
handleActionWithIdentifier: (NSString*) identifier
forLocalNotification: (UILocalNotification*) notification
withResponseInfo: (NSDictionary*) responseInfo
completionHandler: (void(^)()) completionHandler
{
ignoreUnused (application);
SEL selector = @selector (application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &application atIndex:2];
[invocation setArgument: &identifier atIndex:3];
[invocation setArgument: &notification atIndex:4];
[invocation setArgument: &responseInfo atIndex:5];
[invocation setArgument: &completionHandler atIndex:6];
[invocation invoke];
}
}
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
- (void) userNotificationCenter: (UNUserNotificationCenter*) center
willPresentNotification: (UNNotification*) notification
withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler
{
ignoreUnused (center);
SEL selector = @selector (userNotificationCenter:willPresentNotification:withCompletionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &center atIndex:2];
[invocation setArgument: &notification atIndex:3];
[invocation setArgument: &completionHandler atIndex:4];
[invocation invoke];
}
}
- (void) userNotificationCenter: (UNUserNotificationCenter*) center
didReceiveNotificationResponse: (UNNotificationResponse*) response
withCompletionHandler: (void(^)()) completionHandler
{
ignoreUnused (center);
SEL selector = @selector (userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:);
if (_pushNotificationsDelegate != nil && [_pushNotificationsDelegate respondsToSelector: selector])
{
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature: [_pushNotificationsDelegate methodSignatureForSelector: selector]];
[invocation setSelector: selector];
[invocation setTarget: _pushNotificationsDelegate];
[invocation setArgument: &center atIndex:2];
[invocation setArgument: &response atIndex:3];
[invocation setArgument: &completionHandler atIndex:4];
[invocation invoke];
}
}
#endif
@end
namespace juce
{
int juce_iOSMain (int argc, const char* argv[], void* customDelegatePtr);
int juce_iOSMain (int argc, const char* argv[], void* customDelegatePtr)
{
Class delegateClass = (customDelegatePtr != nullptr ? reinterpret_cast<Class> (customDelegatePtr) : [JuceAppStartupDelegate class]);
return UIApplicationMain (argc, const_cast<char**> (argv), nil, NSStringFromClass (delegateClass));
}
//==============================================================================
void LookAndFeel::playAlertSound()
{
// TODO
}
//==============================================================================
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray&, bool, Component*, std::function<void()>)
{
jassertfalse; // no such thing on iOS!
return false;
}
bool DragAndDropContainer::performExternalDragDropOfText (const String&, Component*, std::function<void()>)
{
jassertfalse; // no such thing on iOS!
return false;
}
//==============================================================================
void Desktop::setScreenSaverEnabled (const bool isEnabled)
{
if (! SystemStats::isRunningInAppExtensionSandbox())
[[UIApplication sharedApplication] setIdleTimerDisabled: ! isEnabled];
}
bool Desktop::isScreenSaverEnabled()
{
if (SystemStats::isRunningInAppExtensionSandbox())
return true;
return ! [[UIApplication sharedApplication] isIdleTimerDisabled];
}
//==============================================================================
Image detail::WindowingHelpers::createIconForFile (const File&)
{
return {};
}
//==============================================================================
void SystemClipboard::copyTextToClipboard (const String& text)
{
[[UIPasteboard generalPasteboard] setValue: juceStringToNS (text)
forPasteboardType: @"public.text"];
}
String SystemClipboard::getTextFromClipboard()
{
return nsStringToJuce ([[UIPasteboard generalPasteboard] string]);
}
//==============================================================================
bool detail::MouseInputSourceList::addSource()
{
addSource (sources.size(), MouseInputSource::InputSourceType::touch);
return true;
}
bool detail::MouseInputSourceList::canUseTouch() const
{
return true;
}
bool Desktop::canUseSemiTransparentWindows() noexcept
{
return true;
}
bool Desktop::isDarkModeActive() const
{
if (@available (iOS 12.0, *))
return [[[UIScreen mainScreen] traitCollection] userInterfaceStyle] == UIUserInterfaceStyleDark;
return false;
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
static const auto darkModeSelector = @selector (darkModeChanged:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
class Desktop::NativeDarkModeChangeDetectorImpl
{
public:
NativeDarkModeChangeDetectorImpl()
{
static DelegateClass delegateClass;
delegate.reset ([delegateClass.createInstance() init]);
observer.emplace (delegate.get(), darkModeSelector, UIViewComponentPeer::getDarkModeNotificationName(), nil);
}
private:
struct DelegateClass final : public ObjCClass<NSObject>
{
DelegateClass() : ObjCClass<NSObject> ("JUCEDelegate_")
{
addMethod (darkModeSelector, [] (id, SEL, NSNotification*) { Desktop::getInstance().darkModeChanged(); });
registerClass();
}
};
NSUniquePtr<NSObject> delegate;
Optional<ScopedNotificationCenterObserver> observer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl)
};
std::unique_ptr<Desktop::NativeDarkModeChangeDetectorImpl> Desktop::createNativeDarkModeChangeDetectorImpl()
{
return std::make_unique<NativeDarkModeChangeDetectorImpl>();
}
//==============================================================================
Point<float> MouseInputSource::getCurrentRawMousePosition()
{
return juce_lastMousePos;
}
void MouseInputSource::setRawMousePosition (Point<float>)
{
}
double Desktop::getDefaultMasterScale()
{
return 1.0;
}
Desktop::DisplayOrientation Desktop::getCurrentOrientation() const
{
UIInterfaceOrientation orientation = SystemStats::isRunningInAppExtensionSandbox() ? UIInterfaceOrientationPortrait
: getWindowOrientation();
return Orientations::convertToJuce (orientation);
}
// The most straightforward way of retrieving the screen area available to an iOS app
// seems to be to create a new window (which will take up all available space) and to
// query its frame.
struct TemporaryWindow
{
UIWindow* window = [[UIWindow alloc] init];
~TemporaryWindow() noexcept { [window release]; }
};
static Rectangle<int> getRecommendedWindowBounds()
{
return convertToRectInt (TemporaryWindow().window.frame);
}
static BorderSize<int> getSafeAreaInsets (float masterScale)
{
if (@available (iOS 11.0, *))
{
UIEdgeInsets safeInsets = TemporaryWindow().window.safeAreaInsets;
return detail::WindowingHelpers::roundToInt (BorderSize<double> { safeInsets.top,
safeInsets.left,
safeInsets.bottom,
safeInsets.right }.multipliedBy (1.0 / (double) masterScale));
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
auto statusBarSize = [UIApplication sharedApplication].statusBarFrame.size;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
auto statusBarHeight = jmin (statusBarSize.width, statusBarSize.height);
return { roundToInt (statusBarHeight / masterScale), 0, 0, 0 };
}
//==============================================================================
void Displays::findDisplays (float masterScale)
{
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
static const auto keyboardShownSelector = @selector (juceKeyboardShown:);
static const auto keyboardHiddenSelector = @selector (juceKeyboardHidden:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
class OnScreenKeyboardChangeDetectorImpl
{
public:
OnScreenKeyboardChangeDetectorImpl()
{
static DelegateClass delegateClass;
delegate.reset ([delegateClass.createInstance() init]);
object_setInstanceVariable (delegate.get(), "owner", this);
observers.emplace_back (delegate.get(), keyboardShownSelector, UIKeyboardDidShowNotification, nil);
observers.emplace_back (delegate.get(), keyboardHiddenSelector, UIKeyboardDidHideNotification, nil);
}
auto getInsets() const { return insets; }
private:
struct DelegateClass final : public ObjCClass<NSObject>
{
DelegateClass() : ObjCClass<NSObject> ("JUCEOnScreenKeyboardObserver_")
{
addIvar<OnScreenKeyboardChangeDetectorImpl*> ("owner");
addMethod (keyboardShownSelector, [] (id self, SEL, NSNotification* notification)
{
setKeyboardScreenBounds (self, [&]() -> BorderSize<double>
{
auto* info = [notification userInfo];
if (info == nullptr)
return {};
auto* value = static_cast<NSValue*> ([info objectForKey: UIKeyboardFrameEndUserInfoKey]);
if (value == nullptr)
return {};
auto* display = Desktop::getInstance().getDisplays().getPrimaryDisplay();
if (display == nullptr)
return {};
const auto rect = convertToRectInt ([value CGRectValue]);
BorderSize<double> result;
if (rect.getY() == display->totalArea.getY())
result.setTop (rect.getHeight());
if (rect.getBottom() == display->totalArea.getBottom())
result.setBottom (rect.getHeight());
return result;
}());
});
addMethod (keyboardHiddenSelector, [] (id self, SEL, NSNotification*)
{
setKeyboardScreenBounds (self, {});
});
registerClass();
}
private:
static void setKeyboardScreenBounds (id self, BorderSize<double> insets)
{
if (std::exchange (getIvar<OnScreenKeyboardChangeDetectorImpl*> (self, "owner")->insets, insets) != insets)
Desktop::getInstance().displays->refresh();
}
};
BorderSize<double> insets;
NSUniquePtr<NSObject> delegate;
std::vector<ScopedNotificationCenterObserver> observers;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OnScreenKeyboardChangeDetectorImpl)
};
JUCE_AUTORELEASEPOOL
{
static OnScreenKeyboardChangeDetectorImpl keyboardChangeDetector;
UIScreen* s = [UIScreen mainScreen];
Display d;
d.totalArea = convertToRectInt ([s bounds]) / masterScale;
d.userArea = getRecommendedWindowBounds() / masterScale;
d.safeAreaInsets = getSafeAreaInsets (masterScale);
const auto scaledInsets = keyboardChangeDetector.getInsets().multipliedBy (1.0 / (double) masterScale);
d.keyboardInsets = detail::WindowingHelpers::roundToInt (scaledInsets);
d.isMain = true;
d.scale = masterScale * s.scale;
d.dpi = 160 * d.scale;
displays.add (d);
}
}
} // namespace juce
@@ -0,0 +1,854 @@
/*
==============================================================================
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
{
//==============================================================================
class LinuxComponentPeer final : public ComponentPeer,
private XWindowSystemUtilities::XSettings::Listener
{
public:
LinuxComponentPeer (Component& comp, int windowStyleFlags, ::Window parentToAddTo)
: ComponentPeer (comp, windowStyleFlags),
isAlwaysOnTop (comp.isAlwaysOnTop())
{
// it's dangerous to create a window on a thread other than the message thread.
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
const auto* instance = XWindowSystem::getInstance();
if (! instance->isX11Available())
return;
if (isAlwaysOnTop)
++WindowUtilsInternal::numAlwaysOnTopPeers;
repainter = std::make_unique<LinuxRepaintManager> (*this);
windowH = instance->createWindow (parentToAddTo, this);
parentWindow = parentToAddTo;
setTitle (component.getName());
if (auto* xSettings = instance->getXSettings())
xSettings->addListener (this);
getNativeRealtimeModifiers = []() -> ModifierKeys { return XWindowSystem::getInstance()->getNativeRealtimeModifiers(); };
updateVBlankTimer();
}
~LinuxComponentPeer() override
{
// it's dangerous to delete a window on a thread other than the message thread.
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
auto* instance = XWindowSystem::getInstance();
repainter = nullptr;
instance->destroyWindow (windowH);
if (auto* xSettings = instance->getXSettings())
xSettings->removeListener (this);
if (isAlwaysOnTop)
--WindowUtilsInternal::numAlwaysOnTopPeers;
}
::Window getWindowHandle() const noexcept
{
return windowH;
}
//==============================================================================
void* getNativeHandle() const override
{
return reinterpret_cast<void*> (getWindowHandle());
}
//==============================================================================
void forceSetBounds (const Rectangle<int>& correctedNewBounds, bool isNowFullScreen)
{
bounds = correctedNewBounds;
updateScaleFactorFromNewBounds (bounds, false);
auto physicalBounds = parentWindow == 0 ? Desktop::getInstance().getDisplays().logicalToPhysical (bounds)
: bounds * currentScaleFactor;
WeakReference<Component> deletionChecker (&component);
XWindowSystem::getInstance()->setBounds (windowH, physicalBounds, isNowFullScreen);
fullScreen = isNowFullScreen;
if (deletionChecker != nullptr)
{
updateBorderSize();
handleMovedOrResized();
}
}
void setBounds (const Rectangle<int>& newBounds, bool isNowFullScreen) override
{
const auto correctedNewBounds = newBounds.withSize (jmax (1, newBounds.getWidth()),
jmax (1, newBounds.getHeight()));
if (bounds != correctedNewBounds || fullScreen != isNowFullScreen)
forceSetBounds (correctedNewBounds, isNowFullScreen);
}
Point<int> getScreenPosition (bool physical) const
{
auto physicalParentPosition = XWindowSystem::getInstance()->getPhysicalParentScreenPosition();
auto parentPosition = parentWindow == 0 ? Desktop::getInstance().getDisplays().physicalToLogical (physicalParentPosition)
: physicalParentPosition / currentScaleFactor;
auto screenBounds = parentWindow == 0 ? bounds
: bounds.translated (parentPosition.x, parentPosition.y);
if (physical)
return parentWindow == 0 ? Desktop::getInstance().getDisplays().logicalToPhysical (screenBounds.getTopLeft())
: screenBounds.getTopLeft() * currentScaleFactor;
return screenBounds.getTopLeft();
}
Rectangle<int> getBounds() const override
{
return bounds;
}
OptionalBorderSize getFrameSizeIfPresent() const override
{
return windowBorder;
}
BorderSize<int> getFrameSize() const override
{
const auto optionalBorderSize = getFrameSizeIfPresent();
return optionalBorderSize ? (*optionalBorderSize) : BorderSize<int>();
}
Point<float> localToGlobal (Point<float> relativePosition) override
{
return localToGlobal (*this, relativePosition);
}
Point<float> globalToLocal (Point<float> screenPosition) override
{
return globalToLocal (*this, screenPosition);
}
using ComponentPeer::localToGlobal;
using ComponentPeer::globalToLocal;
//==============================================================================
StringArray getAvailableRenderingEngines() override
{
return { "Software Renderer" };
}
void setVisible (bool shouldBeVisible) override
{
XWindowSystem::getInstance()->setVisible (windowH, shouldBeVisible);
}
void setTitle (const String& title) override
{
XWindowSystem::getInstance()->setTitle (windowH, title);
}
void setMinimised (bool shouldBeMinimised) override
{
if (shouldBeMinimised)
XWindowSystem::getInstance()->setMinimised (windowH, shouldBeMinimised);
else
setVisible (true);
}
bool isMinimised() const override
{
return XWindowSystem::getInstance()->isMinimised (windowH);
}
void setFullScreen (bool shouldBeFullScreen) override
{
auto r = lastNonFullscreenBounds; // (get a copy of this before de-minimising)
setMinimised (false);
if (fullScreen != shouldBeFullScreen)
{
const auto usingNativeTitleBar = ((styleFlags & windowHasTitleBar) != 0);
if (usingNativeTitleBar)
XWindowSystem::getInstance()->setMaximised (windowH, shouldBeFullScreen);
if (shouldBeFullScreen)
r = usingNativeTitleBar ? XWindowSystem::getInstance()->getWindowBounds (windowH, parentWindow)
: Desktop::getInstance().getDisplays().getDisplayForRect (bounds)->userArea;
if (! r.isEmpty())
setBounds (detail::ScalingHelpers::scaledScreenPosToUnscaled (component, r), shouldBeFullScreen);
component.repaint();
}
}
bool isFullScreen() const override
{
return fullScreen;
}
bool contains (Point<int> localPos, bool trueIfInAChildWindow) const override
{
if (! bounds.withZeroOrigin().contains (localPos))
return false;
for (int i = Desktop::getInstance().getNumComponents(); --i >= 0;)
{
auto* c = Desktop::getInstance().getComponent (i);
if (c == &component)
break;
if (! c->isVisible())
continue;
auto* otherPeer = c->getPeer();
jassert (otherPeer == nullptr || dynamic_cast<LinuxComponentPeer*> (c->getPeer()) != nullptr);
if (auto* peer = static_cast<LinuxComponentPeer*> (otherPeer))
if (peer->contains (globalToLocal (*peer, localToGlobal (*this, localPos.toFloat())).roundToInt(), true))
return false;
}
if (trueIfInAChildWindow)
return true;
return XWindowSystem::getInstance()->contains (windowH, localPos * currentScaleFactor);
}
void toFront (bool makeActive) override
{
if (makeActive)
{
setVisible (true);
grabFocus();
}
XWindowSystem::getInstance()->toFront (windowH, makeActive);
handleBroughtToFront();
}
void toBehind (ComponentPeer* other) override
{
if (auto* otherPeer = dynamic_cast<LinuxComponentPeer*> (other))
{
if (otherPeer->styleFlags & windowIsTemporary)
return;
setMinimised (false);
XWindowSystem::getInstance()->toBehind (windowH, otherPeer->windowH);
}
else
{
jassertfalse; // wrong type of window?
}
}
bool isFocused() const override
{
return XWindowSystem::getInstance()->isFocused (windowH);
}
void grabFocus() override
{
if (XWindowSystem::getInstance()->grabFocus (windowH))
isActiveApplication = true;
}
//==============================================================================
void repaint (const Rectangle<int>& area) override
{
if (repainter != nullptr)
repainter->repaint (area.getIntersection (bounds.withZeroOrigin()));
}
void performAnyPendingRepaintsNow() override
{
if (repainter != nullptr)
repainter->performAnyPendingRepaintsNow();
}
void setIcon (const Image& newIcon) override
{
XWindowSystem::getInstance()->setIcon (windowH, newIcon);
}
double getPlatformScaleFactor() const noexcept override
{
return currentScaleFactor;
}
void setAlpha (float) override {}
bool setAlwaysOnTop (bool) override { return false; }
void textInputRequired (Point<int>, TextInputTarget&) override {}
//==============================================================================
void addOpenGLRepaintListener (Component* dummy)
{
if (dummy != nullptr)
glRepaintListeners.addIfNotAlreadyThere (dummy);
}
void removeOpenGLRepaintListener (Component* dummy)
{
if (dummy != nullptr)
glRepaintListeners.removeAllInstancesOf (dummy);
}
void repaintOpenGLContexts()
{
for (auto* c : glRepaintListeners)
c->handleCommandMessage (0);
}
//==============================================================================
::Window getParentWindow() { return parentWindow; }
void setParentWindow (::Window newParent) { parentWindow = newParent; }
//==============================================================================
bool isConstrainedNativeWindow() const
{
return constrainer != nullptr
&& (styleFlags & (windowHasTitleBar | windowIsResizable)) == (windowHasTitleBar | windowIsResizable)
&& ! isKioskMode();
}
void updateWindowBounds()
{
if (windowH == 0)
{
jassertfalse;
return;
}
if (isConstrainedNativeWindow())
XWindowSystem::getInstance()->updateConstraints (windowH);
auto physicalBounds = XWindowSystem::getInstance()->getWindowBounds (windowH, parentWindow);
updateScaleFactorFromNewBounds (physicalBounds, true);
bounds = parentWindow == 0 ? Desktop::getInstance().getDisplays().physicalToLogical (physicalBounds)
: physicalBounds / currentScaleFactor;
updateVBlankTimer();
}
void updateBorderSize()
{
if ((styleFlags & windowHasTitleBar) == 0)
{
windowBorder = ComponentPeer::OptionalBorderSize { BorderSize<int>() };
}
else if (! windowBorder
|| ((*windowBorder).getTopAndBottom() == 0 && (*windowBorder).getLeftAndRight() == 0))
{
windowBorder = [&]()
{
if (auto unscaledBorderSize = XWindowSystem::getInstance()->getBorderSize (windowH))
return OptionalBorderSize { (*unscaledBorderSize).multipliedBy (1.0 / currentScaleFactor) };
return OptionalBorderSize {};
}();
}
}
bool setWindowAssociation (::Window windowIn)
{
clearWindowAssociation();
association = { this, windowIn };
return association.isValid();
}
void clearWindowAssociation() { association = {}; }
void startHostManagedResize (Point<int>, ResizableBorderComponent::Zone zone) override
{
XWindowSystem::getInstance()->startHostManagedResize (windowH, zone);
}
//==============================================================================
static bool isActiveApplication;
bool focused = false;
private:
//==============================================================================
class LinuxRepaintManager
{
public:
LinuxRepaintManager (LinuxComponentPeer& p)
: peer (p),
isSemiTransparentWindow ((peer.getStyleFlags() & ComponentPeer::windowIsSemiTransparent) != 0)
{
}
void dispatchDeferredRepaints()
{
XWindowSystem::getInstance()->processPendingPaintsForWindow (peer.windowH);
if (XWindowSystem::getInstance()->getNumPaintsPendingForWindow (peer.windowH) > 0)
return;
if (! regionsNeedingRepaint.isEmpty())
performAnyPendingRepaintsNow();
else if (Time::getApproximateMillisecondCounter() > lastTimeImageUsed + 3000)
image = Image();
}
void repaint (Rectangle<int> area)
{
regionsNeedingRepaint.add (area * peer.currentScaleFactor);
}
void performAnyPendingRepaintsNow()
{
if (XWindowSystem::getInstance()->getNumPaintsPendingForWindow (peer.windowH) > 0)
return;
auto originalRepaintRegion = regionsNeedingRepaint;
regionsNeedingRepaint.clear();
auto totalArea = originalRepaintRegion.getBounds();
if (! totalArea.isEmpty())
{
const auto wasImageNull = image.isNull();
if (wasImageNull || image.getWidth() < totalArea.getWidth()
|| image.getHeight() < totalArea.getHeight())
{
image = XWindowSystem::getInstance()->createImage (isSemiTransparentWindow,
totalArea.getWidth(), totalArea.getHeight(),
useARGBImagesForRendering);
if (wasImageNull)
{
// After calling createImage() XWindowSystem::getWindowBounds() will return
// changed coordinates that look like the result of some position
// defaulting mechanism. If we handle a configureNotifyEvent after
// createImage() and before we would issue new, valid coordinates, we will
// apply these default, unwanted coordinates to our window. To avoid that
// we immediately send another positioning message to guarantee that the
// next configureNotifyEvent will read valid values.
//
// This issue only occurs right after peer creation, when the image is
// null. Updating when only the width or height is changed would lead to
// incorrect behaviour.
peer.forceSetBounds (detail::ScalingHelpers::scaledScreenPosToUnscaled (peer.component, peer.component.getBoundsInParent()),
peer.isFullScreen());
}
}
RectangleList<int> adjustedList (originalRepaintRegion);
adjustedList.offsetAll (-totalArea.getX(), -totalArea.getY());
if (XWindowSystem::getInstance()->canUseARGBImages())
for (auto& i : originalRepaintRegion)
image.clear (i - totalArea.getPosition());
{
auto context = peer.getComponent().getLookAndFeel()
.createGraphicsContext (image, -totalArea.getPosition(), adjustedList);
context->addTransform (AffineTransform::scale ((float) peer.currentScaleFactor));
peer.handlePaint (*context);
}
for (auto& i : originalRepaintRegion)
XWindowSystem::getInstance()->blitToWindow (peer.windowH, image, i, totalArea);
}
lastTimeImageUsed = Time::getApproximateMillisecondCounter();
}
private:
LinuxComponentPeer& peer;
const bool isSemiTransparentWindow;
Image image;
uint32 lastTimeImageUsed = 0;
RectangleList<int> regionsNeedingRepaint;
bool useARGBImagesForRendering = XWindowSystem::getInstance()->canUseARGBImages();
JUCE_DECLARE_NON_COPYABLE (LinuxRepaintManager)
};
//==============================================================================
template <typename This>
static Point<float> localToGlobal (This& t, Point<float> relativePosition)
{
return relativePosition + t.getScreenPosition (false).toFloat();
}
template <typename This>
static Point<float> globalToLocal (This& t, Point<float> screenPosition)
{
return screenPosition - t.getScreenPosition (false).toFloat();
}
//==============================================================================
void settingChanged (const XWindowSystemUtilities::XSetting& settingThatHasChanged) override
{
static StringArray possibleSettings { XWindowSystem::getWindowScalingFactorSettingName(),
"Gdk/UnscaledDPI",
"Xft/DPI" };
if (possibleSettings.contains (settingThatHasChanged.name))
forceDisplayUpdate();
}
void updateScaleFactorFromNewBounds (const Rectangle<int>& newBounds, bool isPhysical)
{
Point<int> translation = (parentWindow != 0 ? getScreenPosition (isPhysical) : Point<int>());
const auto& desktop = Desktop::getInstance();
if (auto* display = desktop.getDisplays().getDisplayForRect (newBounds.translated (translation.x, translation.y),
isPhysical))
{
auto newScaleFactor = display->scale / desktop.getGlobalScaleFactor();
if (! approximatelyEqual (newScaleFactor, currentScaleFactor))
{
currentScaleFactor = newScaleFactor;
scaleFactorListeners.call ([&] (ScaleFactorListener& l) { l.nativeScaleFactorChanged (currentScaleFactor); });
}
}
}
void onVBlank()
{
vBlankListeners.call ([] (auto& l) { l.onVBlank(); });
if (repainter != nullptr)
repainter->dispatchDeferredRepaints();
}
void updateVBlankTimer()
{
if (auto* display = Desktop::getInstance().getDisplays().getDisplayForRect (bounds))
{
// Some systems fail to set an explicit refresh rate, or ask for a refresh rate of 0
// (observed on Raspbian Bullseye over VNC). In these situations, use a fallback value.
const auto newIntFrequencyHz = roundToInt (display->verticalFrequencyHz.value_or (0.0));
const auto frequencyToUse = newIntFrequencyHz != 0 ? newIntFrequencyHz : 100;
if (vBlankManager.getTimerInterval() != frequencyToUse)
vBlankManager.startTimerHz (frequencyToUse);
}
}
//==============================================================================
std::unique_ptr<LinuxRepaintManager> repainter;
TimedCallback vBlankManager { [this]() { onVBlank(); } };
::Window windowH = {}, parentWindow = {};
Rectangle<int> bounds;
ComponentPeer::OptionalBorderSize windowBorder;
bool fullScreen = false, isAlwaysOnTop = false;
double currentScaleFactor = 1.0;
Array<Component*> glRepaintListeners;
ScopedWindowAssociation association;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LinuxComponentPeer)
};
bool LinuxComponentPeer::isActiveApplication = false;
//==============================================================================
ComponentPeer* Component::createNewPeer (int styleFlags, void* nativeWindowToAttachTo)
{
return new LinuxComponentPeer (*this, styleFlags, (::Window) nativeWindowToAttachTo);
}
//==============================================================================
JUCE_API bool JUCE_CALLTYPE Process::isForegroundProcess() { return LinuxComponentPeer::isActiveApplication; }
JUCE_API void JUCE_CALLTYPE Process::makeForegroundProcess() {}
JUCE_API void JUCE_CALLTYPE Process::hide() {}
//==============================================================================
void Desktop::setKioskComponent (Component* comp, bool enableOrDisable, bool)
{
if (enableOrDisable)
comp->setBounds (getDisplays().getDisplayForRect (comp->getScreenBounds())->totalArea);
}
void Displays::findDisplays (float masterScale)
{
if (XWindowSystem::getInstance()->getDisplay() != nullptr)
{
displays = XWindowSystem::getInstance()->findDisplays (masterScale);
if (! displays.isEmpty())
updateToLogical();
}
}
bool Desktop::canUseSemiTransparentWindows() noexcept
{
return XWindowSystem::getInstance()->canUseSemiTransparentWindows();
}
class Desktop::NativeDarkModeChangeDetectorImpl : private XWindowSystemUtilities::XSettings::Listener
{
public:
NativeDarkModeChangeDetectorImpl()
{
const auto* windowSystem = XWindowSystem::getInstance();
if (auto* xSettings = windowSystem->getXSettings())
xSettings->addListener (this);
darkModeEnabled = windowSystem->isDarkModeActive();
}
~NativeDarkModeChangeDetectorImpl() override
{
if (auto* windowSystem = XWindowSystem::getInstanceWithoutCreating())
if (auto* xSettings = windowSystem->getXSettings())
xSettings->removeListener (this);
}
bool isDarkModeEnabled() const noexcept { return darkModeEnabled; }
private:
void settingChanged (const XWindowSystemUtilities::XSetting& settingThatHasChanged) override
{
if (settingThatHasChanged.name == XWindowSystem::getThemeNameSettingName())
{
const auto wasDarkModeEnabled = std::exchange (darkModeEnabled, XWindowSystem::getInstance()->isDarkModeActive());
if (darkModeEnabled != wasDarkModeEnabled)
Desktop::getInstance().darkModeChanged();
}
}
bool darkModeEnabled = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl)
};
std::unique_ptr<Desktop::NativeDarkModeChangeDetectorImpl> Desktop::createNativeDarkModeChangeDetectorImpl()
{
return std::make_unique<NativeDarkModeChangeDetectorImpl>();
}
bool Desktop::isDarkModeActive() const
{
return nativeDarkModeChangeDetectorImpl->isDarkModeEnabled();
}
static bool screenSaverAllowed = true;
void Desktop::setScreenSaverEnabled (bool isEnabled)
{
if (screenSaverAllowed != isEnabled)
{
screenSaverAllowed = isEnabled;
XWindowSystem::getInstance()->setScreenSaverEnabled (screenSaverAllowed);
}
}
bool Desktop::isScreenSaverEnabled()
{
return screenSaverAllowed;
}
double Desktop::getDefaultMasterScale() { return 1.0; }
Desktop::DisplayOrientation Desktop::getCurrentOrientation() const { return upright; }
void Desktop::allowedOrientationsChanged() {}
//==============================================================================
bool detail::MouseInputSourceList::addSource()
{
if (sources.isEmpty())
{
addSource (0, MouseInputSource::InputSourceType::mouse);
return true;
}
return false;
}
bool detail::MouseInputSourceList::canUseTouch() const
{
return false;
}
Point<float> MouseInputSource::getCurrentRawMousePosition()
{
return Desktop::getInstance().getDisplays().physicalToLogical (XWindowSystem::getInstance()->getCurrentMousePosition());
}
void MouseInputSource::setRawMousePosition (Point<float> newPosition)
{
XWindowSystem::getInstance()->setMousePosition (Desktop::getInstance().getDisplays().logicalToPhysical (newPosition));
}
//==============================================================================
class MouseCursor::PlatformSpecificHandle
{
public:
explicit PlatformSpecificHandle (const MouseCursor::StandardCursorType type)
: cursorHandle (makeHandle (type)) {}
explicit PlatformSpecificHandle (const detail::CustomMouseCursorInfo& info)
: cursorHandle (makeHandle (info)) {}
~PlatformSpecificHandle()
{
if (cursorHandle != Cursor{})
XWindowSystem::getInstance()->deleteMouseCursor (cursorHandle);
}
static void showInWindow (PlatformSpecificHandle* handle, ComponentPeer* peer)
{
const auto cursor = handle != nullptr ? handle->cursorHandle : Cursor{};
if (peer != nullptr)
XWindowSystem::getInstance()->showCursor ((::Window) peer->getNativeHandle(), cursor);
}
private:
static Cursor makeHandle (const detail::CustomMouseCursorInfo& info)
{
const auto image = info.image.getImage();
return XWindowSystem::getInstance()->createCustomMouseCursorInfo (image.rescaled ((int) (image.getWidth() / info.image.getScale()),
(int) (image.getHeight() / info.image.getScale())), info.hotspot);
}
static Cursor makeHandle (MouseCursor::StandardCursorType type)
{
return XWindowSystem::getInstance()->createStandardMouseCursor (type);
}
Cursor cursorHandle;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE (PlatformSpecificHandle)
JUCE_DECLARE_NON_MOVEABLE (PlatformSpecificHandle)
};
//==============================================================================
static LinuxComponentPeer* getPeerForDragEvent (Component* sourceComp)
{
if (sourceComp == nullptr)
if (auto* draggingSource = Desktop::getInstance().getDraggingMouseSource (0))
sourceComp = draggingSource->getComponentUnderMouse();
if (sourceComp != nullptr)
if (auto* lp = dynamic_cast<LinuxComponentPeer*> (sourceComp->getPeer()))
return lp;
jassertfalse; // This method must be called in response to a component's mouseDown or mouseDrag event!
return nullptr;
}
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles,
Component* sourceComp, std::function<void()> callback)
{
if (files.isEmpty())
return false;
if (auto* peer = getPeerForDragEvent (sourceComp))
return XWindowSystem::getInstance()->externalDragFileInit (peer, files, canMoveFiles, std::move (callback));
// This method must be called in response to a component's mouseDown or mouseDrag event!
jassertfalse;
return false;
}
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComp,
std::function<void()> callback)
{
if (text.isEmpty())
return false;
if (auto* peer = getPeerForDragEvent (sourceComp))
return XWindowSystem::getInstance()->externalDragTextInit (peer, text, std::move (callback));
// This method must be called in response to a component's mouseDown or mouseDrag event!
jassertfalse;
return false;
}
//==============================================================================
void SystemClipboard::copyTextToClipboard (const String& clipText)
{
XWindowSystem::getInstance()->copyTextToClipboard (clipText);
}
String SystemClipboard::getTextFromClipboard()
{
return XWindowSystem::getInstance()->getTextFromClipboard();
}
//==============================================================================
bool KeyPress::isKeyCurrentlyDown (int keyCode)
{
return XWindowSystem::getInstance()->isKeyCurrentlyDown (keyCode);
}
void LookAndFeel::playAlertSound()
{
std::cout << "\a" << std::flush;
}
//==============================================================================
Image detail::WindowingHelpers::createIconForFile (const File&)
{
return {};
}
void juce_LinuxAddRepaintListener (ComponentPeer* peer, Component* dummy);
void juce_LinuxAddRepaintListener (ComponentPeer* peer, Component* dummy)
{
if (auto* linuxPeer = dynamic_cast<LinuxComponentPeer*> (peer))
linuxPeer->addOpenGLRepaintListener (dummy);
}
void juce_LinuxRemoveRepaintListener (ComponentPeer* peer, Component* dummy);
void juce_LinuxRemoveRepaintListener (ComponentPeer* peer, Component* dummy)
{
if (auto* linuxPeer = dynamic_cast<LinuxComponentPeer*> (peer))
linuxPeer->removeOpenGLRepaintListener (dummy);
}
} // namespace juce
@@ -0,0 +1,588 @@
/*
==============================================================================
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
{
void LookAndFeel::playAlertSound()
{
NSBeep();
}
//==============================================================================
static NSRect getDragRect (NSView* view, NSEvent* event)
{
auto eventPos = [event locationInWindow];
return [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f)
fromView: nil];
}
static NSView* getNSViewForDragEvent (Component* sourceComp)
{
if (sourceComp == nullptr)
if (auto* draggingSource = Desktop::getInstance().getDraggingMouseSource (0))
sourceComp = draggingSource->getComponentUnderMouse();
if (sourceComp != nullptr)
return (NSView*) sourceComp->getWindowHandle();
jassertfalse; // This method must be called in response to a component's mouseDown or mouseDrag event!
return nil;
}
class NSDraggingSourceHelper final : public ObjCClass<NSObject<NSDraggingSource>>
{
public:
static void setText (id self, const String& text)
{
object_setInstanceVariable (self, "text", new String (text));
}
static void setCompletionCallback (id self, std::function<void()> cb)
{
object_setInstanceVariable (self, "callback", new std::function<void()> (cb));
}
static void setDragOperation (id self, NSDragOperation op)
{
object_setInstanceVariable (self, "operation", new NSDragOperation (op));
}
static NSDraggingSourceHelper& get()
{
static NSDraggingSourceHelper draggingSourceHelper;
return draggingSourceHelper;
}
private:
NSDraggingSourceHelper()
: ObjCClass ("JUCENSDraggingSourceHelper_")
{
addIvar<std::function<void()>*> ("callback");
addIvar<String*> ("text");
addIvar<NSDragOperation*> ("operation");
addMethod (@selector (dealloc), [] (id self, SEL)
{
delete getIvar<String*> (self, "text");
delete getIvar<std::function<void()>*> (self, "callback");
delete getIvar<NSDragOperation*> (self, "operation");
sendSuperclassMessage<void> (self, @selector (dealloc));
});
addMethod (@selector (pasteboard:item:provideDataForType:), [] (id self, SEL, NSPasteboard* sender, NSPasteboardItem*, NSString* type)
{
if ([type compare: NSPasteboardTypeString] == NSOrderedSame)
if (auto* text = getIvar<String*> (self, "text"))
[sender setData: [juceStringToNS (*text) dataUsingEncoding: NSUTF8StringEncoding]
forType: NSPasteboardTypeString];
});
addMethod (@selector (draggingSession:sourceOperationMaskForDraggingContext:), [] (id self, SEL, NSDraggingSession*, NSDraggingContext)
{
return *getIvar<NSDragOperation*> (self, "operation");
});
addMethod (@selector (draggingSession:endedAtPoint:operation:), [] (id self, SEL, NSDraggingSession*, NSPoint p, NSDragOperation)
{
// Our view doesn't receive a mouse up when the drag ends so we need to generate one here and send it...
if (auto* view = getNSViewForDragEvent (nullptr))
if (auto* cgEvent = CGEventCreateMouseEvent (nullptr, kCGEventLeftMouseUp, CGPointMake (p.x, p.y), kCGMouseButtonLeft))
if (id e = [NSEvent eventWithCGEvent: cgEvent])
[view mouseUp: e];
if (auto* cb = getIvar<std::function<void()>*> (self, "callback"))
cb->operator()();
});
addProtocol (@protocol (NSPasteboardItemDataProvider));
registerClass();
}
};
bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComponent,
std::function<void()> callback)
{
if (text.isEmpty())
return false;
if (auto* view = getNSViewForDragEvent (sourceComponent))
{
JUCE_AUTORELEASEPOOL
{
if (auto event = [[view window] currentEvent])
{
id helper = [NSDraggingSourceHelper::get().createInstance() init];
NSDraggingSourceHelper::setText (helper, text);
NSDraggingSourceHelper::setDragOperation (helper, NSDragOperationCopy);
if (callback != nullptr)
NSDraggingSourceHelper::setCompletionCallback (helper, callback);
auto pasteboardItem = [[NSPasteboardItem new] autorelease];
[pasteboardItem setDataProvider: helper
forTypes: [NSArray arrayWithObjects: NSPasteboardTypeString, nil]];
auto dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter: pasteboardItem] autorelease];
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: nsEmptyString()];
[dragItem setDraggingFrame: getDragRect (view, event) contents: image];
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
if (auto session = [view beginDraggingSessionWithItems: [NSArray arrayWithObject: dragItem]
event: event
source: helper])
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
{
session.animatesToStartingPositionsOnCancelOrFail = YES;
session.draggingFormation = NSDraggingFormationNone;
return true;
}
}
}
}
return false;
}
bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles,
Component* sourceComponent, std::function<void()> callback)
{
if (files.isEmpty())
return false;
if (auto* view = getNSViewForDragEvent (sourceComponent))
{
JUCE_AUTORELEASEPOOL
{
if (auto event = [[view window] currentEvent])
{
auto dragItems = [[[NSMutableArray alloc] init] autorelease];
for (auto& filename : files)
{
auto* nsFilename = juceStringToNS (filename);
auto fileURL = [NSURL fileURLWithPath: nsFilename];
auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: fileURL];
auto eventPos = [event locationInWindow];
auto dragRect = [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f)
fromView: nil];
auto dragImage = [[NSWorkspace sharedWorkspace] iconForFile: nsFilename];
[dragItem setDraggingFrame: dragRect
contents: dragImage];
[dragItems addObject: dragItem];
[dragItem release];
}
auto helper = [NSDraggingSourceHelper::get().createInstance() autorelease];
if (callback != nullptr)
NSDraggingSourceHelper::setCompletionCallback (helper, callback);
NSDraggingSourceHelper::setDragOperation (helper, canMoveFiles ? NSDragOperationMove
: NSDragOperationCopy);
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
return [view beginDraggingSessionWithItems: dragItems
event: event
source: helper] != nullptr;
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
}
}
return false;
}
//==============================================================================
bool Desktop::canUseSemiTransparentWindows() noexcept
{
return true;
}
Point<float> MouseInputSource::getCurrentRawMousePosition()
{
JUCE_AUTORELEASEPOOL
{
auto p = [NSEvent mouseLocation];
return { (float) p.x, (float) (getMainScreenHeight() - p.y) };
}
}
static ComponentPeer* findPeerContainingPoint (Point<float> globalPos)
{
for (int i = 0; i < juce::ComponentPeer::getNumPeers(); ++i)
{
auto* peer = juce::ComponentPeer::getPeer (i);
if (peer->contains (peer->globalToLocal (globalPos).toInt(), false))
return peer;
}
return nullptr;
}
void MouseInputSource::setRawMousePosition (Point<float> newPosition)
{
const auto oldPosition = Desktop::getInstance().getMainMouseSource().getRawScreenPosition();
// this rubbish needs to be done around the warp call, to avoid causing a
// bizarre glitch..
CGAssociateMouseAndMouseCursorPosition (false);
CGWarpMouseCursorPosition (convertToCGPoint (newPosition));
CGAssociateMouseAndMouseCursorPosition (true);
// Mouse enter and exit events seem to be always generated as a consequence of programmatically
// moving the mouse. However, when the mouse stays within the same peer no mouse move event is
// generated, and we lose track of the correct Component under the mouse. Hence, we need to
// generate this missing event here.
if (auto* peer = findPeerContainingPoint (newPosition); peer != nullptr
&& peer == findPeerContainingPoint (oldPosition))
{
peer->handleMouseEvent (MouseInputSource::InputSourceType::mouse,
peer->globalToLocal (newPosition),
ModifierKeys::currentModifiers,
0.0f,
0.0f,
Time::currentTimeMillis());
}
}
double Desktop::getDefaultMasterScale()
{
return 1.0;
}
Desktop::DisplayOrientation Desktop::getCurrentOrientation() const
{
return upright;
}
bool Desktop::isDarkModeActive() const
{
return [[[NSUserDefaults standardUserDefaults] stringForKey: nsStringLiteral ("AppleInterfaceStyle")]
isEqualToString: nsStringLiteral ("Dark")];
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
static const auto darkModeSelector = @selector (darkModeChanged:);
static const auto keyboardVisibilitySelector = @selector (keyboardVisiblityChanged:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
class Desktop::NativeDarkModeChangeDetectorImpl
{
public:
NativeDarkModeChangeDetectorImpl()
{
static DelegateClass delegateClass;
delegate.reset ([delegateClass.createInstance() init]);
observer.emplace (delegate.get(),
darkModeSelector,
@"AppleInterfaceThemeChangedNotification",
nil,
[NSDistributedNotificationCenter class]);
}
private:
struct DelegateClass final : public ObjCClass<NSObject>
{
DelegateClass() : ObjCClass<NSObject> ("JUCEDelegate_")
{
addMethod (darkModeSelector, [] (id, SEL, NSNotification*) { Desktop::getInstance().darkModeChanged(); });
registerClass();
}
};
NSUniquePtr<NSObject> delegate;
Optional<ScopedNotificationCenterObserver> observer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl)
};
std::unique_ptr<Desktop::NativeDarkModeChangeDetectorImpl> Desktop::createNativeDarkModeChangeDetectorImpl()
{
return std::make_unique<NativeDarkModeChangeDetectorImpl>();
}
//==============================================================================
class ScreenSaverDefeater final : public Timer
{
public:
ScreenSaverDefeater()
{
startTimer (5000);
timerCallback();
}
void timerCallback() override
{
if (Process::isForegroundProcess())
{
if (assertion == nullptr)
assertion.reset (new PMAssertion());
}
else
{
assertion.reset();
}
}
struct PMAssertion
{
PMAssertion() : assertionID (kIOPMNullAssertionID)
{
[[maybe_unused]] IOReturn res = IOPMAssertionCreateWithName (kIOPMAssertionTypePreventUserIdleDisplaySleep,
kIOPMAssertionLevelOn,
CFSTR ("JUCE Playback"),
&assertionID);
jassert (res == kIOReturnSuccess);
}
~PMAssertion()
{
if (assertionID != kIOPMNullAssertionID)
IOPMAssertionRelease (assertionID);
}
IOPMAssertionID assertionID;
};
std::unique_ptr<PMAssertion> assertion;
};
static std::unique_ptr<ScreenSaverDefeater> screenSaverDefeater;
void Desktop::setScreenSaverEnabled (const bool isEnabled)
{
if (isEnabled)
screenSaverDefeater.reset();
else if (screenSaverDefeater == nullptr)
screenSaverDefeater.reset (new ScreenSaverDefeater());
}
bool Desktop::isScreenSaverEnabled()
{
return screenSaverDefeater == nullptr;
}
//==============================================================================
struct DisplaySettingsChangeCallback final : private DeletedAtShutdown
{
DisplaySettingsChangeCallback()
{
CGDisplayRegisterReconfigurationCallback (displayReconfigurationCallback, this);
}
~DisplaySettingsChangeCallback()
{
CGDisplayRemoveReconfigurationCallback (displayReconfigurationCallback, this);
clearSingletonInstance();
}
static void displayReconfigurationCallback (CGDirectDisplayID, CGDisplayChangeSummaryFlags, void* userInfo)
{
if (auto* thisPtr = static_cast<DisplaySettingsChangeCallback*> (userInfo))
NullCheckedInvocation::invoke (thisPtr->forceDisplayUpdate);
}
std::function<void()> forceDisplayUpdate;
JUCE_DECLARE_SINGLETON (DisplaySettingsChangeCallback, false)
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DisplaySettingsChangeCallback)
};
JUCE_IMPLEMENT_SINGLETON (DisplaySettingsChangeCallback)
static Rectangle<int> convertDisplayRect (NSRect r, CGFloat mainScreenBottom)
{
r.origin.y = mainScreenBottom - (r.origin.y + r.size.height);
return convertToRectInt (r);
}
static Displays::Display getDisplayFromScreen (NSScreen* s, CGFloat& mainScreenBottom, const float masterScale)
{
Displays::Display d;
d.isMain = (approximatelyEqual (mainScreenBottom, 0.0));
if (d.isMain)
mainScreenBottom = [s frame].size.height;
d.userArea = convertDisplayRect ([s visibleFrame], mainScreenBottom) / masterScale;
d.totalArea = convertDisplayRect ([s frame], mainScreenBottom) / masterScale;
d.scale = masterScale;
if ([s respondsToSelector: @selector (backingScaleFactor)])
d.scale *= s.backingScaleFactor;
NSSize dpi = [[[s deviceDescription] objectForKey: NSDeviceResolution] sizeValue];
d.dpi = (dpi.width + dpi.height) / 2.0;
#if defined (MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
if (@available (macOS 12.0, *))
{
const auto safeInsets = [s safeAreaInsets];
d.safeAreaInsets = detail::WindowingHelpers::roundToInt (BorderSize<double> { safeInsets.top,
safeInsets.left,
safeInsets.bottom,
safeInsets.right }.multipliedBy (1.0 / (double) masterScale));
}
#endif
return d;
}
void Displays::findDisplays (const float masterScale)
{
JUCE_AUTORELEASEPOOL
{
if (DisplaySettingsChangeCallback::getInstanceWithoutCreating() == nullptr)
DisplaySettingsChangeCallback::getInstance()->forceDisplayUpdate = [this] { refresh(); };
CGFloat mainScreenBottom = 0;
for (NSScreen* s in [NSScreen screens])
displays.add (getDisplayFromScreen (s, mainScreenBottom, masterScale));
}
}
//==============================================================================
static void selectImageForDrawing (const Image& image)
{
[NSGraphicsContext saveGraphicsState];
if (@available (macOS 10.10, *))
{
[NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithCGContext: juce_getImageContext (image)
flipped: false]];
return;
}
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
[NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithGraphicsPort: juce_getImageContext (image)
flipped: false]];
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
}
static void releaseImageAfterDrawing()
{
[[NSGraphicsContext currentContext] flushGraphics];
[NSGraphicsContext restoreGraphicsState];
}
Image detail::WindowingHelpers::createIconForFile (const File& file)
{
JUCE_AUTORELEASEPOOL
{
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: juceStringToNS (file.getFullPathName())];
Image result (Image::ARGB, (int) [image size].width, (int) [image size].height, true);
selectImageForDrawing (result);
[image drawAtPoint: NSMakePoint (0, 0)
fromRect: NSMakeRect (0, 0, [image size].width, [image size].height)
operation: NSCompositingOperationSourceOver fraction: 1.0f];
releaseImageAfterDrawing();
return result;
}
}
static Image createNSWindowSnapshot (NSWindow* nsWindow)
{
JUCE_AUTORELEASEPOOL
{
// CGWindowListCreateImage is replaced by functions in the ScreenCaptureKit framework, but
// that framework is only available from macOS 12.3 onwards.
// A suitable @available check should be added once the minimum build OS is 12.3 or greater,
// so that ScreenCaptureKit can be weak-linked.
#if defined (MAC_OS_VERSION_14_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_14_0
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
#define JUCE_DEPRECATION_IGNORED 1
#endif
CGImageRef screenShot = CGWindowListCreateImage (CGRectNull,
kCGWindowListOptionIncludingWindow,
(CGWindowID) [nsWindow windowNumber],
kCGWindowImageBoundsIgnoreFraming);
#if JUCE_DEPRECATION_IGNORED
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
#undef JUCE_DEPRECATION_IGNORED
#endif
NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage: screenShot];
Image result (Image::ARGB, (int) [bitmapRep size].width, (int) [bitmapRep size].height, true);
selectImageForDrawing (result);
[bitmapRep drawAtPoint: NSMakePoint (0, 0)];
releaseImageAfterDrawing();
[bitmapRep release];
CGImageRelease (screenShot);
return result;
}
}
Image createSnapshotOfNativeWindow (void* nativeWindowHandle)
{
if (id windowOrView = (id) nativeWindowHandle)
{
if ([windowOrView isKindOfClass: [NSWindow class]])
return createNSWindowSnapshot ((NSWindow*) windowOrView);
if ([windowOrView isKindOfClass: [NSView class]])
return createNSWindowSnapshot ([(NSView*) windowOrView window]);
}
return {};
}
//==============================================================================
void SystemClipboard::copyTextToClipboard (const String& text)
{
NSPasteboard* pb = [NSPasteboard generalPasteboard];
[pb declareTypes: [NSArray arrayWithObject: NSPasteboardTypeString]
owner: nil];
[pb setString: juceStringToNS (text)
forType: NSPasteboardTypeString];
}
String SystemClipboard::getTextFromClipboard()
{
return nsStringToJuce ([[NSPasteboard generalPasteboard] stringForType: NSPasteboardTypeString]);
}
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
/*
==============================================================================
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.
==============================================================================
*/
#if JUCE_WINDOWS
namespace juce::detail
{
class WindowsHooks::Hooks
{
public:
Hooks() = default;
~Hooks()
{
if (mouseWheelHook != nullptr)
UnhookWindowsHookEx (mouseWheelHook);
if (keyboardHook != nullptr)
UnhookWindowsHookEx (keyboardHook);
}
static inline std::weak_ptr<Hooks> weak;
private:
static LRESULT CALLBACK mouseWheelHookCallback (int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0 && wParam == WM_MOUSEWHEEL)
{
// using a local copy of this struct to support old mingw libraries
struct MOUSEHOOKSTRUCTEX_ final : public MOUSEHOOKSTRUCT { DWORD mouseData; };
auto& hs = *(MOUSEHOOKSTRUCTEX_*) lParam;
if (auto* comp = Desktop::getInstance().findComponentAt ({ hs.pt.x, hs.pt.y }))
{
if (auto* target = static_cast<HWND> (comp->getWindowHandle()))
{
const ScopedThreadDPIAwarenessSetter scope { target };
return PostMessage (target, WM_MOUSEWHEEL,
hs.mouseData & 0xffff0000, MAKELPARAM (hs.pt.x, hs.pt.y));
}
}
}
return CallNextHookEx (getSingleton()->mouseWheelHook, nCode, wParam, lParam);
}
static LRESULT CALLBACK keyboardHookCallback (int nCode, WPARAM wParam, LPARAM lParam)
{
auto& msg = *reinterpret_cast<MSG*> (lParam);
if (nCode == HC_ACTION && wParam == PM_REMOVE && HWNDComponentPeer::offerKeyMessageToJUCEWindow (msg))
{
msg = {};
msg.message = WM_USER;
return 0;
}
return CallNextHookEx (getSingleton()->keyboardHook, nCode, wParam, lParam);
}
HHOOK mouseWheelHook = SetWindowsHookEx (WH_MOUSE,
mouseWheelHookCallback,
(HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(),
GetCurrentThreadId());
HHOOK keyboardHook = SetWindowsHookEx (WH_GETMESSAGE,
keyboardHookCallback,
(HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(),
GetCurrentThreadId());
};
auto WindowsHooks::getSingleton() -> std::shared_ptr<Hooks>
{
auto& weak = Hooks::weak;
if (auto locked = weak.lock())
return locked;
auto strong = std::make_shared<Hooks>();
weak = strong;
return strong;
}
} // namespace juce::detail
#endif
@@ -0,0 +1,46 @@
/*
==============================================================================
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.
==============================================================================
*/
#if JUCE_WINDOWS
namespace juce::detail
{
class WindowsHooks
{
public:
WindowsHooks() = default;
private:
class Hooks;
static std::shared_ptr<Hooks> getSingleton();
std::shared_ptr<Hooks> hooks = getSingleton();
};
} // namespace juce::detail
#endif
@@ -0,0 +1,240 @@
/*
==============================================================================
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
{
namespace X11SymbolHelpers
{
template <typename FuncPtr>
struct SymbolBinding
{
FuncPtr& func;
const char* name;
};
template <typename FuncPtr>
SymbolBinding<FuncPtr> makeSymbolBinding (FuncPtr& func, const char* name)
{
return { func, name };
}
template <typename FuncPtr>
bool loadSymbols (DynamicLibrary& lib, SymbolBinding<FuncPtr> binding)
{
if (auto* func = lib.getFunction (binding.name))
{
binding.func = reinterpret_cast<FuncPtr> (func);
return true;
}
return false;
}
template <typename FuncPtr, typename... Args>
bool loadSymbols (DynamicLibrary& lib1, DynamicLibrary& lib2, SymbolBinding<FuncPtr> binding)
{
return loadSymbols (lib1, binding) || loadSymbols (lib2, binding);
}
template <typename FuncPtr, typename... Args>
bool loadSymbols (DynamicLibrary& lib, SymbolBinding<FuncPtr> binding, Args... args)
{
return loadSymbols (lib, binding) && loadSymbols (lib, args...);
}
template <typename FuncPtr, typename... Args>
bool loadSymbols (DynamicLibrary& lib1, DynamicLibrary& lib2, SymbolBinding<FuncPtr> binding, Args... args)
{
return loadSymbols (lib1, lib2, binding) && loadSymbols (lib1, lib2, args...);
}
}
//==============================================================================
bool X11Symbols::loadAllSymbols()
{
using namespace X11SymbolHelpers;
if (! loadSymbols (xLib, xextLib,
makeSymbolBinding (xAllocClassHint, "XAllocClassHint"),
makeSymbolBinding (xAllocSizeHints, "XAllocSizeHints"),
makeSymbolBinding (xAllocWMHints, "XAllocWMHints"),
makeSymbolBinding (xBitmapBitOrder, "XBitmapBitOrder"),
makeSymbolBinding (xBitmapUnit, "XBitmapUnit"),
makeSymbolBinding (xChangeActivePointerGrab, "XChangeActivePointerGrab"),
makeSymbolBinding (xChangeProperty, "XChangeProperty"),
makeSymbolBinding (xCheckTypedWindowEvent, "XCheckTypedWindowEvent"),
makeSymbolBinding (xCheckWindowEvent, "XCheckWindowEvent"),
makeSymbolBinding (xClearArea, "XClearArea"),
makeSymbolBinding (xCloseDisplay, "XCloseDisplay"),
makeSymbolBinding (xConnectionNumber, "XConnectionNumber"),
makeSymbolBinding (xConvertSelection, "XConvertSelection"),
makeSymbolBinding (xCreateColormap, "XCreateColormap"),
makeSymbolBinding (xCreateFontCursor, "XCreateFontCursor"),
makeSymbolBinding (xCreateGC, "XCreateGC"),
makeSymbolBinding (xCreateImage, "XCreateImage"),
makeSymbolBinding (xCreatePixmap, "XCreatePixmap"),
makeSymbolBinding (xCreatePixmapCursor, "XCreatePixmapCursor"),
makeSymbolBinding (xCreatePixmapFromBitmapData, "XCreatePixmapFromBitmapData"),
makeSymbolBinding (xCreateWindow, "XCreateWindow"),
makeSymbolBinding (xDefaultRootWindow, "XDefaultRootWindow"),
makeSymbolBinding (xDefaultScreen, "XDefaultScreen"),
makeSymbolBinding (xDefaultScreenOfDisplay, "XDefaultScreenOfDisplay"),
makeSymbolBinding (xDefaultVisual, "XDefaultVisual"),
makeSymbolBinding (xDefineCursor, "XDefineCursor"),
makeSymbolBinding (xDeleteContext, "XDeleteContext"),
makeSymbolBinding (xDeleteProperty, "XDeleteProperty"),
makeSymbolBinding (xDestroyImage, "XDestroyImage"),
makeSymbolBinding (xDestroyWindow, "XDestroyWindow"),
makeSymbolBinding (xDisplayHeight, "XDisplayHeight"),
makeSymbolBinding (xDisplayHeightMM, "XDisplayHeightMM"),
makeSymbolBinding (xDisplayWidth, "XDisplayWidth"),
makeSymbolBinding (xDisplayWidthMM, "XDisplayWidthMM"),
makeSymbolBinding (xEventsQueued, "XEventsQueued"),
makeSymbolBinding (xFindContext, "XFindContext"),
makeSymbolBinding (xFlush, "XFlush"),
makeSymbolBinding (xFree, "XFree"),
makeSymbolBinding (xFreeCursor, "XFreeCursor"),
makeSymbolBinding (xFreeColormap, "XFreeColormap"),
makeSymbolBinding (xFreeGC, "XFreeGC"),
makeSymbolBinding (xFreeModifiermap, "XFreeModifiermap"),
makeSymbolBinding (xFreePixmap, "XFreePixmap"),
makeSymbolBinding (xGetAtomName, "XGetAtomName"),
makeSymbolBinding (xGetErrorDatabaseText, "XGetErrorDatabaseText"),
makeSymbolBinding (xGetErrorText, "XGetErrorText"),
makeSymbolBinding (xGetGeometry, "XGetGeometry"),
makeSymbolBinding (xGetImage, "XGetImage"),
makeSymbolBinding (xGetInputFocus, "XGetInputFocus"),
makeSymbolBinding (xGetModifierMapping, "XGetModifierMapping"),
makeSymbolBinding (xGetPointerMapping, "XGetPointerMapping"),
makeSymbolBinding (xGetSelectionOwner, "XGetSelectionOwner"),
makeSymbolBinding (xGetVisualInfo, "XGetVisualInfo"),
makeSymbolBinding (xGetWMHints, "XGetWMHints"),
makeSymbolBinding (xGetWindowAttributes, "XGetWindowAttributes"),
makeSymbolBinding (xGetWindowProperty, "XGetWindowProperty"),
makeSymbolBinding (xGrabPointer, "XGrabPointer"),
makeSymbolBinding (xGrabServer, "XGrabServer"),
makeSymbolBinding (xImageByteOrder, "XImageByteOrder"),
makeSymbolBinding (xInitImage, "XInitImage"),
makeSymbolBinding (xInitThreads, "XInitThreads"),
makeSymbolBinding (xInstallColormap, "XInstallColormap"),
makeSymbolBinding (xInternAtom, "XInternAtom"),
makeSymbolBinding (xkbKeycodeToKeysym, "XkbKeycodeToKeysym"),
makeSymbolBinding (xKeysymToKeycode, "XKeysymToKeycode"),
makeSymbolBinding (xListProperties, "XListProperties"),
makeSymbolBinding (xLockDisplay, "XLockDisplay"),
makeSymbolBinding (xLookupString, "XLookupString"),
makeSymbolBinding (xMapRaised, "XMapRaised"),
makeSymbolBinding (xMapWindow, "XMapWindow"),
makeSymbolBinding (xMoveResizeWindow, "XMoveResizeWindow"),
makeSymbolBinding (xNextEvent, "XNextEvent"),
makeSymbolBinding (xOpenDisplay, "XOpenDisplay"),
makeSymbolBinding (xPeekEvent, "XPeekEvent"),
makeSymbolBinding (xPending, "XPending"),
makeSymbolBinding (xPutImage, "XPutImage"),
makeSymbolBinding (xPutPixel, "XPutPixel"),
makeSymbolBinding (xQueryBestCursor, "XQueryBestCursor"),
makeSymbolBinding (xQueryExtension, "XQueryExtension"),
makeSymbolBinding (xQueryPointer, "XQueryPointer"),
makeSymbolBinding (xQueryTree, "XQueryTree"),
makeSymbolBinding (xRefreshKeyboardMapping, "XRefreshKeyboardMapping"),
makeSymbolBinding (xReparentWindow, "XReparentWindow"),
makeSymbolBinding (xResizeWindow, "XResizeWindow"),
makeSymbolBinding (xRestackWindows, "XRestackWindows"),
makeSymbolBinding (xRootWindow, "XRootWindow"),
makeSymbolBinding (xSaveContext, "XSaveContext"),
makeSymbolBinding (xScreenCount, "XScreenCount"),
makeSymbolBinding (xScreenNumberOfScreen, "XScreenNumberOfScreen"),
makeSymbolBinding (xSelectInput, "XSelectInput"),
makeSymbolBinding (xSendEvent, "XSendEvent"),
makeSymbolBinding (xSetClassHint, "XSetClassHint"),
makeSymbolBinding (xSetErrorHandler, "XSetErrorHandler"),
makeSymbolBinding (xSetIOErrorHandler, "XSetIOErrorHandler"),
makeSymbolBinding (xSetInputFocus, "XSetInputFocus"),
makeSymbolBinding (xSetSelectionOwner, "XSetSelectionOwner"),
makeSymbolBinding (xSetWMHints, "XSetWMHints"),
makeSymbolBinding (xSetWMIconName, "XSetWMIconName"),
makeSymbolBinding (xSetWMName, "XSetWMName"),
makeSymbolBinding (xSetWMNormalHints, "XSetWMNormalHints"),
makeSymbolBinding (xStringListToTextProperty, "XStringListToTextProperty"),
makeSymbolBinding (xSync, "XSync"),
makeSymbolBinding (xSynchronize, "XSynchronize"),
makeSymbolBinding (xTranslateCoordinates, "XTranslateCoordinates"),
makeSymbolBinding (xrmUniqueQuark, "XrmUniqueQuark"),
makeSymbolBinding (xUngrabPointer, "XUngrabPointer"),
makeSymbolBinding (xUngrabServer, "XUngrabServer"),
makeSymbolBinding (xUnlockDisplay, "XUnlockDisplay"),
makeSymbolBinding (xUnmapWindow, "XUnmapWindow"),
makeSymbolBinding (xutf8TextListToTextProperty, "Xutf8TextListToTextProperty"),
makeSymbolBinding (xWarpPointer, "XWarpPointer")))
return false;
#if JUCE_USE_XCURSOR
loadSymbols (xcursorLib,
makeSymbolBinding (xcursorImageCreate, "XcursorImageCreate"),
makeSymbolBinding (xcursorImageLoadCursor, "XcursorImageLoadCursor"),
makeSymbolBinding (xcursorImageDestroy, "XcursorImageDestroy"));
#endif
#if JUCE_USE_XINERAMA
loadSymbols (xineramaLib,
makeSymbolBinding (xineramaIsActive, "XineramaIsActive"),
makeSymbolBinding (xineramaQueryScreens, "XineramaQueryScreens"));
#endif
#if JUCE_USE_XRENDER
loadSymbols (xrenderLib,
makeSymbolBinding (xRenderQueryVersion, "XRenderQueryVersion"),
makeSymbolBinding (xRenderFindStandardFormat, "XRenderFindStandardFormat"),
makeSymbolBinding (xRenderFindFormat, "XRenderFindFormat"),
makeSymbolBinding (xRenderFindVisualFormat, "XRenderFindVisualFormat"));
#endif
#if JUCE_USE_XRANDR
loadSymbols (xrandrLib,
makeSymbolBinding (xRRGetScreenResources, "XRRGetScreenResources"),
makeSymbolBinding (xRRFreeScreenResources, "XRRFreeScreenResources"),
makeSymbolBinding (xRRGetOutputInfo, "XRRGetOutputInfo"),
makeSymbolBinding (xRRFreeOutputInfo, "XRRFreeOutputInfo"),
makeSymbolBinding (xRRGetCrtcInfo, "XRRGetCrtcInfo"),
makeSymbolBinding (xRRFreeCrtcInfo, "XRRFreeCrtcInfo"),
makeSymbolBinding (xRRGetOutputPrimary, "XRRGetOutputPrimary"));
#endif
#if JUCE_USE_XSHM
loadSymbols (xLib, xextLib,
makeSymbolBinding (xShmAttach, "XShmAttach"),
makeSymbolBinding (xShmCreateImage, "XShmCreateImage"),
makeSymbolBinding (xShmDetach, "XShmDetach"),
makeSymbolBinding (xShmGetEventBase, "XShmGetEventBase"),
makeSymbolBinding (xShmPutImage, "XShmPutImage"),
makeSymbolBinding (xShmQueryVersion, "XShmQueryVersion"));
#endif
return true;
}
//==============================================================================
JUCE_IMPLEMENT_SINGLETON (X11Symbols)
} // namespace juce
@@ -0,0 +1,620 @@
/*
==============================================================================
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
{
//==============================================================================
namespace ReturnHelpers
{
template <typename Type>
Type returnDefaultConstructedAnyType() { return {}; }
template <>
inline void returnDefaultConstructedAnyType<void>() {}
}
#define JUCE_GENERATE_FUNCTION_WITH_DEFAULT(functionName, objectName, args, returnType) \
using functionName = returnType (*) args; \
functionName objectName = [] args -> returnType { return ReturnHelpers::returnDefaultConstructedAnyType<returnType>(); };
//==============================================================================
class JUCE_API X11Symbols
{
public:
//==============================================================================
bool loadAllSymbols();
//==============================================================================
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XAllocClassHint, xAllocClassHint,
(),
XClassHint*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XAllocSizeHints, xAllocSizeHints,
(),
XSizeHints*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XAllocWMHints, xAllocWMHints,
(),
XWMHints*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XBitmapBitOrder, xBitmapBitOrder,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XBitmapUnit, xBitmapUnit,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XChangeActivePointerGrab, xChangeActivePointerGrab,
(::Display*, unsigned int, Cursor, ::Time),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XChangeProperty, xChangeProperty,
(::Display*, ::Window, Atom, Atom, int, int, const unsigned char*, int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCheckTypedWindowEvent, xCheckTypedWindowEvent,
(::Display*, ::Window, int, XEvent*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCheckWindowEvent, xCheckWindowEvent,
(::Display*, ::Window, long, XEvent*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XClearArea, xClearArea,
(::Display*, ::Window, int, int, unsigned int, unsigned int, Bool),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCloseDisplay, xCloseDisplay,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XConnectionNumber, xConnectionNumber,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XConvertSelection, xConvertSelection,
(::Display*, Atom, Atom, Atom, ::Window, ::Time),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreateColormap, xCreateColormap,
(::Display*, ::Window, Visual*, int),
Colormap)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreateFontCursor, xCreateFontCursor,
(::Display*, unsigned int),
Cursor)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreateGC, xCreateGC,
(::Display*, ::Drawable, unsigned long, XGCValues*),
GC)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreateImage, xCreateImage,
(::Display*, Visual*, unsigned int, int, int, const char*, unsigned int, unsigned int, int, int),
XImage*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreatePixmap, xCreatePixmap,
(::Display*, ::Drawable, unsigned int, unsigned int, unsigned int),
Pixmap)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreatePixmapCursor, xCreatePixmapCursor,
(::Display*, Pixmap, Pixmap, XColor*, XColor*, unsigned int, unsigned int),
Cursor)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreatePixmapFromBitmapData, xCreatePixmapFromBitmapData,
(::Display*, ::Drawable, const char*, unsigned int, unsigned int, unsigned long, unsigned long, unsigned int),
Pixmap)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XCreateWindow, xCreateWindow,
(::Display*, ::Window, int, int, unsigned int, unsigned int, unsigned int, int, unsigned int, Visual*, unsigned long, XSetWindowAttributes*),
::Window)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDefaultRootWindow, xDefaultRootWindow,
(::Display*),
::Window)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDefaultScreen, xDefaultScreen,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDefaultScreenOfDisplay, xDefaultScreenOfDisplay,
(::Display*),
Screen*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDefaultVisual, xDefaultVisual,
(::Display*, int),
Visual*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDefineCursor, xDefineCursor,
(::Display*, ::Window, Cursor),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDeleteContext, xDeleteContext,
(::Display*, XID, XContext),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDeleteProperty, xDeleteProperty,
(::Display*, Window, Atom),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDestroyImage, xDestroyImage,
(XImage*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDestroyWindow, xDestroyWindow,
(::Display*, ::Window),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDisplayHeight, xDisplayHeight,
(::Display*, int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDisplayHeightMM, xDisplayHeightMM,
(::Display*, int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDisplayWidth, xDisplayWidth,
(::Display*, int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XDisplayWidthMM, xDisplayWidthMM,
(::Display*, int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XEventsQueued, xEventsQueued,
(::Display*, int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFindContext, xFindContext,
(::Display*, XID, XContext, XPointer*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFlush, xFlush,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFree, xFree,
(void*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFreeCursor, xFreeCursor,
(::Display*, Cursor),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFreeColormap ,xFreeColormap,
(::Display*, Colormap),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFreeGC, xFreeGC,
(::Display*, GC),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFreeModifiermap, xFreeModifiermap,
(XModifierKeymap*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XFreePixmap, xFreePixmap,
(::Display*, Pixmap),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetAtomName, xGetAtomName,
(::Display*, Atom),
char*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetErrorDatabaseText, xGetErrorDatabaseText,
(::Display*, const char*, const char*, const char*, const char*, int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetErrorText, xGetErrorText,
(::Display*, int, const char*, int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetGeometry, xGetGeometry,
(::Display*, ::Drawable, ::Window*, int*, int*, unsigned int*, unsigned int*, unsigned int*, unsigned int*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetImage, xGetImage,
(::Display*, ::Drawable, int, int, unsigned int, unsigned int, unsigned long, int),
XImage*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetInputFocus, xGetInputFocus,
(::Display*, ::Window*, int*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetModifierMapping, xGetModifierMapping,
(::Display*),
XModifierKeymap*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetPointerMapping, xGetPointerMapping,
(::Display*, unsigned char[], int),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetSelectionOwner, xGetSelectionOwner,
(::Display*, Atom),
::Window)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetVisualInfo, xGetVisualInfo,
(::Display*, long, XVisualInfo*, int*),
XVisualInfo*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetWMHints, xGetWMHints,
(::Display*, ::Window),
XWMHints*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetWindowAttributes, xGetWindowAttributes,
(::Display*, ::Window, XWindowAttributes*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGetWindowProperty, xGetWindowProperty,
(::Display*, ::Window, Atom, long, long, Bool, Atom, Atom*, int*, unsigned long*, unsigned long*, unsigned char**),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGrabPointer, xGrabPointer,
(::Display*, ::Window, Bool, unsigned int, int, int, ::Window, Cursor, ::Time),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XGrabServer, xGrabServer,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XImageByteOrder, xImageByteOrder,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XInitImage, xInitImage,
(XImage*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XInitThreads, xInitThreads,
(),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XInstallColormap, xInstallColormap,
(::Display*, Colormap),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XInternAtom, xInternAtom,
(::Display*, const char*, Bool),
Atom)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XkbKeycodeToKeysym, xkbKeycodeToKeysym,
(::Display*, KeyCode, unsigned int, unsigned int),
KeySym)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XKeysymToKeycode, xKeysymToKeycode,
(::Display*, KeySym),
KeyCode)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XListProperties, xListProperties,
(::Display*, Window, int*),
Atom*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XLockDisplay, xLockDisplay,
(::Display*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XLookupString, xLookupString,
(XKeyEvent*, const char*, int, KeySym*, XComposeStatus*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XMapRaised, xMapRaised,
(::Display*, ::Window),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XMapWindow, xMapWindow,
(::Display*, ::Window),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XMoveResizeWindow, xMoveResizeWindow,
(::Display*, ::Window, int, int, unsigned int, unsigned int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XNextEvent, xNextEvent,
(::Display*, XEvent*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XOpenDisplay, xOpenDisplay,
(const char*),
::Display*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XPeekEvent, xPeekEvent,
(::Display*, XEvent*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XPending, xPending,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XPutImage, xPutImage,
(::Display*, ::Drawable, GC, XImage*, int, int, int, int, unsigned int, unsigned int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XPutPixel, xPutPixel,
(XImage*, int, int, unsigned long),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XQueryBestCursor, xQueryBestCursor,
(::Display*, ::Drawable, unsigned int, unsigned int, unsigned int*, unsigned int*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XQueryExtension, xQueryExtension,
(::Display*, const char*, int*, int*, int*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XQueryPointer, xQueryPointer,
(::Display*, ::Window, ::Window*, ::Window*, int*, int*, int*, int*, unsigned int*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XQueryTree, xQueryTree,
(::Display*, ::Window, ::Window*, ::Window*, ::Window**, unsigned int*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRefreshKeyboardMapping, xRefreshKeyboardMapping,
(XMappingEvent*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XReparentWindow, xReparentWindow,
(::Display*, ::Window, ::Window, int, int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XResizeWindow, xResizeWindow,
(::Display*, Window, unsigned int, unsigned int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRestackWindows, xRestackWindows,
(::Display*, ::Window[], int),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRootWindow, xRootWindow,
(::Display*, int),
::Window)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSaveContext, xSaveContext,
(::Display*, XID, XContext, XPointer),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XScreenCount, xScreenCount,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XScreenNumberOfScreen, xScreenNumberOfScreen,
(Screen*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSelectInput, xSelectInput,
(::Display*, ::Window, long),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSendEvent, xSendEvent,
(::Display*, ::Window, Bool, long, XEvent*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetClassHint, xSetClassHint,
(::Display*, ::Window, XClassHint*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetErrorHandler, xSetErrorHandler,
(XErrorHandler),
XErrorHandler)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetIOErrorHandler, xSetIOErrorHandler,
(XIOErrorHandler),
XIOErrorHandler)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetInputFocus, xSetInputFocus,
(::Display*, ::Window, int, ::Time),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetSelectionOwner, xSetSelectionOwner,
(::Display*, Atom, ::Window, ::Time),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetWMHints, xSetWMHints,
(::Display*, ::Window, XWMHints*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetWMIconName, xSetWMIconName,
(::Display*, ::Window, XTextProperty*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetWMName, xSetWMName,
(::Display*, ::Window, XTextProperty*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSetWMNormalHints, xSetWMNormalHints,
(::Display*, ::Window, XSizeHints*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XStringListToTextProperty, xStringListToTextProperty,
(char**, int, XTextProperty*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (Xutf8TextListToTextProperty, xutf8TextListToTextProperty,
(::Display*, char**, int, XICCEncodingStyle, XTextProperty*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSync, xSync,
(::Display*, Bool),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XSynchronize, xSynchronize,
(::Display*, Bool),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XTranslateCoordinates, xTranslateCoordinates,
(::Display*, ::Window, ::Window, int, int, int*, int*, ::Window*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XrmUniqueQuark, xrmUniqueQuark,
(),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XUngrabPointer, xUngrabPointer,
(::Display*, ::Time),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XUngrabServer, xUngrabServer,
(::Display*),
int)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XUnlockDisplay, xUnlockDisplay,
(::Display*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XUnmapWindow, xUnmapWindow,
(::Display*, ::Window),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XWarpPointer, xWarpPointer,
(::Display*, ::Window, ::Window, int, int, unsigned int, unsigned int, int, int),
void)
#if JUCE_USE_XCURSOR
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XcursorImageCreate, xcursorImageCreate,
(int, int),
XcursorImage*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XcursorImageLoadCursor, xcursorImageLoadCursor,
(::Display*, XcursorImage*),
Cursor)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XcursorImageDestroy, xcursorImageDestroy,
(XcursorImage*),
void)
#endif
#if JUCE_USE_XINERAMA
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XineramaIsActive, xineramaIsActive,
(::Display*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XineramaQueryScreens, xineramaQueryScreens,
(::Display*, int*),
XineramaScreenInfo*)
#endif
#if JUCE_USE_XRENDER
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRenderQueryVersion, xRenderQueryVersion,
(::Display*, int*, int*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRenderFindStandardFormat, xRenderFindStandardFormat,
(Display*, int),
XRenderPictFormat*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRenderFindFormat, xRenderFindFormat,
(Display*, unsigned long, XRenderPictFormat*, int),
XRenderPictFormat*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRenderFindVisualFormat, xRenderFindVisualFormat,
(Display*, Visual*),
XRenderPictFormat*)
#endif
#if JUCE_USE_XRANDR
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRGetScreenResources, xRRGetScreenResources,
(::Display*, Window),
XRRScreenResources*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRFreeScreenResources, xRRFreeScreenResources,
(XRRScreenResources*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRGetOutputInfo, xRRGetOutputInfo,
(::Display*, XRRScreenResources*, RROutput),
XRROutputInfo*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRFreeOutputInfo, xRRFreeOutputInfo,
(XRROutputInfo*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRGetCrtcInfo, xRRGetCrtcInfo,
(::Display*, XRRScreenResources*, RRCrtc),
XRRCrtcInfo*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRFreeCrtcInfo, xRRFreeCrtcInfo,
(XRRCrtcInfo*),
void)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XRRGetOutputPrimary, xRRGetOutputPrimary,
(::Display*, ::Window),
RROutput)
#endif
#if JUCE_USE_XSHM
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmAttach, xShmAttach,
(::Display*, XShmSegmentInfo*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmCreateImage, xShmCreateImage,
(::Display*, Visual*, unsigned int, int, const char*, XShmSegmentInfo*, unsigned int, unsigned int),
XImage*)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmDetach, xShmDetach,
(::Display*, XShmSegmentInfo*),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmGetEventBase, xShmGetEventBase,
(::Display*),
Status)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmPutImage, xShmPutImage,
(::Display*, ::Drawable, GC, XImage*, int, int, int, int, unsigned int, unsigned int, bool),
Bool)
JUCE_GENERATE_FUNCTION_WITH_DEFAULT (XShmQueryVersion, xShmQueryVersion,
(::Display*, int*, int*, Bool*),
Bool)
#endif
//==============================================================================
JUCE_DECLARE_SINGLETON (X11Symbols, false)
private:
X11Symbols() = default;
~X11Symbols()
{
clearSingletonInstance();
}
//==============================================================================
DynamicLibrary xLib { "libX11.so.6" }, xextLib { "libXext.so.6" };
#if JUCE_USE_XCURSOR
DynamicLibrary xcursorLib { "libXcursor.so.1" };
#endif
#if JUCE_USE_XINERAMA
DynamicLibrary xineramaLib { "libXinerama.so.1" };
#endif
#if JUCE_USE_XRENDER
DynamicLibrary xrenderLib { "libXrender.so.1" };
#endif
#if JUCE_USE_XRANDR
DynamicLibrary xrandrLib { "libXrandr.so.2" };
#endif
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (X11Symbols)
};
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
/*
==============================================================================
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
{
//==============================================================================
namespace XWindowSystemUtilities
{
//==============================================================================
/** A handy struct that uses XLockDisplay and XUnlockDisplay to lock the X server
using RAII.
@tags{GUI}
*/
struct ScopedXLock
{
ScopedXLock();
~ScopedXLock();
};
//==============================================================================
/** Gets a specified window property and stores its associated data, freeing it
on deletion.
@tags{GUI}
*/
struct GetXProperty
{
GetXProperty (::Display* display, ::Window windowH, Atom property,
long offset, long length, bool shouldDelete, Atom requestedType);
~GetXProperty();
bool success = false;
unsigned char* data = nullptr;
unsigned long numItems = 0, bytesLeft = 0;
Atom actualType;
int actualFormat = -1;
};
//==============================================================================
/** Initialises and stores some atoms for the display.
@tags{GUI}
*/
struct Atoms
{
enum ProtocolItems
{
TAKE_FOCUS = 0,
DELETE_WINDOW = 1,
PING = 2
};
Atoms() = default;
explicit Atoms (::Display*);
static Atom getIfExists (::Display*, const char* name);
static Atom getCreating (::Display*, const char* name);
static String getName (::Display*, Atom);
static bool isMimeTypeFile (::Display*, Atom);
static constexpr unsigned long DndVersion = 3;
Atom protocols, protocolList[3], changeState, state, userTime, activeWin, pid, windowType, windowState, windowStateHidden,
XdndAware, XdndEnter, XdndLeave, XdndPosition, XdndStatus, XdndDrop, XdndFinished, XdndSelection,
XdndTypeList, XdndActionList, XdndActionDescription, XdndActionCopy, XdndActionPrivate,
XembedMsgType, XembedInfo, allowedActions[5], allowedMimeTypes[4], utf8String, clipboard, targets;
};
//==============================================================================
/** Represents a setting according to the XSETTINGS specification.
@tags{GUI}
*/
struct XSetting
{
enum class Type
{
integer,
string,
colour,
invalid
};
XSetting() = default;
XSetting (const String& n, int v) : name (n), type (Type::integer), integerValue (v) {}
XSetting (const String& n, const String& v) : name (n), type (Type::string), stringValue (v) {}
XSetting (const String& n, const Colour& v) : name (n), type (Type::colour), colourValue (v) {}
bool isValid() const noexcept { return type != Type::invalid; }
String name;
Type type = Type::invalid;
int integerValue = -1;
String stringValue;
Colour colourValue;
};
/** Parses and stores the X11 settings for a display according to the XSETTINGS
specification.
@tags{GUI}
*/
class XSettings
{
public:
static std::unique_ptr<XSettings> createXSettings (::Display*);
//==============================================================================
void update();
::Window getSettingsWindow() const noexcept { return settingsWindow; }
XSetting getSetting (const String& settingName) const;
//==============================================================================
struct Listener
{
virtual ~Listener() = default;
virtual void settingChanged (const XSetting& settingThatHasChanged) = 0;
};
void addListener (Listener* listenerToAdd) { listeners.add (listenerToAdd); }
void removeListener (Listener* listenerToRemove) { listeners.remove (listenerToRemove); }
private:
::Display* display = nullptr;
::Window settingsWindow = None;
Atom settingsAtom;
int lastUpdateSerial = -1;
std::unordered_map<String, XSetting> settings;
ListenerList<Listener> listeners;
XSettings (::Display*, Atom, ::Window);
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XSettings)
};
}
//==============================================================================
class LinuxComponentPeer;
class XWindowSystem : public DeletedAtShutdown
{
public:
//==============================================================================
::Window createWindow (::Window parentWindow, LinuxComponentPeer*) const;
void destroyWindow (::Window);
void setTitle (::Window, const String&) const;
void setIcon (::Window , const Image&) const;
void setVisible (::Window, bool shouldBeVisible) const;
void setBounds (::Window, Rectangle<int>, bool fullScreen) const;
void updateConstraints (::Window) const;
ComponentPeer::OptionalBorderSize getBorderSize (::Window) const;
Rectangle<int> getWindowBounds (::Window, ::Window parentWindow);
Point<int> getPhysicalParentScreenPosition() const;
bool contains (::Window, Point<int> localPos) const;
void setMinimised (::Window, bool shouldBeMinimised) const;
bool isMinimised (::Window) const;
void setMaximised (::Window, bool shouldBeMinimised) const;
void toFront (::Window, bool makeActive) const;
void toBehind (::Window, ::Window otherWindow) const;
bool isFocused (::Window) const;
bool grabFocus (::Window) const;
bool canUseSemiTransparentWindows() const;
bool canUseARGBImages() const;
bool isDarkModeActive() const;
int getNumPaintsPendingForWindow (::Window);
void processPendingPaintsForWindow (::Window);
void addPendingPaintForWindow (::Window);
void removePendingPaintForWindow (::Window);
Image createImage (bool isSemiTransparentWindow, int width, int height, bool argb) const;
void blitToWindow (::Window, Image, Rectangle<int> destinationRect, Rectangle<int> totalRect) const;
void setScreenSaverEnabled (bool enabled) const;
Point<float> getCurrentMousePosition() const;
void setMousePosition (Point<float> pos) const;
Cursor createCustomMouseCursorInfo (const Image&, Point<int> hotspot) const;
void deleteMouseCursor (Cursor cursorHandle) const;
Cursor createStandardMouseCursor (MouseCursor::StandardCursorType) const;
void showCursor (::Window, Cursor cursorHandle) const;
bool isKeyCurrentlyDown (int keyCode) const;
ModifierKeys getNativeRealtimeModifiers() const;
Array<Displays::Display> findDisplays (float masterScale) const;
::Window createKeyProxy (::Window);
void deleteKeyProxy (::Window) const;
bool externalDragFileInit (LinuxComponentPeer*, const StringArray& files, bool canMove, std::function<void()>&& callback) const;
bool externalDragTextInit (LinuxComponentPeer*, const String& text, std::function<void()>&& callback) const;
void copyTextToClipboard (const String&);
String getTextFromClipboard() const;
String getLocalClipboardContent() const noexcept { return localClipboardContent; }
::Display* getDisplay() const noexcept { return display; }
const XWindowSystemUtilities::Atoms& getAtoms() const noexcept { return atoms; }
XWindowSystemUtilities::XSettings* getXSettings() const noexcept { return xSettings.get(); }
bool isX11Available() const noexcept { return xIsAvailable; }
void startHostManagedResize (::Window window,
ResizableBorderComponent::Zone zone);
static String getWindowScalingFactorSettingName() { return "Gdk/WindowScalingFactor"; }
static String getThemeNameSettingName() { return "Net/ThemeName"; }
//==============================================================================
void handleWindowMessage (LinuxComponentPeer*, XEvent&) const;
bool isParentWindowOf (::Window, ::Window possibleChild) const;
//==============================================================================
JUCE_DECLARE_SINGLETON (XWindowSystem, false)
private:
XWindowSystem();
~XWindowSystem();
//==============================================================================
struct VisualAndDepth
{
Visual* visual;
int depth;
};
struct DisplayVisuals
{
explicit DisplayVisuals (::Display*);
VisualAndDepth getBestVisualForWindow (bool) const;
bool isValid() const noexcept;
Visual* visual16Bit = nullptr;
Visual* visual24Bit = nullptr;
Visual* visual32Bit = nullptr;
};
bool initialiseXDisplay();
void destroyXDisplay();
//==============================================================================
::Window getFocusWindow (::Window) const;
bool isFrontWindow (::Window) const;
//==============================================================================
void xchangeProperty (::Window, Atom, Atom, int, const void*, int) const;
void removeWindowDecorations (::Window) const;
void addWindowButtons (::Window, int) const;
void setWindowType (::Window, int) const;
void initialisePointerMap();
void deleteIconPixmaps (::Window) const;
void updateModifierMappings() const;
long getUserTime (::Window) const;
bool isHidden (Window) const;
bool isIconic (Window) const;
void initialiseXSettings();
//==============================================================================
void handleKeyPressEvent (LinuxComponentPeer*, XKeyEvent&) const;
void handleKeyReleaseEvent (LinuxComponentPeer*, const XKeyEvent&) const;
void handleWheelEvent (LinuxComponentPeer*, const XButtonPressedEvent&, float) const;
void handleButtonPressEvent (LinuxComponentPeer*, const XButtonPressedEvent&, int) const;
void handleButtonPressEvent (LinuxComponentPeer*, const XButtonPressedEvent&) const;
void handleButtonReleaseEvent (LinuxComponentPeer*, const XButtonReleasedEvent&) const;
void handleMotionNotifyEvent (LinuxComponentPeer*, const XPointerMovedEvent&) const;
void handleEnterNotifyEvent (LinuxComponentPeer*, const XEnterWindowEvent&) const;
void handleLeaveNotifyEvent (LinuxComponentPeer*, const XLeaveWindowEvent&) const;
void handleFocusInEvent (LinuxComponentPeer*) const;
void handleFocusOutEvent (LinuxComponentPeer*) const;
void handleExposeEvent (LinuxComponentPeer*, XExposeEvent&) const;
void handleConfigureNotifyEvent (LinuxComponentPeer*, XConfigureEvent&) const;
void handleGravityNotify (LinuxComponentPeer*) const;
void propertyNotifyEvent (LinuxComponentPeer*, const XPropertyEvent&) const;
void handleMappingNotify (XMappingEvent&) const;
void handleClientMessageEvent (LinuxComponentPeer*, XClientMessageEvent&, XEvent&) const;
void handleXEmbedMessage (LinuxComponentPeer*, XClientMessageEvent&) const;
void dismissBlockingModals (LinuxComponentPeer*) const;
void dismissBlockingModals (LinuxComponentPeer*, const XConfigureEvent&) const;
void updateConstraints (::Window, ComponentPeer&) const;
::Window findTopLevelWindowOf (::Window) const;
static void windowMessageReceive (XEvent&);
//==============================================================================
bool xIsAvailable = false;
XWindowSystemUtilities::Atoms atoms;
::Display* display = nullptr;
std::unique_ptr<DisplayVisuals> displayVisuals;
std::unique_ptr<XWindowSystemUtilities::XSettings> xSettings;
#if JUCE_USE_XSHM
std::map<::Window, int> shmPaintsPendingMap;
#endif
int shmCompletionEvent = 0;
int pointerMap[5] = {};
String localClipboardContent;
Point<int> parentScreenPosition;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XWindowSystem)
};
} // namespace juce