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,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
{
/** An action that can be performed by an accessible UI element.
@tags{Accessibility}
*/
enum class AccessibilityActionType
{
/** Represents a "press" action.
This will be called when the user "clicks" the UI element using an
accessibility client.
*/
press,
/** Represents a "toggle" action.
This will be called when the user toggles the state of a UI element,
for example a toggle button or the selection of a list item.
*/
toggle,
/** Indicates that the UI element has received focus.
This will be called when a UI element receives focus from an accessibility
client, or keyboard focus from the application.
*/
focus,
/** Represents the user showing a contextual menu for a UI element.
This will be called for UI elements which expand and collapse to
show contextual information or menus, or show a popup.
*/
showMenu
};
/** A simple wrapper for building a collection of supported accessibility actions
and corresponding callbacks for a UI element.
Pass one of these when constructing an `AccessibilityHandler` to enable users
to interact with a UI element via the supported actions.
@tags{Accessibility}
*/
class JUCE_API AccessibilityActions
{
public:
/** Constructor.
Creates a default AccessibilityActions object with no action callbacks.
*/
AccessibilityActions() = default;
/** Adds an action.
When the user performs this action with an accessibility client
`actionCallback` will be called.
Returns a reference to itself so that several calls can be chained.
*/
AccessibilityActions& addAction (AccessibilityActionType type,
std::function<void()> actionCallback)
{
actionMap[type] = std::move (actionCallback);
return *this;
}
/** Returns true if the specified action is supported. */
bool contains (AccessibilityActionType type) const
{
return actionMap.find (type) != actionMap.end();
}
/** If an action has been registered for the provided action type, invokes the
action and returns true. Otherwise, returns false.
*/
bool invoke (AccessibilityActionType type) const
{
auto iter = actionMap.find (type);
if (iter == actionMap.end())
return false;
iter->second();
return true;
}
private:
std::map<AccessibilityActionType, std::function<void()>> actionMap;
};
} // namespace juce
@@ -0,0 +1,81 @@
/*
==============================================================================
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 list of events that can be notified to any subscribed accessibility clients.
To post a notification, call `AccessibilityHandler::notifyAccessibilityEvent`
on the associated handler with the appropriate `AccessibilityEvent` type and
listening clients will be notified.
@tags{Accessibility}
*/
enum class AccessibilityEvent
{
/** Indicates that the UI element's value has changed.
This should be called on the handler that implements `AccessibilityValueInterface`
for the UI element that has changed.
*/
valueChanged,
/** Indicates that the title of the UI element has changed.
This should be called on the handler whose title has changed.
*/
titleChanged,
/** Indicates that the structure of the UI elements has changed in a
significant way.
This should be called on the top-level handler whose structure has changed.
*/
structureChanged,
/** Indicates that the selection of a text element has changed.
This should be called on the handler that implements `AccessibilityTextInterface`
for the text element that has changed.
*/
textSelectionChanged,
/** Indicates that the visible text of a text element has changed.
This should be called on the handler that implements `AccessibilityTextInterface`
for the text element that has changed.
*/
textChanged,
/** Indicates that the selection of rows in a list or table has changed.
This should be called on the handler that implements `AccessibilityTableInterface`
for the UI element that has changed.
*/
rowSelectionChanged
};
} // namespace juce
@@ -0,0 +1,71 @@
/*
==============================================================================
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
{
/** The list of available roles for an AccessibilityHandler object.
When creating a custom AccessibilityHandler you should select the role that
best describes the UI element being represented.
@tags{Accessibility}
*/
enum class AccessibilityRole
{
button,
toggleButton,
radioButton,
comboBox,
image,
slider,
label,
staticText,
editableText,
menuItem,
menuBar,
popupMenu,
table,
tableHeader,
column,
row,
cell,
hyperlink,
list,
listItem,
tree,
treeItem,
progressBar,
group,
dialogWindow,
window,
scrollBar,
tooltip,
splashScreen,
ignored,
unspecified
};
} // namespace juce
@@ -0,0 +1,52 @@
/*
==============================================================================
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
{
/** An abstract interface which represents a UI element that supports a cell interface.
This typically represents a single cell inside of a UI element which implements an
AccessibilityTableInterface.
@tags{Accessibility}
*/
class JUCE_API AccessibilityCellInterface
{
public:
/** Destructor. */
virtual ~AccessibilityCellInterface() = default;
/** Returns the indentation level for the cell. */
virtual int getDisclosureLevel() const = 0;
/** Returns the AccessibilityHandler of the table which contains the cell. */
virtual const AccessibilityHandler* getTableHandler() const = 0;
/** Returns a list of the accessibility elements that are disclosed by this element, if any. */
virtual std::vector<const AccessibilityHandler*> getDisclosedRows() const { return {}; }
};
} // namespace juce
@@ -0,0 +1,97 @@
/*
==============================================================================
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
{
/** An abstract interface which represents a UI element that supports a table interface.
Examples of UI elements which typically support a table interface are lists, tables,
and trees.
@tags{Accessibility}
*/
class JUCE_API AccessibilityTableInterface
{
public:
/** Destructor. */
virtual ~AccessibilityTableInterface() = default;
/** Returns the total number of rows in the table. */
virtual int getNumRows() const = 0;
/** Returns the total number of columns in the table. */
virtual int getNumColumns() const = 0;
/** Returns the AccessibilityHandler for one of the cells in the table, or
nullptr if there is no cell at the specified position.
*/
virtual const AccessibilityHandler* getCellHandler (int row, int column) const = 0;
/** Returns the AccessibilityHandler for a row in the table, or nullptr if there is
no row at this index.
The row component should have a child component for each column in the table.
*/
virtual const AccessibilityHandler* getRowHandler (int row) const = 0;
/** Returns the AccessibilityHandler for the header, or nullptr if there is
no header.
If you supply a header, it must have exactly the same number of children
as there are columns in the table.
*/
virtual const AccessibilityHandler* getHeaderHandler() const = 0;
/** A simple span of elements. */
struct Span { int begin, num; };
/** Given the handler of one of the cells in the table, returns the rows covered
by that cell, or null if the cell does not exist in the table.
This function replaces the getRowIndex and getRowSpan
functions from AccessibilityCellInterface. Most of the time, it's easier for the
table itself to keep track of cell locations, than to delegate to the individual
cells.
*/
virtual Optional<Span> getRowSpan (const AccessibilityHandler&) const = 0;
/** Given the handler of one of the cells in the table, returns the columns covered
by that cell, or null if the cell does not exist in the table.
This function replaces the getColumnIndex and getColumnSpan
functions from AccessibilityCellInterface. Most of the time, it's easier for the
table itself to keep track of cell locations, than to delegate to the individual
cells.
*/
virtual Optional<Span> getColumnSpan (const AccessibilityHandler&) const = 0;
/** Attempts to scroll the table (if necessary) so that the cell with the given handler
is visible.
*/
virtual void showCell (const AccessibilityHandler&) const = 0;
};
} // namespace juce
@@ -0,0 +1,84 @@
/*
==============================================================================
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
{
/** An abstract interface which represents a UI element that supports a text interface.
A UI element can use this interface to provide extended textual information which
cannot be conveyed using just the title, description, and help text properties of
AccessibilityHandler. This is typically for text that an accessibility client might
want to read line-by-line, or provide text selection and input for.
@tags{Accessibility}
*/
class JUCE_API AccessibilityTextInterface
{
public:
/** Destructor. */
virtual ~AccessibilityTextInterface() = default;
/** Returns true if the text being displayed is protected and should not be
exposed to the user, for example a password entry field.
*/
virtual bool isDisplayingProtectedText() const = 0;
/** Returns true if the text being displayed is read-only or false if editable. */
virtual bool isReadOnly() const = 0;
/** Returns the total number of characters in the text element. */
virtual int getTotalNumCharacters() const = 0;
/** Returns the range of characters that are currently selected, or an empty
range if nothing is selected.
*/
virtual Range<int> getSelection() const = 0;
/** Selects a section of the text. */
virtual void setSelection (Range<int> newRange) = 0;
/** Gets the current text insertion position, if supported. */
virtual int getTextInsertionOffset() const = 0;
/** Returns a section of text. */
virtual String getText (Range<int> range) const = 0;
/** Returns the full text. */
String getAllText() const { return getText ({ 0, getTotalNumCharacters() }); }
/** Replaces the text with a new string. */
virtual void setText (const String& newText) = 0;
/** Returns the bounding box in screen coordinates for a range of text.
As the range may span multiple lines, this method returns a RectangleList.
*/
virtual RectangleList<int> getTextBounds (Range<int> textRange) const = 0;
/** Returns the index of the character at a given position in screen coordinates. */
virtual int getOffsetAtPoint (Point<int> point) const = 0;
};
} // namespace juce
@@ -0,0 +1,222 @@
/*
==============================================================================
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
{
/** An abstract interface representing the value of an accessibility element.
Values should be used when information needs to be conveyed which cannot
be represented by the accessibility element's label alone. For example, a
gain slider with the label "Gain" needs to also provide a value for its
position whereas a "Save" button does not.
This class allows for full control over the value text/numeric conversion,
ranged, and read-only properties but in most cases you'll want to use one
of the derived classes below which handle some of this for you.
@see AccessibilityTextValueInterface, AccessibilityNumericValueInterface,
AccessibilityRangedNumericValueInterface
@tags{Accessibility}
*/
class JUCE_API AccessibilityValueInterface
{
public:
/** Destructor. */
virtual ~AccessibilityValueInterface() = default;
/** Returns true if the value is read-only and cannot be modified by an
accessibility client.
@see setValue, setValueAsString
*/
virtual bool isReadOnly() const = 0;
/** Returns the current value as a double. */
virtual double getCurrentValue() const = 0;
/** Returns the current value as a String. */
virtual String getCurrentValueAsString() const = 0;
/** Sets the current value to a new double value. */
virtual void setValue (double newValue) = 0;
/** Sets the current value to a new String value. */
virtual void setValueAsString (const String& newValue) = 0;
/** Represents the range of this value, if supported.
Return one of these from the `getRange()` method, providing a minimum,
maximum, and interval value for the range to indicate that this is a
ranged value.
The default state is an "invalid" range, indicating that the accessibility
element does not support ranged values.
@see AccessibilityRangedNumericValueInterface
@tags{Accessibility}
*/
class JUCE_API AccessibleValueRange
{
public:
/** Constructor.
Creates a default, "invalid" range that can be returned from
`AccessibilityValueInterface::getRange()` to indicate that the value
interface does not support ranged values.
*/
AccessibleValueRange() = default;
/** The minimum and maximum values for this range, inclusive. */
struct JUCE_API MinAndMax { double min, max; };
/** Constructor.
Creates a valid AccessibleValueRange with the provided minimum, maximum,
and interval values.
*/
AccessibleValueRange (MinAndMax valueRange, double interval)
: valid (true),
range (valueRange),
stepSize (interval)
{
jassert (range.min < range.max);
}
/** Returns true if this represents a valid range. */
bool isValid() const noexcept { return valid; }
/** Returns the minimum value for this range. */
double getMinimumValue() const noexcept { return range.min; }
/** Returns the maximum value for this range. */
double getMaximumValue() const noexcept { return range.max; }
/** Returns the interval for this range. */
double getInterval() const noexcept { return stepSize; }
private:
bool valid = false;
MinAndMax range {};
double stepSize = 0.0;
};
/** If this is a ranged value, this should return a valid AccessibleValueRange
object representing the supported numerical range.
*/
virtual AccessibleValueRange getRange() const = 0;
};
//==============================================================================
/** A value interface that represents a text value.
@tags{Accessibility}
*/
class JUCE_API AccessibilityTextValueInterface : public AccessibilityValueInterface
{
public:
/** Returns true if the value is read-only and cannot be modified by an
accessibility client.
@see setValueAsString
*/
bool isReadOnly() const override = 0;
/** Returns the current value. */
String getCurrentValueAsString() const override = 0;
/** Sets the current value to a new value. */
void setValueAsString (const String& newValue) override = 0;
/** @internal */
double getCurrentValue() const final { return getCurrentValueAsString().getDoubleValue(); }
/** @internal */
void setValue (double newValue) final { setValueAsString (String (newValue)); }
/** @internal */
AccessibleValueRange getRange() const final { return {}; }
};
//==============================================================================
/** A value interface that represents a non-ranged numeric value.
@tags{Accessibility}
*/
class JUCE_API AccessibilityNumericValueInterface : public AccessibilityValueInterface
{
public:
/** Returns true if the value is read-only and cannot be modified by an
accessibility client.
@see setValue
*/
bool isReadOnly() const override = 0;
/** Returns the current value. */
double getCurrentValue() const override = 0;
/** Sets the current value to a new value. */
void setValue (double newValue) override = 0;
/** @internal */
String getCurrentValueAsString() const final { return String (getCurrentValue()); }
/** @internal */
void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); }
/** @internal */
AccessibleValueRange getRange() const final { return {}; }
};
//==============================================================================
/** A value interface that represents a ranged numeric value.
@tags{Accessibility}
*/
class JUCE_API AccessibilityRangedNumericValueInterface : public AccessibilityValueInterface
{
public:
/** Returns true if the value is read-only and cannot be modified by an
accessibility client.
@see setValueAsString
*/
bool isReadOnly() const override = 0;
/** Returns the current value. */
double getCurrentValue() const override = 0;
/** Sets the current value to a new value. */
void setValue (double newValue) override = 0;
/** Returns the range. */
AccessibleValueRange getRange() const override = 0;
/** @internal */
String getCurrentValueAsString() const final { return String (getCurrentValue()); }
/** @internal */
void setValueAsString (const String& newValue) final { setValue (newValue.getDoubleValue()); }
};
} // namespace juce
@@ -0,0 +1,403 @@
/*
==============================================================================
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
{
AccessibilityHandler* AccessibilityHandler::currentlyFocusedHandler = nullptr;
class NativeChildHandler
{
public:
static NativeChildHandler& getInstance()
{
static NativeChildHandler instance;
return instance;
}
void* getNativeChild (Component& component) const
{
if (auto it = nativeChildForComponent.find (&component);
it != nativeChildForComponent.end())
{
return it->second;
}
return nullptr;
}
Component* getComponent (void* nativeChild) const
{
if (auto it = componentForNativeChild.find (nativeChild);
it != componentForNativeChild.end())
{
return it->second;
}
return nullptr;
}
void setNativeChild (Component& component, void* nativeChild)
{
clearComponent (component);
if (nativeChild != nullptr)
{
nativeChildForComponent[&component] = nativeChild;
componentForNativeChild[nativeChild] = &component;
}
}
private:
NativeChildHandler() = default;
void clearComponent (Component& component)
{
if (auto* nativeChild = getNativeChild (component))
componentForNativeChild.erase (nativeChild);
nativeChildForComponent.erase (&component);
}
std::map<void*, Component*> componentForNativeChild;
std::map<Component*, void*> nativeChildForComponent;
};
AccessibilityHandler::AccessibilityHandler (Component& comp,
AccessibilityRole accessibilityRole,
AccessibilityActions accessibilityActions,
Interfaces interfacesIn)
: component (comp),
typeIndex (typeid (component)),
role (accessibilityRole),
actions (std::move (accessibilityActions)),
interfaces (std::move (interfacesIn)),
nativeImpl (createNativeImpl (*this))
{
}
AccessibilityHandler::~AccessibilityHandler()
{
giveAwayFocus();
detail::AccessibilityHelpers::notifyAccessibilityEvent (*this, detail::AccessibilityHelpers::Event::elementDestroyed);
}
//==============================================================================
AccessibleState AccessibilityHandler::getCurrentState() const
{
if (component.isCurrentlyBlockedByAnotherModalComponent()
&& Component::getCurrentlyModalComponent()->isVisible())
return {};
auto state = AccessibleState().withFocusable();
return hasFocus (false) ? state.withFocused() : state;
}
bool AccessibilityHandler::isIgnored() const
{
return role == AccessibilityRole::ignored || getCurrentState().isIgnored();
}
static bool isComponentVisibleWithinWindow (const Component& comp)
{
if (auto* peer = comp.getPeer())
return ! peer->getAreaCoveredBy (comp).getIntersection (peer->getComponent().getLocalBounds()).isEmpty();
return false;
}
static bool isComponentVisibleWithinParent (Component* comp)
{
if (auto* parent = comp->getParentComponent())
{
if (comp->getBoundsInParent().getIntersection (parent->getLocalBounds()).isEmpty())
return false;
return isComponentVisibleWithinParent (parent);
}
return true;
}
bool AccessibilityHandler::isVisibleWithinParent() const
{
return getCurrentState().isAccessibleOffscreen()
|| (isComponentVisibleWithinParent (&component) && isComponentVisibleWithinWindow (component));
}
//==============================================================================
const AccessibilityActions& AccessibilityHandler::getActions() const noexcept
{
return actions;
}
AccessibilityValueInterface* AccessibilityHandler::getValueInterface() const
{
return interfaces.value.get();
}
AccessibilityTableInterface* AccessibilityHandler::getTableInterface() const
{
return interfaces.table.get();
}
AccessibilityCellInterface* AccessibilityHandler::getCellInterface() const
{
return interfaces.cell.get();
}
AccessibilityTextInterface* AccessibilityHandler::getTextInterface() const
{
return interfaces.text.get();
}
//==============================================================================
static AccessibilityHandler* findEnclosingHandler (Component* comp)
{
if (comp != nullptr)
{
if (auto* handler = comp->getAccessibilityHandler())
return handler;
return findEnclosingHandler (comp->getParentComponent());
}
return nullptr;
}
static AccessibilityHandler* getUnignoredAncestor (AccessibilityHandler* handler)
{
while (handler != nullptr
&& (handler->isIgnored() || ! handler->isVisibleWithinParent())
&& handler->getParent() != nullptr)
{
handler = handler->getParent();
}
return handler;
}
static AccessibilityHandler* findFirstUnignoredChild (const std::vector<AccessibilityHandler*>& handlers)
{
if (! handlers.empty())
{
const auto iter = std::find_if (handlers.cbegin(), handlers.cend(),
[] (const AccessibilityHandler* handler) { return ! handler->isIgnored() && handler->isVisibleWithinParent(); });
if (iter != handlers.cend())
return *iter;
for (auto* handler : handlers)
if (auto* unignored = findFirstUnignoredChild (handler->getChildren()))
return unignored;
}
return nullptr;
}
static AccessibilityHandler* getFirstUnignoredDescendant (AccessibilityHandler* handler)
{
if (handler != nullptr && (handler->isIgnored() || ! handler->isVisibleWithinParent()))
return findFirstUnignoredChild (handler->getChildren());
return handler;
}
AccessibilityHandler* AccessibilityHandler::getParent() const
{
if (auto* focusContainer = component.findFocusContainer())
return getUnignoredAncestor (findEnclosingHandler (focusContainer));
return nullptr;
}
std::vector<AccessibilityHandler*> AccessibilityHandler::getChildren() const
{
if (! component.isFocusContainer() && component.getParentComponent() != nullptr)
return {};
const auto addChildComponentHandler = [this] (Component* focusableComponent,
std::vector<AccessibilityHandler*>& childHandlers)
{
if (focusableComponent == nullptr)
return;
if (auto* handler = findEnclosingHandler (focusableComponent))
{
if (! handler->getCurrentState().isFocusable() || ! isParentOf (handler))
return;
if (auto* unignored = getFirstUnignoredDescendant (handler))
if (std::find (childHandlers.cbegin(), childHandlers.cend(), unignored) == childHandlers.cend())
childHandlers.push_back (unignored);
}
};
std::vector<AccessibilityHandler*> children;
if (auto traverser = component.createFocusTraverser())
{
addChildComponentHandler (traverser->getDefaultComponent (&component), children);
for (auto* focusableChild : traverser->getAllComponents (&component))
addChildComponentHandler (focusableChild, children);
}
return children;
}
bool AccessibilityHandler::isParentOf (const AccessibilityHandler* possibleChild) const noexcept
{
while (possibleChild != nullptr)
{
possibleChild = possibleChild->getParent();
if (possibleChild == this)
return true;
}
return false;
}
AccessibilityHandler* AccessibilityHandler::getChildAt (Point<int> screenPoint)
{
if (auto* comp = Desktop::getInstance().findComponentAt (screenPoint))
{
if (auto* handler = getUnignoredAncestor (findEnclosingHandler (comp)))
if (isParentOf (handler))
return handler;
}
return nullptr;
}
AccessibilityHandler* AccessibilityHandler::getChildFocus()
{
return hasFocus (true) ? getUnignoredAncestor (currentlyFocusedHandler)
: nullptr;
}
bool AccessibilityHandler::hasFocus (bool trueIfChildFocused) const
{
return currentlyFocusedHandler != nullptr
&& (currentlyFocusedHandler == this
|| (trueIfChildFocused && isParentOf (currentlyFocusedHandler)));
}
void AccessibilityHandler::grabFocus()
{
if (! hasFocus (false))
grabFocusInternal (true);
}
void AccessibilityHandler::giveAwayFocus() const
{
if (hasFocus (true))
giveAwayFocusInternal();
}
void AccessibilityHandler::grabFocusInternal (bool canTryParent)
{
if (getCurrentState().isFocusable() && ! isIgnored())
{
takeFocus();
return;
}
if (isParentOf (currentlyFocusedHandler))
return;
if (auto traverser = component.createFocusTraverser())
{
if (auto* defaultComp = traverser->getDefaultComponent (&component))
{
if (auto* handler = getUnignoredAncestor (findEnclosingHandler (defaultComp)))
{
if (isParentOf (handler))
{
handler->grabFocusInternal (false);
return;
}
}
}
}
if (canTryParent)
if (auto* parent = getParent())
parent->grabFocusInternal (true);
}
void AccessibilityHandler::giveAwayFocusInternal() const
{
currentlyFocusedHandler = nullptr;
detail::AccessibilityHelpers::notifyAccessibilityEvent (*this, detail::AccessibilityHelpers::Event::focusChanged);
}
void AccessibilityHandler::takeFocus()
{
currentlyFocusedHandler = this;
detail::AccessibilityHelpers::notifyAccessibilityEvent (*this, detail::AccessibilityHelpers::Event::focusChanged);
if ((component.isShowing() || component.isOnDesktop())
&& component.getWantsKeyboardFocus()
&& ! component.hasKeyboardFocus (true))
{
component.grabKeyboardFocus();
}
}
std::unique_ptr<AccessibilityHandler::AccessibilityNativeImpl> AccessibilityHandler::createNativeImpl (AccessibilityHandler& handler)
{
#if JUCE_NATIVE_ACCESSIBILITY_INCLUDED
return std::make_unique<AccessibilityNativeImpl> (handler);
#else
ignoreUnused (handler);
return nullptr;
#endif
}
void* AccessibilityHandler::getNativeChildForComponent (Component& component)
{
return NativeChildHandler::getInstance().getNativeChild (component);
}
Component* AccessibilityHandler::getComponentForNativeChild (void* nativeChild)
{
return NativeChildHandler::getInstance().getComponent (nativeChild);
}
void AccessibilityHandler::setNativeChildForComponent (Component& component, void* nativeChild)
{
NativeChildHandler::getInstance().setNativeChild (component, nativeChild);
}
#if ! JUCE_NATIVE_ACCESSIBILITY_INCLUDED
void AccessibilityHandler::notifyAccessibilityEvent (AccessibilityEvent) const {}
void AccessibilityHandler::postAnnouncement (const String&, AnnouncementPriority) {}
AccessibilityNativeHandle* AccessibilityHandler::getNativeImplementation() const { return nullptr; }
#endif
} // namespace juce
@@ -0,0 +1,345 @@
/*
==============================================================================
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;
/** Base class for accessible Components.
This class wraps a Component and provides methods that allow an accessibility client,
such as VoiceOver on macOS, or Narrator on Windows, to control it.
It handles hierarchical navigation, properties, state, and various interfaces.
@tags{Accessibility}
*/
class JUCE_API AccessibilityHandler
{
public:
/** Utility struct which holds one or more accessibility interfaces.
The main purpose of this class is to provide convenience constructors from each
of the four types of accessibility interface.
*/
struct JUCE_API Interfaces
{
Interfaces() = default;
Interfaces (std::unique_ptr<AccessibilityValueInterface> ptr) : value (std::move (ptr)) {}
Interfaces (std::unique_ptr<AccessibilityTextInterface> ptr) : text (std::move (ptr)) {}
Interfaces (std::unique_ptr<AccessibilityTableInterface> ptr) : table (std::move (ptr)) {}
Interfaces (std::unique_ptr<AccessibilityCellInterface> ptr) : cell (std::move (ptr)) {}
Interfaces (std::unique_ptr<AccessibilityValueInterface> valueIn,
std::unique_ptr<AccessibilityTextInterface> textIn,
std::unique_ptr<AccessibilityTableInterface> tableIn,
std::unique_ptr<AccessibilityCellInterface> cellIn)
: value (std::move (valueIn)),
text (std::move (textIn)),
table (std::move (tableIn)),
cell (std::move (cellIn))
{
}
std::unique_ptr<AccessibilityValueInterface> value;
std::unique_ptr<AccessibilityTextInterface> text;
std::unique_ptr<AccessibilityTableInterface> table;
std::unique_ptr<AccessibilityCellInterface> cell;
};
/** Constructor.
This will create a AccessibilityHandler which wraps the provided Component and makes
it visible to accessibility clients. You must also specify a role for the UI element
from the `AccessibilityRole` list which best describes it.
To enable users to interact with the UI element you should provide the set of supported
actions and their associated callbacks via the `accessibilityActions` parameter.
For UI elements that support more complex interaction the value, text, table, and cell
interfaces should be implemented as required and passed as the final argument of this
constructor. See the documentation of these classes for more information about the
types of control they represent and which methods need to be implemented.
*/
AccessibilityHandler (Component& componentToWrap,
AccessibilityRole accessibilityRole,
AccessibilityActions actions = {},
Interfaces interfaces = {});
/** Destructor. */
virtual ~AccessibilityHandler();
//==============================================================================
/** Returns the Component that this handler represents. */
const Component& getComponent() const noexcept { return component; }
/** Returns the Component that this handler represents. */
Component& getComponent() noexcept { return component; }
//==============================================================================
/** The type of UI element that this accessibility handler represents.
@see AccessibilityRole
*/
AccessibilityRole getRole() const noexcept { return role; }
/** The title of the UI element.
This will be read out by the system and should be concise, preferably matching
the visible title of the UI element (if any). For example, this might be the
text of a button or a simple label.
The default implementation will call `Component::getTitle()`, but you can override
this to return a different string if required.
If neither a name nor a description is provided then the UI element may be
ignored by accessibility clients.
This must be a localised string.
*/
virtual String getTitle() const { return component.getTitle(); }
/** A short description of the UI element.
This may be read out by the system. It should not include the type of the UI
element and should ideally be a single word, for example "Open" for a button
that opens a window.
The default implementation will call `Component::getDescription()`, but you
can override this to return a different string if required.
If neither a name nor a description is provided then the UI element may be
ignored by accessibility clients.
This must be a localised string.
*/
virtual String getDescription() const { return component.getDescription(); }
/** Some help text for the UI element (if required).
This may be read out by the system. This string functions in a similar way to
a tooltip, for example "Click to open window." for a button which opens a window.
The default implementation will call `Component::getHelpText()`, but you can
override this to return a different string if required.
This must be a localised string.
*/
virtual String getHelp() const { return component.getHelpText(); }
/** Returns the current state of the UI element.
The default implementation of this method will set the focusable flag and, if
this UI element is currently focused, will also set the focused flag.
*/
virtual AccessibleState getCurrentState() const;
/** Returns true if this UI element should be ignored by accessibility clients. */
bool isIgnored() const;
/** Returns true if this UI element is visible within its parent.
This will always return true for UI elements with the AccessibleState::accessibleOffscreen
flag set.
*/
bool isVisibleWithinParent() const;
//==============================================================================
/** Returns the set of actions that the UI element supports and the associated
callbacks.
*/
const AccessibilityActions& getActions() const noexcept;
/** Returns the value interface for this UI element, or nullptr if it is not supported.
@see AccessibilityValueInterface
*/
AccessibilityValueInterface* getValueInterface() const;
/** Returns the table interface for this UI element, or nullptr if it is not supported.
@see AccessibilityTableInterface
*/
AccessibilityTableInterface* getTableInterface() const;
/** Returns the cell interface for this UI element, or nullptr if it is not supported.
@see AccessibilityCellInterface
*/
AccessibilityCellInterface* getCellInterface() const;
/** Returns the text interface for this UI element, or nullptr if it is not supported.
@see AccessibilityTextInterface
*/
AccessibilityTextInterface* getTextInterface() const;
//==============================================================================
/** Returns the first unignored parent of this UI element in the accessibility hierarchy,
or nullptr if this is a root element without a parent.
*/
AccessibilityHandler* getParent() const;
/** Returns the unignored children of this UI element in the accessibility hierarchy. */
std::vector<AccessibilityHandler*> getChildren() const;
/** Checks whether a given UI element is a child of this one in the accessibility
hierarchy.
*/
bool isParentOf (const AccessibilityHandler* possibleChild) const noexcept;
/** Returns the deepest child of this UI element in the accessibility hierarchy that
contains the given screen point, or nullptr if there is no child at this point.
*/
AccessibilityHandler* getChildAt (Point<int> screenPoint);
/** Returns the deepest UI element which currently has focus.
This can be a child of this UI element or, if no child is focused,
this element itself.
Note that this can be different to the value of the Component with keyboard
focus returned by Component::getCurrentlyFocusedComponent().
@see hasFocus
*/
AccessibilityHandler* getChildFocus();
/** Returns true if this UI element has the focus.
@param trueIfChildFocused if this is true, this method will also return true
if any child of this UI element in the accessibility
hierarchy has focus
*/
bool hasFocus (bool trueIfChildFocused) const;
/** Tries to give focus to this UI element.
If the UI element is focusable and not ignored this will update the currently focused
element, try to give keyboard focus to the Component it represents, and notify any
listening accessibility clients that the current focus has changed.
@see hasFocus, giveAwayFocus
*/
void grabFocus();
/** If this UI element or any of its children in the accessibility hierarchy currently
have focus, this will defocus it.
This will also give away the keyboard focus from the Component it represents, and
notify any listening accessibility clients that the current focus has changed.
@see hasFocus, grabFocus
*/
void giveAwayFocus() const;
//==============================================================================
/** Used to send a notification to any observing accessibility clients that something
has changed in the UI element.
@see AccessibilityEvent
*/
void notifyAccessibilityEvent (AccessibilityEvent event) const;
/** A priority level that can help an accessibility client determine how to handle
an announcement request.
Exactly what this controls is platform-specific, but generally a low priority
announcement will be read when the screen reader is free, whereas a high priority
announcement will interrupt the current speech.
*/
enum class AnnouncementPriority
{
low,
medium,
high
};
/** Posts an announcement to be made to the user.
@param announcementString a localised string containing the announcement to be read out
@param priority the appropriate priority level for the announcement
*/
static void postAnnouncement (const String& announcementString, AnnouncementPriority priority);
//==============================================================================
/** @internal */
AccessibilityNativeHandle* getNativeImplementation() const;
/** @internal */
std::type_index getTypeIndex() const { return typeIndex; }
/** @internal */
static void clearCurrentlyFocusedHandler() { currentlyFocusedHandler = nullptr; }
/** @internal
The following functions provide the means to associate JUCE Components with OS specific
types that provide their own accessibility mechanisms. This way accessibility navigation
can move from a JUCE Component to a native, embedded window and back.
These functions assume that the concrete types behind the void* are
- Windows: HWND
- MacOS: NSView*
- iOS: UIView*
- Android: GlobalRef that points to an android.view.View
*/
static void* getNativeChildForComponent (Component& component);
/** @internal */
static void setNativeChildForComponent (Component& component, void* nativeChild);
/** @internal */
static Component* getComponentForNativeChild (void* nativeChild);
private:
//==============================================================================
friend class AccessibilityNativeHandle;
//==============================================================================
void grabFocusInternal (bool);
void giveAwayFocusInternal() const;
void takeFocus();
static AccessibilityHandler* currentlyFocusedHandler;
//==============================================================================
Component& component;
std::type_index typeIndex;
const AccessibilityRole role;
AccessibilityActions actions;
Interfaces interfaces;
//==============================================================================
class AccessibilityNativeImpl;
std::unique_ptr<AccessibilityNativeImpl> nativeImpl;
static std::unique_ptr<AccessibilityNativeImpl> createNativeImpl (AccessibilityHandler&);
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityHandler)
};
} // namespace juce
@@ -0,0 +1,227 @@
/*
==============================================================================
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
{
/** Represents the state of an accessible UI element.
An instance of this class is returned by `AccessibilityHandler::getCurrentState()`
to convey its current state to an accessibility client.
@see AccessibilityHandler
@tags{Accessibility}
*/
class JUCE_API AccessibleState
{
public:
/** Constructor.
Represents a "default" state with no flags set. To set a flag, use one of the
`withX()` methods - these can be chained together to set multiple flags.
*/
AccessibleState() = default;
//==============================================================================
/** Sets the checkable flag and returns the new state.
@see isCheckable
*/
[[nodiscard]] AccessibleState withCheckable() const noexcept { return withFlag (Flags::checkable); }
/** Sets the checked flag and returns the new state.
@see isChecked
*/
[[nodiscard]] AccessibleState withChecked() const noexcept { return withFlag (Flags::checked); }
/** Sets the collapsed flag and returns the new state.
@see isCollapsed
*/
[[nodiscard]] AccessibleState withCollapsed() const noexcept { return withFlag (Flags::collapsed); }
/** Sets the expandable flag and returns the new state.
@see isExpandable
*/
[[nodiscard]] AccessibleState withExpandable() const noexcept { return withFlag (Flags::expandable); }
/** Sets the expanded flag and returns the new state.
@see isExpanded
*/
[[nodiscard]] AccessibleState withExpanded() const noexcept { return withFlag (Flags::expanded); }
/** Sets the focusable flag and returns the new state.
@see isFocusable
*/
[[nodiscard]] AccessibleState withFocusable() const noexcept { return withFlag (Flags::focusable); }
/** Sets the focused flag and returns the new state.
@see isFocused
*/
[[nodiscard]] AccessibleState withFocused() const noexcept { return withFlag (Flags::focused); }
/** Sets the ignored flag and returns the new state.
@see isIgnored
*/
[[nodiscard]] AccessibleState withIgnored() const noexcept { return withFlag (Flags::ignored); }
/** Sets the selectable flag and returns the new state.
@see isSelectable
*/
[[nodiscard]] AccessibleState withSelectable() const noexcept { return withFlag (Flags::selectable); }
/** Sets the multiSelectable flag and returns the new state.
@see isMultiSelectable
*/
[[nodiscard]] AccessibleState withMultiSelectable() const noexcept { return withFlag (Flags::multiSelectable); }
/** Sets the selected flag and returns the new state.
@see isSelected
*/
[[nodiscard]] AccessibleState withSelected() const noexcept { return withFlag (Flags::selected); }
/** Sets the accessible offscreen flag and returns the new state.
@see isSelected
*/
[[nodiscard]] AccessibleState withAccessibleOffscreen() const noexcept { return withFlag (Flags::accessibleOffscreen); }
//==============================================================================
/** Returns true if the UI element is checkable.
@see withCheckable
*/
bool isCheckable() const noexcept { return isFlagSet (Flags::checkable); }
/** Returns true if the UI element is checked.
@see withChecked
*/
bool isChecked() const noexcept { return isFlagSet (Flags::checked); }
/** Returns true if the UI element is collapsed.
@see withCollapsed
*/
bool isCollapsed() const noexcept { return isFlagSet (Flags::collapsed); }
/** Returns true if the UI element is expandable.
@see withExpandable
*/
bool isExpandable() const noexcept { return isFlagSet (Flags::expandable); }
/** Returns true if the UI element is expanded.
@see withExpanded
*/
bool isExpanded() const noexcept { return isFlagSet (Flags::expanded); }
/** Returns true if the UI element is focusable.
@see withFocusable
*/
bool isFocusable() const noexcept { return isFlagSet (Flags::focusable); }
/** Returns true if the UI element is focused.
@see withFocused
*/
bool isFocused() const noexcept { return isFlagSet (Flags::focused); }
/** Returns true if the UI element is ignored.
@see withIgnored
*/
bool isIgnored() const noexcept { return isFlagSet (Flags::ignored); }
/** Returns true if the UI element supports multiple item selection.
@see withMultiSelectable
*/
bool isMultiSelectable() const noexcept { return isFlagSet (Flags::multiSelectable); }
/** Returns true if the UI element is selectable.
@see withSelectable
*/
bool isSelectable() const noexcept { return isFlagSet (Flags::selectable); }
/** Returns true if the UI element is selected.
@see withSelected
*/
bool isSelected() const noexcept { return isFlagSet (Flags::selected); }
/** Returns true if the UI element is accessible offscreen.
@see withSelected
*/
bool isAccessibleOffscreen() const noexcept { return isFlagSet (Flags::accessibleOffscreen); }
private:
enum Flags
{
checkable = (1 << 0),
checked = (1 << 1),
collapsed = (1 << 2),
expandable = (1 << 3),
expanded = (1 << 4),
focusable = (1 << 5),
focused = (1 << 6),
ignored = (1 << 7),
multiSelectable = (1 << 8),
selectable = (1 << 9),
selected = (1 << 10),
accessibleOffscreen = (1 << 11)
};
[[nodiscard]] AccessibleState withFlag (int flag) const noexcept
{
auto copy = *this;
copy.flags |= flag;
return copy;
}
bool isFlagSet (int flag) const noexcept
{
return (flags & flag) != 0;
}
int flags = 0;
};
} // namespace juce
@@ -0,0 +1,100 @@
/*
==============================================================================
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
{
JUCEApplication::JUCEApplication() {}
JUCEApplication::~JUCEApplication() {}
//==============================================================================
JUCEApplication* JUCE_CALLTYPE JUCEApplication::getInstance() noexcept
{
return dynamic_cast<JUCEApplication*> (JUCEApplicationBase::getInstance());
}
bool JUCEApplication::moreThanOneInstanceAllowed() { return true; }
void JUCEApplication::anotherInstanceStarted (const String&) {}
void JUCEApplication::suspended() {}
void JUCEApplication::resumed() {}
void JUCEApplication::systemRequestedQuit() { quit(); }
void JUCEApplication::unhandledException (const std::exception*, const String&, int)
{
jassertfalse;
}
//==============================================================================
ApplicationCommandTarget* JUCEApplication::getNextCommandTarget()
{
return nullptr;
}
void JUCEApplication::getAllCommands (Array<CommandID>& commands)
{
commands.add (StandardApplicationCommandIDs::quit);
}
void JUCEApplication::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result)
{
if (commandID == StandardApplicationCommandIDs::quit)
{
result.setInfo (TRANS ("Quit"),
TRANS ("Quits the application"),
"Application", 0);
result.defaultKeypresses.add (KeyPress ('q', ModifierKeys::commandModifier, 0));
}
}
bool JUCEApplication::perform (const InvocationInfo& info)
{
if (info.commandID == StandardApplicationCommandIDs::quit)
{
systemRequestedQuit();
return true;
}
return false;
}
//==============================================================================
bool JUCEApplication::initialiseApp()
{
if (JUCEApplicationBase::initialiseApp())
{
#if JUCE_MAC
initialiseMacMainMenu(); // (needs to get the app's name)
#endif
return true;
}
return false;
}
} // namespace juce
@@ -0,0 +1,193 @@
/*
==============================================================================
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
{
//==============================================================================
/**
An instance of this class is used to specify initialisation and shutdown
code for the application.
Any application that wants to run an event loop must declare a subclass of
JUCEApplicationBase or JUCEApplication, and implement its various pure virtual
methods.
It then needs to use the START_JUCE_APPLICATION macro somewhere in a CPP file
to declare an instance of this class and generate suitable platform-specific
boilerplate code to launch the app.
Note that this class is derived from JUCEApplicationBase, which contains most
of the useful methods and functionality. This derived class is here simply as
a convenient way to also inherit from an ApplicationCommandTarget, and to implement
default versions of some of the pure virtual base class methods. But you can derive
your app object directly from JUCEApplicationBase if you want to, and by doing so
can avoid having a dependency on the juce_gui_basics module.
e.g. @code
class MyJUCEApp : public JUCEApplication
{
public:
MyJUCEApp() {}
~MyJUCEApp() {}
void initialise (const String& commandLine) override
{
myMainWindow.reset (new MyApplicationWindow());
myMainWindow->setBounds (100, 100, 400, 500);
myMainWindow->setVisible (true);
}
void shutdown() override
{
myMainWindow = nullptr;
}
const String getApplicationName() override
{
return "Super JUCE-o-matic";
}
const String getApplicationVersion() override
{
return "1.0";
}
private:
std::unique_ptr<MyApplicationWindow> myMainWindow;
};
// this generates boilerplate code to launch our app class:
START_JUCE_APPLICATION (MyJUCEApp)
@endcode
@see JUCEApplicationBase, START_JUCE_APPLICATION
@tags{GUI}
*/
class JUCE_API JUCEApplication : public JUCEApplicationBase,
public ApplicationCommandTarget
{
public:
//==============================================================================
/** Constructs a JUCE app object.
If subclasses implement a constructor or destructor, they shouldn't call any
JUCE code in there - put your startup/shutdown code in initialise() and
shutdown() instead.
*/
JUCEApplication();
/** Destructor.
If subclasses implement a constructor or destructor, they shouldn't call any
JUCE code in there - put your startup/shutdown code in initialise() and
shutdown() instead.
*/
~JUCEApplication() override;
//==============================================================================
/** Returns the global instance of the application object being run. */
static JUCEApplication* JUCE_CALLTYPE getInstance() noexcept;
//==============================================================================
#if DOXYGEN
/** Returns the application's name. */
virtual const String getApplicationName() = 0;
/** Returns the application's version number. */
virtual const String getApplicationVersion() = 0;
#endif
/** Checks whether multiple instances of the app are allowed.
If your application class returns true for this, more than one instance is
permitted to run (except on OSX where the OS automatically stops you launching
a second instance of an app without explicitly starting it from the command-line).
If it's false, the second instance won't start, but you will still get a
callback to anotherInstanceStarted() to tell you about this - which
gives you a chance to react to what the user was trying to do.
*/
bool moreThanOneInstanceAllowed() override;
/** Indicates that the user has tried to start up another instance of the app.
This will get called even if moreThanOneInstanceAllowed() is false.
*/
void anotherInstanceStarted (const String& commandLine) override;
/** Called when the operating system is trying to close the application.
The default implementation of this method is to call quit(), but it may
be overloaded to ignore the request or do some other special behaviour
instead. For example, you might want to offer the user the chance to save
their changes before quitting, and give them the chance to cancel.
If you want to send a quit signal to your app, this is the correct method
to call, because it means that requests that come from the system get handled
in the same way as those from your own application code. So e.g. you'd
call this method from a "quit" item on a menu bar.
*/
void systemRequestedQuit() override;
/** This method is called when the application is being put into background mode
by the operating system.
*/
void suspended() override;
/** This method is called when the application is being woken from background mode
by the operating system.
*/
void resumed() override;
/** If any unhandled exceptions make it through to the message dispatch loop, this
callback will be triggered, in case you want to log them or do some other
type of error-handling.
If the type of exception is derived from the std::exception class, the pointer
passed-in will be valid. If the exception is of unknown type, this pointer
will be null.
*/
void unhandledException (const std::exception* e,
const String& sourceFilename,
int lineNumber) override;
//==============================================================================
/** @internal */
ApplicationCommandTarget* getNextCommandTarget() override;
/** @internal */
void getCommandInfo (CommandID, ApplicationCommandInfo&) override;
/** @internal */
void getAllCommands (Array<CommandID>&) override;
/** @internal */
bool perform (const InvocationInfo&) override;
private:
bool initialiseApp() override;
JUCE_DECLARE_NON_COPYABLE (JUCEApplication)
};
} // namespace juce
@@ -0,0 +1,51 @@
/*
==============================================================================
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
{
ArrowButton::ArrowButton (const String& name, float arrowDirectionInRadians, Colour arrowColour)
: Button (name), colour (arrowColour)
{
path.addTriangle (0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.5f);
path.applyTransform (AffineTransform::rotation (MathConstants<float>::twoPi * arrowDirectionInRadians, 0.5f, 0.5f));
}
ArrowButton::~ArrowButton() {}
void ArrowButton::paintButton (Graphics& g, bool /*shouldDrawButtonAsHighlighted*/, bool shouldDrawButtonAsDown)
{
Path p (path);
const float offset = shouldDrawButtonAsDown ? 1.0f : 0.0f;
p.applyTransform (path.getTransformToScaleToFit (offset, offset, (float) getWidth() - 3.0f, (float) getHeight() - 3.0f, false));
DropShadow (Colours::black.withAlpha (0.3f), shouldDrawButtonAsDown ? 2 : 4, Point<int>()).drawForPath (g, p);
g.setColour (colour);
g.fillPath (p);
}
} // namespace juce
@@ -0,0 +1,65 @@
/*
==============================================================================
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 button with an arrow in it.
@see Button
@tags{GUI}
*/
class JUCE_API ArrowButton : public Button
{
public:
//==============================================================================
/** Creates an ArrowButton.
@param buttonName the name to give the button
@param arrowDirection the direction the arrow should point in, where 0.0 is
pointing right, 0.25 is down, 0.5 is left, 0.75 is up
@param arrowColour the colour to use for the arrow
*/
ArrowButton (const String& buttonName,
float arrowDirection,
Colour arrowColour);
/** Destructor. */
~ArrowButton() override;
/** @internal */
void paintButton (Graphics&, bool, bool) override;
private:
Colour colour;
Path path;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ArrowButton)
};
} // namespace juce
@@ -0,0 +1,721 @@
/*
==============================================================================
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 Button::CallbackHelper final : public Timer,
public ApplicationCommandManagerListener,
public Value::Listener,
public KeyListener
{
CallbackHelper (Button& b) : button (b) {}
void timerCallback() override
{
button.repeatTimerCallback();
}
bool keyStateChanged (bool, Component*) override
{
return button.keyStateChangedCallback();
}
void valueChanged (Value& value) override
{
if (value.refersToSameSourceAs (button.isOn))
button.setToggleState (button.isOn.getValue(), dontSendNotification, sendNotification);
}
bool keyPressed (const KeyPress&, Component*) override
{
// returning true will avoid forwarding events for keys that we're using as shortcuts
return button.isShortcutPressed();
}
void applicationCommandInvoked (const ApplicationCommandTarget::InvocationInfo& info) override
{
if (info.commandID == button.commandID
&& (info.commandFlags & ApplicationCommandInfo::dontTriggerVisualFeedback) == 0)
button.flashButtonState();
}
void applicationCommandListChanged() override
{
button.applicationCommandListChangeCallback();
}
Button& button;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CallbackHelper)
};
//==============================================================================
Button::Button (const String& name) : Component (name), text (name)
{
callbackHelper.reset (new CallbackHelper (*this));
setWantsKeyboardFocus (true);
isOn.addListener (callbackHelper.get());
}
Button::~Button()
{
clearShortcuts();
if (commandManagerToUse != nullptr)
commandManagerToUse->removeListener (callbackHelper.get());
isOn.removeListener (callbackHelper.get());
callbackHelper.reset();
}
//==============================================================================
void Button::setButtonText (const String& newText)
{
if (text != newText)
{
text = newText;
repaint();
}
}
void Button::setTooltip (const String& newTooltip)
{
SettableTooltipClient::setTooltip (newTooltip);
generateTooltip = false;
}
void Button::updateAutomaticTooltip (const ApplicationCommandInfo& info)
{
if (generateTooltip && commandManagerToUse != nullptr)
{
auto tt = info.description.isNotEmpty() ? info.description
: info.shortName;
for (auto& kp : commandManagerToUse->getKeyMappings()->getKeyPressesAssignedToCommand (commandID))
{
auto key = kp.getTextDescription();
tt << " [";
if (key.length() == 1)
tt << TRANS ("shortcut") << ": '" << key << "']";
else
tt << key << ']';
}
SettableTooltipClient::setTooltip (tt);
}
}
void Button::setConnectedEdges (int newFlags)
{
if (connectedEdgeFlags != newFlags)
{
connectedEdgeFlags = newFlags;
repaint();
}
}
//==============================================================================
void Button::checkToggleableState (bool wasToggleable)
{
if (isToggleable() != wasToggleable)
invalidateAccessibilityHandler();
}
void Button::setToggleable (bool isNowToggleable)
{
const auto wasToggleable = isToggleable();
canBeToggled = isNowToggleable;
checkToggleableState (wasToggleable);
}
void Button::setToggleState (bool shouldBeOn, NotificationType notification)
{
setToggleState (shouldBeOn, notification, notification);
}
void Button::setToggleState (bool shouldBeOn, NotificationType clickNotification, NotificationType stateNotification)
{
if (shouldBeOn != lastToggleState)
{
WeakReference<Component> deletionWatcher (this);
if (shouldBeOn)
{
turnOffOtherButtonsInGroup (clickNotification, stateNotification);
if (deletionWatcher == nullptr)
return;
}
// This test is done so that if the value is void rather than explicitly set to
// false, the value won't be changed unless the required value is true.
if (getToggleState() != shouldBeOn)
{
isOn = shouldBeOn;
if (deletionWatcher == nullptr)
return;
}
lastToggleState = shouldBeOn;
repaint();
if (clickNotification != dontSendNotification)
{
// async callbacks aren't possible here
jassert (clickNotification != sendNotificationAsync);
sendClickMessage (ModifierKeys::currentModifiers);
if (deletionWatcher == nullptr)
return;
}
if (stateNotification != dontSendNotification)
sendStateMessage();
else
buttonStateChanged();
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged);
}
}
void Button::setToggleState (bool shouldBeOn, bool sendChange)
{
setToggleState (shouldBeOn, sendChange ? sendNotification : dontSendNotification);
}
void Button::setClickingTogglesState (bool shouldToggle) noexcept
{
const auto wasToggleable = isToggleable();
clickTogglesState = shouldToggle;
checkToggleableState (wasToggleable);
// if you've got clickTogglesState turned on, you shouldn't also connect the button
// up to be a command invoker. Instead, your command handler must flip the state of whatever
// it is that this button represents, and the button will update its state to reflect this
// in the applicationCommandListChanged() method.
jassert (commandManagerToUse == nullptr || ! clickTogglesState);
}
void Button::setRadioGroupId (int newGroupId, NotificationType notification)
{
if (radioGroupId != newGroupId)
{
radioGroupId = newGroupId;
if (lastToggleState)
turnOffOtherButtonsInGroup (notification, notification);
setToggleable (true);
invalidateAccessibilityHandler();
}
}
void Button::turnOffOtherButtonsInGroup (NotificationType clickNotification, NotificationType stateNotification)
{
if (auto* p = getParentComponent())
{
if (radioGroupId != 0)
{
WeakReference<Component> deletionWatcher (this);
for (auto* c : p->getChildren())
{
if (c != this)
{
if (auto b = dynamic_cast<Button*> (c))
{
if (b->getRadioGroupId() == radioGroupId)
{
b->setToggleState (false, clickNotification, stateNotification);
if (deletionWatcher == nullptr)
return;
}
}
}
}
}
}
}
//==============================================================================
void Button::enablementChanged()
{
updateState();
repaint();
}
Button::ButtonState Button::updateState()
{
return updateState (isMouseOver (true), isMouseButtonDown());
}
Button::ButtonState Button::updateState (bool over, bool down)
{
ButtonState newState = buttonNormal;
if (isEnabled() && isVisible() && ! isCurrentlyBlockedByAnotherModalComponent())
{
if ((down && (over || (triggerOnMouseDown && buttonState == buttonDown))) || isKeyDown)
newState = buttonDown;
else if (over)
newState = buttonOver;
}
setState (newState);
return newState;
}
void Button::setState (ButtonState newState)
{
if (buttonState != newState)
{
buttonState = newState;
repaint();
if (buttonState == buttonDown)
{
buttonPressTime = Time::getApproximateMillisecondCounter();
lastRepeatTime = 0;
}
sendStateMessage();
}
}
bool Button::isDown() const noexcept { return buttonState == buttonDown; }
bool Button::isOver() const noexcept { return buttonState != buttonNormal; }
void Button::buttonStateChanged() {}
uint32 Button::getMillisecondsSinceButtonDown() const noexcept
{
auto now = Time::getApproximateMillisecondCounter();
return now > buttonPressTime ? now - buttonPressTime : 0;
}
void Button::setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) noexcept
{
triggerOnMouseDown = isTriggeredOnMouseDown;
}
bool Button::getTriggeredOnMouseDown() const noexcept
{
return triggerOnMouseDown;
}
//==============================================================================
void Button::clicked()
{
}
void Button::clicked (const ModifierKeys&)
{
clicked();
}
enum { clickMessageId = 0x2f3f4f99 };
void Button::triggerClick()
{
postCommandMessage (clickMessageId);
}
void Button::internalClickCallback (const ModifierKeys& modifiers)
{
if (clickTogglesState)
{
const bool shouldBeOn = (radioGroupId != 0 || ! lastToggleState);
if (shouldBeOn != getToggleState())
{
setToggleState (shouldBeOn, sendNotification);
return;
}
}
sendClickMessage (modifiers);
}
void Button::flashButtonState()
{
if (isEnabled())
{
needsToRelease = true;
setState (buttonDown);
callbackHelper->startTimer (100);
}
}
void Button::handleCommandMessage (int commandId)
{
if (commandId == clickMessageId)
{
if (isEnabled())
{
flashButtonState();
internalClickCallback (ModifierKeys::currentModifiers);
}
}
else
{
Component::handleCommandMessage (commandId);
}
}
//==============================================================================
void Button::addListener (Listener* l) { buttonListeners.add (l); }
void Button::removeListener (Listener* l) { buttonListeners.remove (l); }
void Button::sendClickMessage (const ModifierKeys& modifiers)
{
Component::BailOutChecker checker (this);
if (commandManagerToUse != nullptr && commandID != 0)
{
ApplicationCommandTarget::InvocationInfo info (commandID);
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromButton;
info.originatingComponent = this;
commandManagerToUse->invoke (info, true);
}
clicked (modifiers);
if (checker.shouldBailOut())
return;
buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonClicked (this); });
if (checker.shouldBailOut())
return;
NullCheckedInvocation::invoke (onClick);
}
void Button::sendStateMessage()
{
Component::BailOutChecker checker (this);
buttonStateChanged();
if (checker.shouldBailOut())
return;
buttonListeners.callChecked (checker, [this] (Listener& l) { l.buttonStateChanged (this); });
if (checker.shouldBailOut())
return;
NullCheckedInvocation::invoke (onStateChange);
}
//==============================================================================
void Button::paint (Graphics& g)
{
if (needsToRelease && isEnabled())
{
needsToRelease = false;
needsRepainting = true;
}
paintButton (g, isOver(), isDown());
lastStatePainted = buttonState;
}
//==============================================================================
void Button::mouseEnter (const MouseEvent&) { updateState (true, false); }
void Button::mouseExit (const MouseEvent&) { updateState (false, false); }
void Button::mouseDown (const MouseEvent& e)
{
updateState (true, true);
if (isDown())
{
if (autoRepeatDelay >= 0)
callbackHelper->startTimer (autoRepeatDelay);
if (triggerOnMouseDown)
internalClickCallback (e.mods);
}
}
void Button::mouseUp (const MouseEvent& e)
{
const auto wasDown = isDown();
const auto wasOver = isOver();
updateState (isMouseSourceOver (e), false);
if (wasDown && wasOver && ! triggerOnMouseDown)
{
if (lastStatePainted != buttonDown)
flashButtonState();
WeakReference<Component> deletionWatcher (this);
internalClickCallback (e.mods);
if (deletionWatcher != nullptr)
updateState (isMouseSourceOver (e), false);
}
}
void Button::mouseDrag (const MouseEvent& e)
{
auto oldState = buttonState;
updateState (isMouseSourceOver (e), true);
if (autoRepeatDelay >= 0 && buttonState != oldState && isDown())
callbackHelper->startTimer (autoRepeatSpeed);
}
bool Button::isMouseSourceOver (const MouseEvent& e)
{
if (e.source.isTouch() || e.source.isPen())
return getLocalBounds().toFloat().contains (e.position);
return isMouseOver();
}
void Button::focusGained (FocusChangeType)
{
updateState();
repaint();
}
void Button::focusLost (FocusChangeType)
{
updateState();
repaint();
}
void Button::visibilityChanged()
{
needsToRelease = false;
updateState();
}
void Button::parentHierarchyChanged()
{
auto* newKeySource = shortcuts.isEmpty() ? nullptr : getTopLevelComponent();
if (newKeySource != keySource.get())
{
if (keySource != nullptr)
keySource->removeKeyListener (callbackHelper.get());
keySource = newKeySource;
if (keySource != nullptr)
keySource->addKeyListener (callbackHelper.get());
}
}
//==============================================================================
void Button::setCommandToTrigger (ApplicationCommandManager* newCommandManager,
CommandID newCommandID, bool generateTip)
{
commandID = newCommandID;
generateTooltip = generateTip;
if (commandManagerToUse != newCommandManager)
{
if (commandManagerToUse != nullptr)
commandManagerToUse->removeListener (callbackHelper.get());
commandManagerToUse = newCommandManager;
if (commandManagerToUse != nullptr)
commandManagerToUse->addListener (callbackHelper.get());
// if you've got clickTogglesState turned on, you shouldn't also connect the button
// up to be a command invoker. Instead, your command handler must flip the state of whatever
// it is that this button represents, and the button will update its state to reflect this
// in the applicationCommandListChanged() method.
jassert (commandManagerToUse == nullptr || ! clickTogglesState);
}
if (commandManagerToUse != nullptr)
applicationCommandListChangeCallback();
else
setEnabled (true);
}
void Button::applicationCommandListChangeCallback()
{
if (commandManagerToUse != nullptr)
{
ApplicationCommandInfo info (0);
if (commandManagerToUse->getTargetForCommand (commandID, info) != nullptr)
{
updateAutomaticTooltip (info);
setEnabled ((info.flags & ApplicationCommandInfo::isDisabled) == 0);
setToggleState ((info.flags & ApplicationCommandInfo::isTicked) != 0, dontSendNotification);
}
else
{
setEnabled (false);
}
}
}
//==============================================================================
void Button::addShortcut (const KeyPress& key)
{
if (key.isValid())
{
jassert (! isRegisteredForShortcut (key)); // already registered!
shortcuts.add (key);
parentHierarchyChanged();
}
}
void Button::clearShortcuts()
{
shortcuts.clear();
parentHierarchyChanged();
}
bool Button::isShortcutPressed() const
{
if (isShowing() && ! isCurrentlyBlockedByAnotherModalComponent())
for (auto& s : shortcuts)
if (s.isCurrentlyDown())
return true;
return false;
}
bool Button::isRegisteredForShortcut (const KeyPress& key) const
{
for (auto& s : shortcuts)
if (key == s)
return true;
return false;
}
bool Button::keyStateChangedCallback()
{
if (! isEnabled())
return false;
const bool wasDown = isKeyDown;
isKeyDown = isShortcutPressed();
if (autoRepeatDelay >= 0 && (isKeyDown && ! wasDown))
callbackHelper->startTimer (autoRepeatDelay);
updateState();
if (isEnabled() && wasDown && ! isKeyDown)
{
internalClickCallback (ModifierKeys::currentModifiers);
// (return immediately - this button may now have been deleted)
return true;
}
return wasDown || isKeyDown;
}
bool Button::keyPressed (const KeyPress& key)
{
if (isEnabled() && key.isKeyCode (KeyPress::returnKey))
{
triggerClick();
return true;
}
return false;
}
//==============================================================================
void Button::setRepeatSpeed (int initialDelayMillisecs,
int repeatMillisecs,
int minimumDelayInMillisecs) noexcept
{
autoRepeatDelay = initialDelayMillisecs;
autoRepeatSpeed = repeatMillisecs;
autoRepeatMinimumDelay = jmin (autoRepeatSpeed, minimumDelayInMillisecs);
}
void Button::repeatTimerCallback()
{
if (needsRepainting)
{
callbackHelper->stopTimer();
updateState();
needsRepainting = false;
}
else if (autoRepeatSpeed > 0 && (isKeyDown || (updateState() == buttonDown)))
{
auto repeatSpeed = autoRepeatSpeed;
if (autoRepeatMinimumDelay >= 0)
{
auto timeHeldDown = jmin (1.0, getMillisecondsSinceButtonDown() / 4000.0);
timeHeldDown *= timeHeldDown;
repeatSpeed = repeatSpeed + (int) (timeHeldDown * (autoRepeatMinimumDelay - repeatSpeed));
}
repeatSpeed = jmax (1, repeatSpeed);
auto now = Time::getMillisecondCounter();
// if we've been blocked from repeating often enough, speed up the repeat timer to compensate..
if (lastRepeatTime != 0 && (int) (now - lastRepeatTime) > repeatSpeed * 2)
repeatSpeed = jmax (1, repeatSpeed / 2);
lastRepeatTime = now;
callbackHelper->startTimer (repeatSpeed);
internalClickCallback (ModifierKeys::currentModifiers);
}
else if (! needsToRelease)
{
callbackHelper->stopTimer();
}
}
std::unique_ptr<AccessibilityHandler> Button::createAccessibilityHandler()
{
return std::make_unique<detail::ButtonAccessibilityHandler> (*this, AccessibilityRole::button);
}
} // namespace juce
@@ -0,0 +1,550 @@
/*
==============================================================================
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 base class for buttons.
This contains all the logic for button behaviours such as enabling/disabling,
responding to shortcut keystrokes, auto-repeating when held down, toggle-buttons
and radio groups, etc.
@see TextButton, DrawableButton, ToggleButton
@tags{GUI}
*/
class JUCE_API Button : public Component,
public SettableTooltipClient
{
protected:
//==============================================================================
/** Creates a button.
@param buttonName the text to put in the button (the component's name is also
initially set to this string, but these can be changed later
using the setName() and setButtonText() methods)
*/
explicit Button (const String& buttonName);
public:
/** Destructor. */
~Button() override;
//==============================================================================
/** Changes the button's text.
@see getButtonText
*/
void setButtonText (const String& newText);
/** Returns the text displayed in the button.
@see setButtonText
*/
const String& getButtonText() const { return text; }
//==============================================================================
/** Returns true if the button is currently being held down.
@see isOver
*/
bool isDown() const noexcept;
/** Returns true if the mouse is currently over the button.
This will be also be true if the button is being held down.
@see isDown
*/
bool isOver() const noexcept;
//==============================================================================
/** Indicates that the button's on/off state is toggleable.
By default this is false, and will only be true for ToggleButtons, buttons that
are a part of a radio button group, and buttons for which
getClickingTogglesState() == true, however you can use this method to manually
indicate that a button is toggleable.
This will present the button as toggleable to accessibility clients and add an
accessible "toggle" action for the button that invokes setToggleState().
@see ToggleButton, isToggleable, setToggleState, setClickingTogglesState, setRadioGroupId
*/
void setToggleable (bool shouldBeToggleable);
/** Returns true if the button's on/off state is toggleable.
@see setToggleable, setClickingTogglesState
*/
bool isToggleable() const noexcept { return canBeToggled || clickTogglesState; }
/** A button has an on/off state associated with it, and this changes that.
By default buttons are 'off' and for simple buttons that you click to perform
an action you won't change this. Toggle buttons, however will want to
change their state when turned on or off.
@param shouldBeOn whether to set the button's toggle state to be on or
off. If it's a member of a button group, this will
always try to turn it on, and to turn off any other
buttons in the group
@param notification determines the behaviour if the value changes - this
can invoke a synchronous call to clicked(), but
sendNotificationAsync is not supported
@see getToggleState, setRadioGroupId
*/
void setToggleState (bool shouldBeOn, NotificationType notification);
/** Returns true if the button is 'on'.
By default buttons are 'off' and for simple buttons that you click to perform
an action you won't change this. Toggle buttons, however will want to
change their state when turned on or off.
@see setToggleState
*/
bool getToggleState() const noexcept { return isOn.getValue(); }
/** Returns the Value object that represents the button's toggle state.
You can use this Value object to connect the button's state to external values or setters,
either by taking a copy of the Value, or by using Value::referTo() to make it point to
your own Value object.
@see getToggleState, Value
*/
Value& getToggleStateValue() noexcept { return isOn; }
/** This tells the button to automatically flip the toggle state when
the button is clicked.
If set to true, then before the clicked() callback occurs, the toggle-state
of the button is flipped. This will also cause isToggleable() to return true.
@see isToggleable
*/
void setClickingTogglesState (bool shouldAutoToggleOnClick) noexcept;
/** Returns true if this button is set to be an automatic toggle-button.
This returns the last value that was passed to setClickingTogglesState().
*/
bool getClickingTogglesState() const noexcept { return clickTogglesState; }
//==============================================================================
/** Enables the button to act as a member of a mutually-exclusive group
of 'radio buttons'.
If the group ID is set to a non-zero number, then this button will
act as part of a group of buttons with the same ID, only one of
which can be 'on' at the same time. Note that when it's part of
a group, clicking a toggle-button that's 'on' won't turn it off.
To find other buttons with the same ID, this button will search through
its sibling components for ToggleButtons, so all the buttons for a
particular group must be placed inside the same parent component.
Set the group ID back to zero if you want it to act as a normal toggle
button again.
The notification argument lets you specify how other buttons should react
to being turned on or off in response to this call.
@see getRadioGroupId
*/
void setRadioGroupId (int newGroupId, NotificationType notification = sendNotification);
/** Returns the ID of the group to which this button belongs.
(See setRadioGroupId() for an explanation of this).
*/
int getRadioGroupId() const noexcept { return radioGroupId; }
//==============================================================================
/**
Used to receive callbacks when a button is clicked.
@see Button::addListener, Button::removeListener
*/
class JUCE_API Listener
{
public:
/** Destructor. */
virtual ~Listener() = default;
/** Called when the button is clicked. */
virtual void buttonClicked (Button*) = 0;
/** Called when the button's state changes. */
virtual void buttonStateChanged (Button*) {}
};
/** Registers a listener to receive events when this button's state changes.
If the listener is already registered, this will not register it again.
@see removeListener
*/
void addListener (Listener* newListener);
/** Removes a previously-registered button listener
@see addListener
*/
void removeListener (Listener* listener);
//==============================================================================
/** You can assign a lambda to this callback object to have it called when the button is clicked. */
std::function<void()> onClick;
/** You can assign a lambda to this callback object to have it called when the button's state changes. */
std::function<void()> onStateChange;
//==============================================================================
/** Causes the button to act as if it's been clicked.
This will asynchronously make the button draw itself going down and up, and
will then call back the clicked() method as if mouse was clicked on it.
@see clicked
*/
virtual void triggerClick();
//==============================================================================
/** Sets a command ID for this button to automatically invoke when it's clicked.
When the button is pressed, it will use the given manager to trigger the
command ID.
Obviously be careful that the ApplicationCommandManager doesn't get deleted
before this button is. To disable the command triggering, call this method and
pass nullptr as the command manager.
If generateTooltip is true, then the button's tooltip will be automatically
generated based on the name of this command and its current shortcut key.
@see addShortcut, getCommandID
*/
void setCommandToTrigger (ApplicationCommandManager* commandManagerToUse,
CommandID commandID,
bool generateTooltip);
/** Returns the command ID that was set by setCommandToTrigger(). */
CommandID getCommandID() const noexcept { return commandID; }
//==============================================================================
/** Assigns a shortcut key to trigger the button.
The button registers itself with its top-level parent component for keypresses.
Note that a different way of linking buttons to keypresses is by using the
setCommandToTrigger() method to invoke a command.
@see clearShortcuts
*/
void addShortcut (const KeyPress&);
/** Removes all key shortcuts that had been set for this button.
@see addShortcut
*/
void clearShortcuts();
/** Returns true if the given keypress is a shortcut for this button.
@see addShortcut
*/
bool isRegisteredForShortcut (const KeyPress&) const;
//==============================================================================
/** Sets an auto-repeat speed for the button when it is held down.
(Auto-repeat is disabled by default).
@param initialDelayInMillisecs how long to wait after the mouse is pressed before
triggering the next click. If this is zero, auto-repeat
is disabled
@param repeatDelayInMillisecs the frequently subsequent repeated clicks should be
triggered
@param minimumDelayInMillisecs if this is greater than 0, the auto-repeat speed will
get faster, the longer the button is held down, up to the
minimum interval specified here
*/
void setRepeatSpeed (int initialDelayInMillisecs,
int repeatDelayInMillisecs,
int minimumDelayInMillisecs = -1) noexcept;
/** Sets whether the button click should happen when the mouse is pressed or released.
By default the button is only considered to have been clicked when the mouse is
released, but setting this to true will make it call the clicked() method as soon
as the button is pressed.
This is useful if the button is being used to show a pop-up menu, as it allows
the click to be used as a drag onto the menu.
*/
void setTriggeredOnMouseDown (bool isTriggeredOnMouseDown) noexcept;
/** Returns whether the button click happens when the mouse is pressed or released.
@see setTriggeredOnMouseDown
*/
bool getTriggeredOnMouseDown() const noexcept;
/** Returns the number of milliseconds since the last time the button
went into the 'down' state.
*/
uint32 getMillisecondsSinceButtonDown() const noexcept;
//==============================================================================
/** Sets the tooltip for this button.
@see TooltipClient, TooltipWindow
*/
void setTooltip (const String& newTooltip) override;
//==============================================================================
/** A combination of these flags are used by setConnectedEdges(). */
enum ConnectedEdgeFlags
{
ConnectedOnLeft = 1,
ConnectedOnRight = 2,
ConnectedOnTop = 4,
ConnectedOnBottom = 8
};
/** Hints about which edges of the button might be connected to adjoining buttons.
The value passed in is a bitwise combination of any of the values in the
ConnectedEdgeFlags enum.
E.g. if you are placing two buttons adjacent to each other, you could use this to
indicate which edges are touching, and the LookAndFeel might choose to drawn them
without rounded corners on the edges that connect. It's only a hint, so the
LookAndFeel can choose to ignore it if it's not relevant for this type of
button.
*/
void setConnectedEdges (int connectedEdgeFlags);
/** Returns the set of flags passed into setConnectedEdges(). */
int getConnectedEdgeFlags() const noexcept { return connectedEdgeFlags; }
/** Indicates whether the button adjoins another one on its left edge.
@see setConnectedEdges
*/
bool isConnectedOnLeft() const noexcept { return (connectedEdgeFlags & ConnectedOnLeft) != 0; }
/** Indicates whether the button adjoins another one on its right edge.
@see setConnectedEdges
*/
bool isConnectedOnRight() const noexcept { return (connectedEdgeFlags & ConnectedOnRight) != 0; }
/** Indicates whether the button adjoins another one on its top edge.
@see setConnectedEdges
*/
bool isConnectedOnTop() const noexcept { return (connectedEdgeFlags & ConnectedOnTop) != 0; }
/** Indicates whether the button adjoins another one on its bottom edge.
@see setConnectedEdges
*/
bool isConnectedOnBottom() const noexcept { return (connectedEdgeFlags & ConnectedOnBottom) != 0; }
//==============================================================================
/** Used by setState(). */
enum ButtonState
{
buttonNormal,
buttonOver,
buttonDown
};
/** Can be used to force the button into a particular state.
This only changes the button's appearance, it won't trigger a click, or stop any mouse-clicks
from happening.
The state that you set here will only last until it is automatically changed when the mouse
enters or exits the button, or the mouse-button is pressed or released.
*/
void setState (ButtonState newState);
/** Returns the button's current over/down/up state. */
ButtonState getState() const noexcept { return buttonState; }
//==============================================================================
/** This abstract base class is implemented by LookAndFeel classes to provide
button-drawing functionality.
*/
struct JUCE_API LookAndFeelMethods
{
virtual ~LookAndFeelMethods() = default;
virtual void drawButtonBackground (Graphics&, Button&, const Colour& backgroundColour,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) = 0;
virtual Font getTextButtonFont (TextButton&, int buttonHeight) = 0;
virtual int getTextButtonWidthToFitText (TextButton&, int buttonHeight) = 0;
/** Draws the text for a TextButton. */
virtual void drawButtonText (Graphics&, TextButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) = 0;
/** Draws the contents of a standard ToggleButton. */
virtual void drawToggleButton (Graphics&, ToggleButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) = 0;
virtual void changeToggleButtonWidthToFitText (ToggleButton&) = 0;
virtual void drawTickBox (Graphics&, Component&, float x, float y, float w, float h,
bool ticked, bool isEnabled,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) = 0;
virtual void drawDrawableButton (Graphics&, DrawableButton&,
bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) = 0;
};
//==============================================================================
#ifndef DOXYGEN
[[deprecated ("This method's parameters have changed.")]]
void setToggleState (bool, bool);
#endif
protected:
//==============================================================================
/** This method is called when the button has been clicked.
Subclasses can override this to perform whatever actions they need to do.
In general, you wouldn't use this method to receive clicks, but should get your callbacks
by attaching a std::function to the onClick callback, or adding a Button::Listener.
@see triggerClick, onClick
*/
virtual void clicked();
/** This method is called when the button has been clicked.
By default it just calls clicked(), but you might want to override it to handle
things like clicking when a modifier key is pressed, etc.
@see ModifierKeys
*/
virtual void clicked (const ModifierKeys& modifiers);
/** Subclasses should override this to actually paint the button's contents.
It's better to use this than the paint method, because it gives you information
about the over/down state of the button.
@param g the graphics context to use
@param shouldDrawButtonAsHighlighted true if the button is either in the 'over' or 'down' state
@param shouldDrawButtonAsDown true if the button should be drawn in the 'down' position
*/
virtual void paintButton (Graphics& g,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) = 0;
/** Called when the button's up/down/over state changes.
Subclasses can override this if they need to do something special when the button
goes up or down.
@see isDown, isOver
*/
virtual void buttonStateChanged();
//==============================================================================
/** @internal */
virtual void internalClickCallback (const ModifierKeys&);
/** @internal */
void handleCommandMessage (int commandId) override;
/** @internal */
void mouseEnter (const MouseEvent&) override;
/** @internal */
void mouseExit (const MouseEvent&) override;
/** @internal */
void mouseDown (const MouseEvent&) override;
/** @internal */
void mouseDrag (const MouseEvent&) override;
/** @internal */
void mouseUp (const MouseEvent&) override;
/** @internal */
bool keyPressed (const KeyPress&) override;
/** @internal */
using Component::keyStateChanged;
/** @internal */
void paint (Graphics&) override;
/** @internal */
void parentHierarchyChanged() override;
/** @internal */
void visibilityChanged() override;
/** @internal */
void focusGained (FocusChangeType) override;
/** @internal */
void focusLost (FocusChangeType) override;
/** @internal */
void enablementChanged() override;
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
private:
//==============================================================================
Array<KeyPress> shortcuts;
WeakReference<Component> keySource;
String text;
ListenerList<Listener> buttonListeners;
struct CallbackHelper;
std::unique_ptr<CallbackHelper> callbackHelper;
uint32 buttonPressTime = 0, lastRepeatTime = 0;
ApplicationCommandManager* commandManagerToUse = nullptr;
int autoRepeatDelay = -1, autoRepeatSpeed = 0, autoRepeatMinimumDelay = -1;
int radioGroupId = 0, connectedEdgeFlags = 0;
CommandID commandID = {};
ButtonState buttonState = buttonNormal, lastStatePainted = buttonNormal;
Value isOn;
bool canBeToggled = false;
bool lastToggleState = false;
bool clickTogglesState = false;
bool needsToRelease = false;
bool needsRepainting = false;
bool isKeyDown = false;
bool triggerOnMouseDown = false;
bool generateTooltip = false;
void checkToggleableState (bool wasToggleable);
void repeatTimerCallback();
bool keyStateChangedCallback();
void applicationCommandListChangeCallback();
void updateAutomaticTooltip (const ApplicationCommandInfo&);
ButtonState updateState();
ButtonState updateState (bool isOver, bool isDown);
bool isShortcutPressed() const;
void turnOffOtherButtonsInGroup (NotificationType click, NotificationType state);
void flashButtonState();
void sendClickMessage (const ModifierKeys&);
void sendStateMessage();
void setToggleState (bool shouldBeOn, NotificationType click, NotificationType state);
bool isMouseSourceOver (const MouseEvent& e);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Button)
};
} // namespace juce
@@ -0,0 +1,241 @@
/*
==============================================================================
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
{
DrawableButton::DrawableButton (const String& name, const DrawableButton::ButtonStyle buttonStyle)
: Button (name), style (buttonStyle)
{
}
DrawableButton::~DrawableButton()
{
}
//==============================================================================
static std::unique_ptr<Drawable> copyDrawableIfNotNull (const Drawable* const d)
{
if (d != nullptr)
return d->createCopy();
return {};
}
void DrawableButton::setImages (const Drawable* normal,
const Drawable* over,
const Drawable* down,
const Drawable* disabled,
const Drawable* normalOn,
const Drawable* overOn,
const Drawable* downOn,
const Drawable* disabledOn)
{
jassert (normal != nullptr); // you really need to give it at least a normal image..
normalImage = copyDrawableIfNotNull (normal);
overImage = copyDrawableIfNotNull (over);
downImage = copyDrawableIfNotNull (down);
disabledImage = copyDrawableIfNotNull (disabled);
normalImageOn = copyDrawableIfNotNull (normalOn);
overImageOn = copyDrawableIfNotNull (overOn);
downImageOn = copyDrawableIfNotNull (downOn);
disabledImageOn = copyDrawableIfNotNull (disabledOn);
currentImage = nullptr;
buttonStateChanged();
}
//==============================================================================
void DrawableButton::setButtonStyle (const DrawableButton::ButtonStyle newStyle)
{
if (style != newStyle)
{
style = newStyle;
buttonStateChanged();
}
}
void DrawableButton::setEdgeIndent (const int numPixelsIndent)
{
edgeIndent = numPixelsIndent;
repaint();
resized();
}
Rectangle<float> DrawableButton::getImageBounds() const
{
auto r = getLocalBounds();
if (style != ImageStretched)
{
auto indentX = jmin (edgeIndent, proportionOfWidth (0.3f));
auto indentY = jmin (edgeIndent, proportionOfHeight (0.3f));
if (shouldDrawButtonBackground())
{
indentX = jmax (getWidth() / 4, indentX);
indentY = jmax (getHeight() / 4, indentY);
}
else if (style == ImageAboveTextLabel)
{
r = r.withTrimmedBottom (jmin (16, proportionOfHeight (0.25f)));
}
r = r.reduced (indentX, indentY);
}
return r.toFloat();
}
void DrawableButton::resized()
{
Button::resized();
if (currentImage != nullptr)
{
if (style != ImageRaw)
{
int transformFlags = 0;
if (style == ImageStretched)
{
transformFlags |= RectanglePlacement::stretchToFit;
}
else
{
transformFlags |= RectanglePlacement::centred;
if (style == ImageOnButtonBackgroundOriginalSize)
transformFlags |= RectanglePlacement::doNotResize;
}
currentImage->setTransformToFit (getImageBounds(), transformFlags);
}
}
}
void DrawableButton::buttonStateChanged()
{
repaint();
Drawable* imageToDraw = nullptr;
float opacity = 1.0f;
if (isEnabled())
{
imageToDraw = getCurrentImage();
}
else
{
imageToDraw = getToggleState() ? disabledImageOn.get()
: disabledImage.get();
if (imageToDraw == nullptr)
{
opacity = 0.4f;
imageToDraw = getNormalImage();
}
}
if (imageToDraw != currentImage)
{
removeChildComponent (currentImage);
currentImage = imageToDraw;
if (currentImage != nullptr)
{
currentImage->setInterceptsMouseClicks (false, false);
addAndMakeVisible (currentImage);
resized();
}
}
if (currentImage != nullptr)
currentImage->setAlpha (opacity);
}
void DrawableButton::enablementChanged()
{
Button::enablementChanged();
buttonStateChanged();
}
void DrawableButton::colourChanged()
{
repaint();
}
void DrawableButton::paintButton (Graphics& g,
const bool shouldDrawButtonAsHighlighted,
const bool shouldDrawButtonAsDown)
{
auto& lf = getLookAndFeel();
if (shouldDrawButtonBackground())
lf.drawButtonBackground (g, *this,
findColour (getToggleState() ? TextButton::buttonOnColourId
: TextButton::buttonColourId),
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown);
else
lf.drawDrawableButton (g, *this, shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown);
}
//==============================================================================
Drawable* DrawableButton::getCurrentImage() const noexcept
{
if (isDown()) return getDownImage();
if (isOver()) return getOverImage();
return getNormalImage();
}
Drawable* DrawableButton::getNormalImage() const noexcept
{
return (getToggleState() && normalImageOn != nullptr) ? normalImageOn.get()
: normalImage.get();
}
Drawable* DrawableButton::getOverImage() const noexcept
{
if (getToggleState())
{
if (overImageOn != nullptr) return overImageOn.get();
if (normalImageOn != nullptr) return normalImageOn.get();
}
return overImage != nullptr ? overImage.get() : normalImage.get();
}
Drawable* DrawableButton::getDownImage() const noexcept
{
if (auto* d = getToggleState() ? downImageOn.get() : downImage.get())
return d;
return getOverImage();
}
} // 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
{
//==============================================================================
/**
A button that displays a Drawable.
Up to three Drawable objects can be given to this button, to represent the
'normal', 'over' and 'down' states.
@see Button
@tags{GUI}
*/
class JUCE_API DrawableButton : public Button
{
public:
//==============================================================================
enum ButtonStyle
{
ImageFitted, /**< The button will just display the images, but will resize and centre them to fit inside it. */
ImageRaw, /**< The button will just display the images in their normal size and position.
This leaves it up to the caller to make sure the images are the correct size and position for the button. */
ImageAboveTextLabel, /**< Draws the button as a text label across the bottom with the image resized and scaled to fit above it. */
ImageOnButtonBackground, /**< Draws the button as a standard rounded-rectangle button with the image on top. The image will be resized
to match the button's proportions.
Note that if you use this style, the colour IDs that control the button colour are
TextButton::buttonColourId and TextButton::buttonOnColourId. */
ImageOnButtonBackgroundOriginalSize, /** Same as ImageOnButtonBackground, but keeps the original image size. */
ImageStretched /**< Fills the button with a stretched version of the image. */
};
//==============================================================================
/** Creates a DrawableButton.
After creating one of these, use setImages() to specify the drawables to use.
@param buttonName the name to give the component
@param buttonStyle the layout to use
@see ButtonStyle, setButtonStyle, setImages
*/
DrawableButton (const String& buttonName,
ButtonStyle buttonStyle);
/** Destructor. */
~DrawableButton() override;
//==============================================================================
/** Sets up the images to draw for the various button states.
The button will keep its own internal copies of these drawables.
@param normalImage the thing to draw for the button's 'normal' state. An internal copy
will be made of the object passed-in if it is non-null.
@param overImage the thing to draw for the button's 'over' state - if this is
null, the button's normal image will be used when the mouse is
over it. An internal copy will be made of the object passed-in
if it is non-null.
@param downImage the thing to draw for the button's 'down' state - if this is
null, the 'over' image will be used instead (or the normal image
as a last resort). An internal copy will be made of the object
passed-in if it is non-null.
@param disabledImage an image to draw when the button is disabled. If this is null,
the normal image will be drawn with a reduced opacity instead.
An internal copy will be made of the object passed-in if it is
non-null.
@param normalImageOn same as the normalImage, but this is used when the button's toggle
state is 'on'. If this is nullptr, the normal image is used instead
@param overImageOn same as the overImage, but this is used when the button's toggle
state is 'on'. If this is nullptr, the normalImageOn is drawn instead
@param downImageOn same as the downImage, but this is used when the button's toggle
state is 'on'. If this is nullptr, the overImageOn is drawn instead
@param disabledImageOn same as the disabledImage, but this is used when the button's toggle
state is 'on'. If this is nullptr, the normal image will be drawn instead
with a reduced opacity
*/
void setImages (const Drawable* normalImage,
const Drawable* overImage = nullptr,
const Drawable* downImage = nullptr,
const Drawable* disabledImage = nullptr,
const Drawable* normalImageOn = nullptr,
const Drawable* overImageOn = nullptr,
const Drawable* downImageOn = nullptr,
const Drawable* disabledImageOn = nullptr);
//==============================================================================
/** Changes the button's style.
@see ButtonStyle
*/
void setButtonStyle (ButtonStyle newStyle);
/** Returns the current style. */
ButtonStyle getStyle() const noexcept { return style; }
//==============================================================================
/** Gives the button an optional amount of space around the edge of the drawable.
By default there's a gap of about 3 pixels.
*/
void setEdgeIndent (int numPixelsIndent);
/** Returns the current edge indent size. */
int getEdgeIndent() const noexcept { return edgeIndent; }
//==============================================================================
/** Returns the image that the button is currently displaying. */
Drawable* getCurrentImage() const noexcept;
/** Returns the image that the button will use for its normal state. */
Drawable* getNormalImage() const noexcept;
/** Returns the image that the button will use when the mouse is over it. */
Drawable* getOverImage() const noexcept;
/** Returns the image that the button will use when the mouse is held down on it. */
Drawable* getDownImage() const noexcept;
/** Can be overridden to specify a custom position for the image within the button. */
virtual Rectangle<float> getImageBounds() const;
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the link.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
Note that when the ImageOnButtonBackground style is used, the colour IDs that control
the button colour are TextButton::buttonColourId and TextButton::buttonOnColourId.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
textColourId = 0x1004010, /**< The colour to use for the button's text label. */
textColourOnId = 0x1004013, /**< The colour to use for the button's text when the button's toggle state is "on". */
backgroundColourId = 0x1004011, /**< The colour used to fill the button's background (when
the button is toggled 'off'). Note that if you use the
ImageOnButtonBackground style, you should use TextButton::buttonColourId
to change the button's colour. */
backgroundOnColourId = 0x1004012, /**< The colour used to fill the button's background (when
the button is toggled 'on'). Note that if you use the
ImageOnButtonBackground style, you should use TextButton::buttonOnColourId
to change the button's colour. */
};
//==============================================================================
/** @internal */
void paintButton (Graphics&, bool, bool) override;
/** @internal */
void buttonStateChanged() override;
/** @internal */
void resized() override;
/** @internal */
void enablementChanged() override;
/** @internal */
void colourChanged() override;
private:
//==============================================================================
bool shouldDrawButtonBackground() const { return style == ImageOnButtonBackground || style == ImageOnButtonBackgroundOriginalSize; }
//==============================================================================
ButtonStyle style;
std::unique_ptr<Drawable> normalImage, overImage, downImage, disabledImage,
normalImageOn, overImageOn, downImageOn, disabledImageOn;
Drawable* currentImage = nullptr;
int edgeIndent = 3;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DrawableButton)
};
} // namespace juce
@@ -0,0 +1,129 @@
/*
==============================================================================
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
{
HyperlinkButton::HyperlinkButton (const String& linkText,
const URL& linkURL)
: Button (linkText),
url (linkURL),
font (14.0f, Font::underlined),
resizeFont (true),
justification (Justification::centred)
{
setMouseCursor (MouseCursor::PointingHandCursor);
setTooltip (linkURL.toString (false));
}
HyperlinkButton::HyperlinkButton()
: Button (String()),
font (14.0f, Font::underlined),
resizeFont (true),
justification (Justification::centred)
{
setMouseCursor (MouseCursor::PointingHandCursor);
}
HyperlinkButton::~HyperlinkButton()
{
}
//==============================================================================
void HyperlinkButton::setFont (const Font& newFont,
const bool resizeToMatchComponentHeight,
Justification justificationType)
{
font = newFont;
resizeFont = resizeToMatchComponentHeight;
justification = justificationType;
repaint();
}
void HyperlinkButton::setURL (const URL& newURL) noexcept
{
url = newURL;
setTooltip (newURL.toString (false));
}
Font HyperlinkButton::getFontToUse() const
{
if (resizeFont)
return font.withHeight ((float) getHeight() * 0.7f);
return font;
}
void HyperlinkButton::changeWidthToFitText()
{
setSize (getFontToUse().getStringWidth (getButtonText()) + 6, getHeight());
}
void HyperlinkButton::setJustificationType (Justification newJustification)
{
if (justification != newJustification)
{
justification = newJustification;
repaint();
}
}
void HyperlinkButton::colourChanged()
{
repaint();
}
//==============================================================================
void HyperlinkButton::clicked()
{
if (url.isWellFormed())
url.launchInDefaultBrowser();
}
void HyperlinkButton::paintButton (Graphics& g,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown)
{
const Colour textColour (findColour (textColourId));
if (isEnabled())
g.setColour ((shouldDrawButtonAsHighlighted) ? textColour.darker ((shouldDrawButtonAsDown) ? 1.3f : 0.4f)
: textColour);
else
g.setColour (textColour.withMultipliedAlpha (0.4f));
g.setFont (getFontToUse());
g.drawText (getButtonText(), getLocalBounds().reduced (1, 0),
justification.getOnlyHorizontalFlags() | Justification::verticallyCentred,
true);
}
std::unique_ptr<AccessibilityHandler> HyperlinkButton::createAccessibilityHandler()
{
return std::make_unique<detail::ButtonAccessibilityHandler> (*this, AccessibilityRole::hyperlink);
}
} // namespace juce
@@ -0,0 +1,130 @@
/*
==============================================================================
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 button showing an underlined weblink, that will launch the link
when it's clicked.
@see Button
@tags{GUI}
*/
class JUCE_API HyperlinkButton : public Button
{
public:
//==============================================================================
/** Creates a HyperlinkButton.
@param linkText the text that will be displayed in the button - this is
also set as the Component's name, but the text can be
changed later with the Button::setButtonText() method
@param linkURL the URL to launch when the user clicks the button
*/
HyperlinkButton (const String& linkText,
const URL& linkURL);
/** Creates a HyperlinkButton. */
HyperlinkButton();
/** Destructor. */
~HyperlinkButton() override;
//==============================================================================
/** Changes the font to use for the text.
If resizeToMatchComponentHeight is true, the font's height will be adjusted
to match the size of the component.
*/
void setFont (const Font& newFont,
bool resizeToMatchComponentHeight,
Justification justificationType = Justification::horizontallyCentred);
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the link.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
textColourId = 0x1001f00, /**< The colour to use for the URL text. */
};
//==============================================================================
/** Changes the URL that the button will trigger. */
void setURL (const URL& newURL) noexcept;
/** Returns the URL that the button will trigger. */
const URL& getURL() const noexcept { return url; }
//==============================================================================
/** Resizes the button horizontally to fit snugly around the text.
This won't affect the button's height.
*/
void changeWidthToFitText();
//==============================================================================
/** Sets the style of justification to be used for positioning the text.
(The default is Justification::centred)
*/
void setJustificationType (Justification justification);
/** Returns the type of justification, as set in setJustificationType(). */
Justification getJustificationType() const noexcept { return justification; }
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
protected:
//==============================================================================
/** @internal */
void clicked() override;
/** @internal */
void colourChanged() override;
/** @internal */
void paintButton (Graphics&, bool, bool) override;
private:
//==============================================================================
using Button::clicked;
Font getFontToUse() const;
//==============================================================================
URL url;
Font font;
bool resizeFont;
Justification justification;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HyperlinkButton)
};
} // namespace juce
@@ -0,0 +1,198 @@
/*
==============================================================================
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
{
ImageButton::ImageButton (const String& text_)
: Button (text_),
scaleImageToFit (true),
preserveProportions (true),
alphaThreshold (0)
{
}
ImageButton::~ImageButton()
{
}
void ImageButton::setImages (const bool resizeButtonNowToFitThisImage,
const bool rescaleImagesWhenButtonSizeChanges,
const bool preserveImageProportions,
const Image& normalImage_,
const float imageOpacityWhenNormal,
Colour overlayColourWhenNormal,
const Image& overImage_,
const float imageOpacityWhenOver,
Colour overlayColourWhenOver,
const Image& downImage_,
const float imageOpacityWhenDown,
Colour overlayColourWhenDown,
const float hitTestAlphaThreshold)
{
normalImage = normalImage_;
overImage = overImage_;
downImage = downImage_;
if (resizeButtonNowToFitThisImage && normalImage.isValid())
{
imageBounds.setSize (normalImage.getWidth(),
normalImage.getHeight());
setSize (imageBounds.getWidth(), imageBounds.getHeight());
}
scaleImageToFit = rescaleImagesWhenButtonSizeChanges;
preserveProportions = preserveImageProportions;
normalOpacity = imageOpacityWhenNormal;
normalOverlay = overlayColourWhenNormal;
overOpacity = imageOpacityWhenOver;
overOverlay = overlayColourWhenOver;
downOpacity = imageOpacityWhenDown;
downOverlay = overlayColourWhenDown;
alphaThreshold = (uint8) jlimit (0, 0xff, roundToInt (255.0f * hitTestAlphaThreshold));
repaint();
}
Image ImageButton::getCurrentImage() const
{
if (isDown() || getToggleState())
return getDownImage();
if (isOver())
return getOverImage();
return getNormalImage();
}
Image ImageButton::getNormalImage() const
{
return normalImage;
}
Image ImageButton::getOverImage() const
{
return overImage.isValid() ? overImage
: normalImage;
}
Image ImageButton::getDownImage() const
{
return downImage.isValid() ? downImage
: getOverImage();
}
void ImageButton::paintButton (Graphics& g,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown)
{
if (! isEnabled())
{
shouldDrawButtonAsHighlighted = false;
shouldDrawButtonAsDown = false;
}
Image im (getCurrentImage());
if (im.isValid())
{
const int iw = im.getWidth();
const int ih = im.getHeight();
int w = getWidth();
int h = getHeight();
int x = (w - iw) / 2;
int y = (h - ih) / 2;
if (scaleImageToFit)
{
if (preserveProportions)
{
int newW, newH;
const float imRatio = (float) ih / (float) iw;
const float destRatio = (float) h / (float) w;
if (imRatio > destRatio)
{
newW = roundToInt ((float) h / imRatio);
newH = h;
}
else
{
newW = w;
newH = roundToInt ((float) w * imRatio);
}
x = (w - newW) / 2;
y = (h - newH) / 2;
w = newW;
h = newH;
}
else
{
x = 0;
y = 0;
}
}
if (! scaleImageToFit)
{
w = iw;
h = ih;
}
imageBounds.setBounds (x, y, w, h);
const bool useDownImage = shouldDrawButtonAsDown || getToggleState();
getLookAndFeel().drawImageButton (g, &im, x, y, w, h,
useDownImage ? downOverlay
: (shouldDrawButtonAsHighlighted ? overOverlay
: normalOverlay),
useDownImage ? downOpacity
: (shouldDrawButtonAsHighlighted ? overOpacity
: normalOpacity),
*this);
}
}
bool ImageButton::hitTest (int x, int y)
{
if (! Component::hitTest (x, y)) // handle setInterceptsMouseClicks
return false;
if (alphaThreshold == 0)
return true;
Image im (getCurrentImage());
return im.isNull() || ((! imageBounds.isEmpty())
&& alphaThreshold < im.getPixelAt (((x - imageBounds.getX()) * im.getWidth()) / imageBounds.getWidth(),
((y - imageBounds.getY()) * im.getHeight()) / imageBounds.getHeight()).getAlpha());
}
} // namespace juce
@@ -0,0 +1,161 @@
/*
==============================================================================
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
{
//==============================================================================
/**
As the title suggests, this is a button containing an image.
The colour and transparency of the image can be set to vary when the
button state changes.
@see Button, ShapeButton, TextButton
@tags{GUI}
*/
class JUCE_API ImageButton : public Button
{
public:
//==============================================================================
/** Creates an ImageButton.
Use setImage() to specify the image to use. The colours and opacities that
are specified here can be changed later using setImages().
@param name the name to give the component
*/
explicit ImageButton (const String& name = String());
/** Destructor. */
~ImageButton() override;
//==============================================================================
/** Sets up the images to draw in various states.
@param resizeButtonNowToFitThisImage if true, the button will be immediately
resized to the same dimensions as the normal image
@param rescaleImagesWhenButtonSizeChanges if true, the image will be rescaled to fit the
button when the button's size changes
@param preserveImageProportions if true then any rescaling of the image to fit
the button will keep the image's x and y proportions
correct - i.e. it won't distort its shape, although
this might create gaps around the edges
@param normalImage the image to use when the button is in its normal state.
button no longer needs it.
@param imageOpacityWhenNormal the opacity to use when drawing the normal image.
@param overlayColourWhenNormal an overlay colour to use to fill the alpha channel of the
normal image - if this colour is transparent, no overlay
will be drawn. The overlay will be drawn over the top of the
image, so you can basically add a solid or semi-transparent
colour to the image to brighten or darken it
@param overImage the image to use when the mouse is over the button. If
you want to use the same image as was set in the normalImage
parameter, this value can be a null image.
@param imageOpacityWhenOver the opacity to use when drawing the image when the mouse
is over the button
@param overlayColourWhenOver an overlay colour to use to fill the alpha channel of the
image when the mouse is over - if this colour is transparent,
no overlay will be drawn
@param downImage an image to use when the button is pressed down. If set
to a null image, the 'over' image will be drawn instead (or the
normal image if there isn't an 'over' image either).
@param imageOpacityWhenDown the opacity to use when drawing the image when the button
is pressed
@param overlayColourWhenDown an overlay colour to use to fill the alpha channel of the
image when the button is pressed down - if this colour is
transparent, no overlay will be drawn
@param hitTestAlphaThreshold if set to zero, the mouse is considered to be over the button
whenever it's inside the button's bounding rectangle. If
set to values higher than 0, the mouse will only be
considered to be over the image when the value of the
image's alpha channel at that position is greater than
this level.
*/
void setImages (bool resizeButtonNowToFitThisImage,
bool rescaleImagesWhenButtonSizeChanges,
bool preserveImageProportions,
const Image& normalImage,
float imageOpacityWhenNormal,
Colour overlayColourWhenNormal,
const Image& overImage,
float imageOpacityWhenOver,
Colour overlayColourWhenOver,
const Image& downImage,
float imageOpacityWhenDown,
Colour overlayColourWhenDown,
float hitTestAlphaThreshold = 0.0f);
/** Returns the currently set 'normal' image. */
Image getNormalImage() const;
/** Returns the image that's drawn when the mouse is over the button.
If a valid 'over' image has been set, this will return it; otherwise it'll
just return the normal image.
*/
Image getOverImage() const;
/** Returns the image that's drawn when the button is held down.
If a valid 'down' image has been set, this will return it; otherwise it'll
return the 'over' image or normal image, depending on what's available.
*/
Image getDownImage() const;
//==============================================================================
/** This abstract base class is implemented by LookAndFeel classes. */
struct JUCE_API LookAndFeelMethods
{
virtual ~LookAndFeelMethods() = default;
virtual void drawImageButton (Graphics&, Image*,
int imageX, int imageY, int imageW, int imageH,
const Colour& overlayColour, float imageOpacity, ImageButton&) = 0;
};
protected:
//==============================================================================
/** @internal */
bool hitTest (int x, int y) override;
/** @internal */
void paintButton (Graphics&, bool, bool) override;
private:
//==============================================================================
bool scaleImageToFit, preserveProportions;
uint8 alphaThreshold;
Rectangle<int> imageBounds;
Image normalImage, overImage, downImage;
float normalOpacity, overOpacity, downOpacity;
Colour normalOverlay, overOverlay, downOverlay;
Image getCurrentImage() const;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ImageButton)
};
} // namespace juce
@@ -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
{
ShapeButton::ShapeButton (const String& t, Colour n, Colour o, Colour d)
: Button (t),
normalColour (n), overColour (o), downColour (d),
normalColourOn (n), overColourOn (o), downColourOn (d),
useOnColours (false),
maintainShapeProportions (false),
outlineWidth (0.0f)
{
}
ShapeButton::~ShapeButton() {}
void ShapeButton::setColours (Colour newNormalColour, Colour newOverColour, Colour newDownColour)
{
normalColour = newNormalColour;
overColour = newOverColour;
downColour = newDownColour;
}
void ShapeButton::setOnColours (Colour newNormalColourOn, Colour newOverColourOn, Colour newDownColourOn)
{
normalColourOn = newNormalColourOn;
overColourOn = newOverColourOn;
downColourOn = newDownColourOn;
}
void ShapeButton::shouldUseOnColours (bool shouldUse)
{
useOnColours = shouldUse;
}
void ShapeButton::setOutline (Colour newOutlineColour, const float newOutlineWidth)
{
outlineColour = newOutlineColour;
outlineWidth = newOutlineWidth;
}
void ShapeButton::setBorderSize (BorderSize<int> newBorder)
{
border = newBorder;
}
void ShapeButton::setShape (const Path& newShape,
const bool resizeNowToFitThisShape,
const bool maintainShapeProportions_,
const bool hasShadow)
{
shape = newShape;
maintainShapeProportions = maintainShapeProportions_;
shadow.setShadowProperties (DropShadow (Colours::black.withAlpha (0.5f), 3, Point<int>()));
setComponentEffect (hasShadow ? &shadow : nullptr);
if (resizeNowToFitThisShape)
{
auto newBounds = shape.getBounds();
if (hasShadow)
newBounds = newBounds.expanded (4.0f);
shape.applyTransform (AffineTransform::translation (-newBounds.getX(),
-newBounds.getY()));
setSize (1 + (int) (newBounds.getWidth() + outlineWidth) + border.getLeftAndRight(),
1 + (int) (newBounds.getHeight() + outlineWidth) + border.getTopAndBottom());
}
repaint();
}
void ShapeButton::paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown)
{
if (! isEnabled())
{
shouldDrawButtonAsHighlighted = false;
shouldDrawButtonAsDown = false;
}
auto r = border.subtractedFrom (getLocalBounds())
.toFloat()
.reduced (outlineWidth * 0.5f);
if (getComponentEffect() != nullptr)
r = r.reduced (2.0f);
if (shouldDrawButtonAsDown)
{
const float sizeReductionWhenPressed = 0.04f;
r = r.reduced (sizeReductionWhenPressed * r.getWidth(),
sizeReductionWhenPressed * r.getHeight());
}
auto trans = shape.getTransformToScaleToFit (r, maintainShapeProportions);
if (shouldDrawButtonAsDown) g.setColour (getToggleState() && useOnColours ? downColourOn : downColour);
else if (shouldDrawButtonAsHighlighted) g.setColour (getToggleState() && useOnColours ? overColourOn : overColour);
else g.setColour (getToggleState() && useOnColours ? normalColourOn : normalColour);
g.fillPath (shape, trans);
if (outlineWidth > 0.0f)
{
g.setColour (outlineColour);
g.strokePath (shape, PathStrokeType (outlineWidth), trans);
}
}
} // namespace juce
@@ -0,0 +1,126 @@
/*
==============================================================================
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 button that contains a filled shape.
@see Button, ImageButton, TextButton, ArrowButton
@tags{GUI}
*/
class JUCE_API ShapeButton : public Button
{
public:
//==============================================================================
/** Creates a ShapeButton.
@param name a name to give the component - see Component::setName()
@param normalColour the colour to fill the shape with when the mouse isn't over
@param overColour the colour to use when the mouse is over the shape
@param downColour the colour to use when the button is in the pressed-down state
*/
ShapeButton (const String& name,
Colour normalColour,
Colour overColour,
Colour downColour);
/** Destructor. */
~ShapeButton() override;
//==============================================================================
/** Sets the shape to use.
@param newShape the shape to use
@param resizeNowToFitThisShape if true, the button will be resized to fit the shape's bounds
@param maintainShapeProportions if true, the shape's proportions will be kept fixed when
the button is resized
@param hasDropShadow if true, the button will be given a drop-shadow effect
*/
void setShape (const Path& newShape,
bool resizeNowToFitThisShape,
bool maintainShapeProportions,
bool hasDropShadow);
/** Set the colours to use for drawing the shape.
@param normalColour the colour to fill the shape with when the mouse isn't over
@param overColour the colour to use when the mouse is over the shape
@param downColour the colour to use when the button is in the pressed-down state
*/
void setColours (Colour normalColour,
Colour overColour,
Colour downColour);
/** Sets the colours to use for drawing the shape when the button's toggle state is 'on'. To enable this behaviour, use the
shouldUseOnColours() method.
@param normalColourOn the colour to fill the shape with when the mouse isn't over and the button's toggle state is 'on'
@param overColourOn the colour to use when the mouse is over the shape and the button's toggle state is 'on'
@param downColourOn the colour to use when the button is in the pressed-down state and the button's toggle state is 'on'
*/
void setOnColours (Colour normalColourOn,
Colour overColourOn,
Colour downColourOn);
/** Set whether the button should use the 'on' set of colours when its toggle state is 'on'.
By default these will be the same as the normal colours but the setOnColours method can be
used to provide a different set of colours.
*/
void shouldUseOnColours (bool shouldUse);
/** Sets up an outline to draw around the shape.
@param outlineColour the colour to use
@param outlineStrokeWidth the thickness of line to draw
*/
void setOutline (Colour outlineColour, float outlineStrokeWidth);
/** This lets you specify a border to be left around the edge of the button when
drawing the shape.
*/
void setBorderSize (BorderSize<int> border);
/** @internal */
void paintButton (Graphics&, bool, bool) override;
private:
//==============================================================================
Colour normalColour, overColour, downColour,
normalColourOn, overColourOn, downColourOn, outlineColour;
bool useOnColours;
DropShadowEffect shadow;
Path shape;
BorderSize<int> border;
bool maintainShapeProportions;
float outlineWidth;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ShapeButton)
};
} // namespace juce
@@ -0,0 +1,77 @@
/*
==============================================================================
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
{
TextButton::TextButton() : Button (String())
{
}
TextButton::TextButton (const String& name) : Button (name)
{
}
TextButton::TextButton (const String& name, const String& toolTip) : Button (name)
{
setTooltip (toolTip);
}
TextButton::~TextButton()
{
}
void TextButton::paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown)
{
auto& lf = getLookAndFeel();
lf.drawButtonBackground (g, *this,
findColour (getToggleState() ? buttonOnColourId : buttonColourId),
shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown);
lf.drawButtonText (g, *this, shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown);
}
void TextButton::colourChanged()
{
repaint();
}
void TextButton::changeWidthToFitText()
{
changeWidthToFitText (getHeight());
}
void TextButton::changeWidthToFitText (const int newHeight)
{
setSize (getBestWidthForHeight (newHeight), newHeight);
}
int TextButton::getBestWidthForHeight (int buttonHeight)
{
return getLookAndFeel().getTextButtonWidthToFitText (*this, buttonHeight);
}
} // namespace juce
@@ -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.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A button that uses the standard lozenge-shaped background with a line of
text on it.
@see Button, DrawableButton
@tags{GUI}
*/
class JUCE_API TextButton : public Button
{
public:
//==============================================================================
/** Creates a TextButton. */
TextButton();
/** Creates a TextButton.
@param buttonName the text to put in the button (the component's name is also
initially set to this string, but these can be changed later
using the setName() and setButtonText() methods)
*/
explicit TextButton (const String& buttonName);
/** Creates a TextButton.
@param buttonName the text to put in the button (the component's name is also
initially set to this string, but these can be changed later
using the setName() and setButtonText() methods)
@param toolTip an optional string to use as a tooltip
*/
TextButton (const String& buttonName, const String& toolTip);
/** Destructor. */
~TextButton() override;
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the button.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
buttonColourId = 0x1000100, /**< The colour used to fill the button shape (when the button is toggled
'off'). The look-and-feel class might re-interpret this to add
effects, etc. */
buttonOnColourId = 0x1000101, /**< The colour used to fill the button shape (when the button is toggled
'on'). The look-and-feel class might re-interpret this to add
effects, etc. */
textColourOffId = 0x1000102, /**< The colour to use for the button's text when the button's toggle state is "off". */
textColourOnId = 0x1000103 /**< The colour to use for the button's text.when the button's toggle state is "on". */
};
//==============================================================================
/** Changes this button's width to fit neatly around its current text, without
changing its height.
*/
void changeWidthToFitText();
/** Resizes the button's width to fit neatly around its current text, and gives it
the specified height.
*/
void changeWidthToFitText (int newHeight);
/** Returns the width that the LookAndFeel suggests would be best for this button if it
had the given height.
*/
int getBestWidthForHeight (int buttonHeight);
//==============================================================================
/** @internal */
void paintButton (Graphics&, bool, bool) override;
/** @internal */
void colourChanged() override;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextButton)
};
} // namespace juce
@@ -0,0 +1,65 @@
/*
==============================================================================
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
{
ToggleButton::ToggleButton()
: Button (String())
{
setClickingTogglesState (true);
}
ToggleButton::ToggleButton (const String& buttonText)
: Button (buttonText)
{
setClickingTogglesState (true);
}
ToggleButton::~ToggleButton()
{
}
void ToggleButton::paintButton (Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown)
{
getLookAndFeel().drawToggleButton (g, *this, shouldDrawButtonAsHighlighted, shouldDrawButtonAsDown);
}
void ToggleButton::changeWidthToFitText()
{
getLookAndFeel().changeToggleButtonWidthToFitText (*this);
}
void ToggleButton::colourChanged()
{
repaint();
}
std::unique_ptr<AccessibilityHandler> ToggleButton::createAccessibilityHandler()
{
return std::make_unique<detail::ButtonAccessibilityHandler> (*this, AccessibilityRole::toggleButton);
}
} // namespace juce
@@ -0,0 +1,93 @@
/*
==============================================================================
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 button that can be toggled on/off.
All buttons can be toggle buttons, but this lets you create one of the
standard ones which has a tick-box and a text label next to it.
@see Button, DrawableButton, TextButton
@tags{GUI}
*/
class JUCE_API ToggleButton : public Button
{
public:
//==============================================================================
/** Creates a ToggleButton. */
ToggleButton();
/** Creates a ToggleButton.
@param buttonText the text to put in the button (the component's name is also
initially set to this string, but these can be changed later
using the setName() and setButtonText() methods)
*/
explicit ToggleButton (const String& buttonText);
/** Destructor. */
~ToggleButton() override;
//==============================================================================
/** Resizes the button to fit neatly around its current text.
The button's height won't be affected, only its width.
*/
void changeWidthToFitText();
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the button.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
textColourId = 0x1006501, /**< The colour to use for the button's text. */
tickColourId = 0x1006502, /**< The colour to use for the tick mark. */
tickDisabledColourId = 0x1006503 /**< The colour to use for the disabled tick mark and/or outline. */
};
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
protected:
//==============================================================================
/** @internal */
void paintButton (Graphics&, bool, bool) override;
/** @internal */
void colourChanged() override;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToggleButton)
};
} // namespace juce
@@ -0,0 +1,113 @@
/*
==============================================================================
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
{
ToolbarButton::ToolbarButton (const int iid, const String& buttonText,
std::unique_ptr<Drawable> normalIm,
std::unique_ptr<Drawable> toggledOnIm)
: ToolbarItemComponent (iid, buttonText, true),
normalImage (std::move (normalIm)),
toggledOnImage (std::move (toggledOnIm))
{
jassert (normalImage != nullptr);
}
ToolbarButton::~ToolbarButton()
{
}
//==============================================================================
bool ToolbarButton::getToolbarItemSizes (int toolbarDepth, bool /*isToolbarVertical*/, int& preferredSize, int& minSize, int& maxSize)
{
preferredSize = minSize = maxSize = toolbarDepth;
return true;
}
void ToolbarButton::paintButtonArea (Graphics&, int /*width*/, int /*height*/, bool /*isMouseOver*/, bool /*isMouseDown*/)
{
}
void ToolbarButton::contentAreaChanged (const Rectangle<int>&)
{
buttonStateChanged();
}
void ToolbarButton::setCurrentImage (Drawable* const newImage)
{
if (newImage != currentImage)
{
removeChildComponent (currentImage);
currentImage = newImage;
if (currentImage != nullptr)
{
enablementChanged();
addAndMakeVisible (currentImage);
updateDrawable();
}
}
}
void ToolbarButton::updateDrawable()
{
if (currentImage != nullptr)
{
currentImage->setInterceptsMouseClicks (false, false);
currentImage->setTransformToFit (getContentArea().toFloat(), RectanglePlacement::centred);
currentImage->setAlpha (isEnabled() ? 1.0f : 0.5f);
}
}
void ToolbarButton::resized()
{
ToolbarItemComponent::resized();
updateDrawable();
}
void ToolbarButton::enablementChanged()
{
ToolbarItemComponent::enablementChanged();
updateDrawable();
}
Drawable* ToolbarButton::getImageToUse() const
{
if (getStyle() == Toolbar::textOnly)
return nullptr;
if (getToggleState() && toggledOnImage != nullptr)
return toggledOnImage.get();
return normalImage.get();
}
void ToolbarButton::buttonStateChanged()
{
setCurrentImage (getImageToUse());
}
} // namespace juce
@@ -0,0 +1,98 @@
/*
==============================================================================
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 type of button designed to go on a toolbar.
This simple button can have two Drawable objects specified - one for normal
use and another one (optionally) for the button's "on" state if it's a
toggle button.
@see Toolbar, ToolbarItemFactory, ToolbarItemComponent, Drawable, Button
@tags{GUI}
*/
class JUCE_API ToolbarButton : public ToolbarItemComponent
{
public:
//==============================================================================
/** Creates a ToolbarButton.
@param itemId the ID for this toolbar item type. This is passed through to the
ToolbarItemComponent constructor
@param labelText the text to display on the button (if the toolbar is using a style
that shows text labels). This is passed through to the
ToolbarItemComponent constructor
@param normalImage a drawable object that the button should use as its icon. The object
that is passed-in here will be kept by this object and will be
deleted when no longer needed or when this button is deleted.
@param toggledOnImage a drawable object that the button can use as its icon if the button
is in a toggled-on state (see the Button::getToggleState() method). If
nullptr is passed-in here, then the normal image will be used instead,
regardless of the toggle state. The object that is passed-in here will be
owned by this object and will be deleted when no longer needed or when
this button is deleted.
*/
ToolbarButton (int itemId,
const String& labelText,
std::unique_ptr<Drawable> normalImage,
std::unique_ptr<Drawable> toggledOnImage);
/** Destructor. */
~ToolbarButton() override;
//==============================================================================
/** @internal */
bool getToolbarItemSizes (int toolbarDepth, bool isToolbarVertical, int& preferredSize,
int& minSize, int& maxSize) override;
/** @internal */
void paintButtonArea (Graphics&, int width, int height, bool isMouseOver, bool isMouseDown) override;
/** @internal */
void contentAreaChanged (const Rectangle<int>&) override;
/** @internal */
void buttonStateChanged() override;
/** @internal */
void resized() override;
/** @internal */
void enablementChanged() override;
private:
//==============================================================================
std::unique_ptr<Drawable> normalImage, toggledOnImage;
Drawable* currentImage = nullptr;
void updateDrawable();
Drawable* getImageToUse() const;
void setCurrentImage (Drawable*);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToolbarButton)
};
} // namespace juce
@@ -0,0 +1,90 @@
/*
==============================================================================
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 type used to hold the unique ID for an application command.
This is a numeric type, so it can be stored as an integer.
@see ApplicationCommandInfo, ApplicationCommandManager,
ApplicationCommandTarget, KeyPressMappingSet
*/
using CommandID = int;
//==============================================================================
/** A set of general-purpose application command IDs.
Because these commands are likely to be used in most apps, they're defined
here to help different apps to use the same numeric values for them.
Of course you don't have to use these, but some of them are used internally by
JUCE - e.g. the quit ID is recognised as a command by the JUCEApplication class.
@see ApplicationCommandInfo, ApplicationCommandManager,
ApplicationCommandTarget, KeyPressMappingSet
*/
namespace StandardApplicationCommandIDs
{
enum
{
/** This command ID should be used to send a "Quit the App" command.
This command is recognised by the JUCEApplication class, so if it is invoked
and no other ApplicationCommandTarget handles the event first, the JUCEApplication
object will catch it and call JUCEApplicationBase::systemRequestedQuit().
*/
quit = 0x1001,
/** The command ID that should be used to send a "Delete" command. */
del = 0x1002,
/** The command ID that should be used to send a "Cut" command. */
cut = 0x1003,
/** The command ID that should be used to send a "Copy to clipboard" command. */
copy = 0x1004,
/** The command ID that should be used to send a "Paste from clipboard" command. */
paste = 0x1005,
/** The command ID that should be used to send a "Select all" command. */
selectAll = 0x1006,
/** The command ID that should be used to send a "Deselect all" command. */
deselectAll = 0x1007,
/** The command ID that should be used to send a "undo" command. */
undo = 0x1008,
/** The command ID that should be used to send a "redo" command. */
redo = 0x1009
};
}
} // namespace juce
@@ -0,0 +1,66 @@
/*
==============================================================================
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
{
ApplicationCommandInfo::ApplicationCommandInfo (const CommandID cid) noexcept
: commandID (cid), flags (0)
{
}
void ApplicationCommandInfo::setInfo (const String& shortName_,
const String& description_,
const String& categoryName_,
const int flags_) noexcept
{
shortName = shortName_;
description = description_;
categoryName = categoryName_;
flags = flags_;
}
void ApplicationCommandInfo::setActive (const bool b) noexcept
{
if (b)
flags &= ~isDisabled;
else
flags |= isDisabled;
}
void ApplicationCommandInfo::setTicked (const bool b) noexcept
{
if (b)
flags |= isTicked;
else
flags &= ~isTicked;
}
void ApplicationCommandInfo::addDefaultKeypress (const int keyCode, ModifierKeys modifiers) noexcept
{
defaultKeypresses.add (KeyPress (keyCode, modifiers, 0));
}
} // namespace juce
@@ -0,0 +1,190 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Holds information describing an application command.
This object is used to pass information about a particular command, such as its
name, description and other usage flags.
When an ApplicationCommandTarget is asked to provide information about the commands
it can perform, this is the structure gets filled-in to describe each one.
@see ApplicationCommandTarget, ApplicationCommandTarget::getCommandInfo(),
ApplicationCommandManager
@tags{GUI}
*/
struct JUCE_API ApplicationCommandInfo
{
//==============================================================================
explicit ApplicationCommandInfo (CommandID commandID) noexcept;
//==============================================================================
/** Sets a number of the structures values at once.
The meanings of each of the parameters is described below, in the appropriate
member variable's description.
*/
void setInfo (const String& shortName,
const String& description,
const String& categoryName,
int flags) noexcept;
/** An easy way to set or remove the isDisabled bit in the structure's flags field.
If isActive is true, the flags member has the isDisabled bit cleared; if isActive
is false, the bit is set.
*/
void setActive (bool isActive) noexcept;
/** An easy way to set or remove the isTicked bit in the structure's flags field.
*/
void setTicked (bool isTicked) noexcept;
/** Handy method for adding a keypress to the defaultKeypresses array.
This is just so you can write things like:
@code
myinfo.addDefaultKeypress ('s', ModifierKeys::commandModifier);
@endcode
instead of
@code
myinfo.defaultKeypresses.add (KeyPress ('s', ModifierKeys::commandModifier));
@endcode
*/
void addDefaultKeypress (int keyCode, ModifierKeys modifiers) noexcept;
//==============================================================================
/** The command's unique ID number.
*/
CommandID commandID;
/** A short name to describe the command.
This should be suitable for use in menus, on buttons that trigger the command, etc.
You can use the setInfo() method to quickly set this and some of the command's
other properties.
*/
String shortName;
/** A longer description of the command.
This should be suitable for use in contexts such as a KeyMappingEditorComponent or
pop-up tooltip describing what the command does.
You can use the setInfo() method to quickly set this and some of the command's
other properties.
*/
String description;
/** A named category that the command fits into.
You can give your commands any category you like, and these will be displayed in
contexts such as the KeyMappingEditorComponent, where the category is used to group
commands together.
You can use the setInfo() method to quickly set this and some of the command's
other properties.
*/
String categoryName;
/** A list of zero or more keypresses that should be used as the default keys for
this command.
Methods such as KeyPressMappingSet::resetToDefaultMappings() will use the keypresses in
this list to initialise the default set of key-to-command mappings.
@see addDefaultKeypress
*/
Array<KeyPress> defaultKeypresses;
//==============================================================================
/** Flags describing the ways in which this command should be used.
A bitwise-OR of these values is stored in the ApplicationCommandInfo::flags
variable.
*/
enum CommandFlags
{
/** Indicates that the command can't currently be performed.
The ApplicationCommandTarget::getCommandInfo() method must set this flag if it's
not currently permissible to perform the command. If the flag is set, then
components that trigger the command, e.g. PopupMenu, may choose to grey-out the
command or show themselves as not being enabled.
@see ApplicationCommandInfo::setActive
*/
isDisabled = 1 << 0,
/** Indicates that the command should have a tick next to it on a menu.
If your command is shown on a menu and this is set, it'll show a tick next to
it. Other components such as buttons may also use this flag to indicate that it
is a value that can be toggled, and is currently in the 'on' state.
@see ApplicationCommandInfo::setTicked
*/
isTicked = 1 << 1,
/** If this flag is present, then when a KeyPressMappingSet invokes the command,
it will call the command twice, once on key-down and again on key-up.
@see ApplicationCommandTarget::InvocationInfo
*/
wantsKeyUpDownCallbacks = 1 << 2,
/** If this flag is present, then a KeyMappingEditorComponent will not display the
command in its list.
*/
hiddenFromKeyEditor = 1 << 3,
/** If this flag is present, then a KeyMappingEditorComponent will display the
command in its list, but won't allow the assigned keypress to be changed.
*/
readOnlyInKeyEditor = 1 << 4,
/** If this flag is present and the command is invoked from a keypress, then any
buttons or menus that are also connected to the command will not flash to
indicate that they've been triggered.
*/
dontTriggerVisualFeedback = 1 << 5
};
/** A bitwise-OR of the values specified in the CommandFlags enum.
You can use the setInfo() method to quickly set this and some of the command's
other properties.
*/
int flags;
};
} // namespace juce
@@ -0,0 +1,322 @@
/*
==============================================================================
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
{
ApplicationCommandManager::ApplicationCommandManager()
{
keyMappings.reset (new KeyPressMappingSet (*this));
Desktop::getInstance().addFocusChangeListener (this);
}
ApplicationCommandManager::~ApplicationCommandManager()
{
Desktop::getInstance().removeFocusChangeListener (this);
keyMappings.reset();
}
//==============================================================================
void ApplicationCommandManager::clearCommands()
{
commands.clear();
keyMappings->clearAllKeyPresses();
triggerAsyncUpdate();
}
void ApplicationCommandManager::registerCommand (const ApplicationCommandInfo& newCommand)
{
// zero isn't a valid command ID!
jassert (newCommand.commandID != 0);
// the name isn't optional!
jassert (newCommand.shortName.isNotEmpty());
if (auto* command = getMutableCommandForID (newCommand.commandID))
{
// Trying to re-register the same command ID with different parameters can often indicate a typo.
// This assertion is here because I've found it useful catching some mistakes, but it may also cause
// false alarms if you're deliberately updating some flags for a command.
jassert (newCommand.shortName == getCommandForID (newCommand.commandID)->shortName
&& newCommand.categoryName == getCommandForID (newCommand.commandID)->categoryName
&& newCommand.defaultKeypresses == getCommandForID (newCommand.commandID)->defaultKeypresses
&& (newCommand.flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor))
== (getCommandForID (newCommand.commandID)->flags & (ApplicationCommandInfo::wantsKeyUpDownCallbacks | ApplicationCommandInfo::hiddenFromKeyEditor | ApplicationCommandInfo::readOnlyInKeyEditor)));
*command = newCommand;
}
else
{
auto* newInfo = new ApplicationCommandInfo (newCommand);
newInfo->flags &= ~ApplicationCommandInfo::isTicked;
commands.add (newInfo);
keyMappings->resetToDefaultMapping (newCommand.commandID);
triggerAsyncUpdate();
}
}
void ApplicationCommandManager::registerAllCommandsForTarget (ApplicationCommandTarget* target)
{
if (target != nullptr)
{
Array<CommandID> commandIDs;
target->getAllCommands (commandIDs);
for (int i = 0; i < commandIDs.size(); ++i)
{
ApplicationCommandInfo info (commandIDs.getUnchecked (i));
target->getCommandInfo (info.commandID, info);
registerCommand (info);
}
}
}
void ApplicationCommandManager::removeCommand (const CommandID commandID)
{
for (int i = commands.size(); --i >= 0;)
{
if (commands.getUnchecked (i)->commandID == commandID)
{
commands.remove (i);
triggerAsyncUpdate();
const Array<KeyPress> keys (keyMappings->getKeyPressesAssignedToCommand (commandID));
for (int j = keys.size(); --j >= 0;)
keyMappings->removeKeyPress (keys.getReference (j));
}
}
}
void ApplicationCommandManager::commandStatusChanged()
{
triggerAsyncUpdate();
}
//==============================================================================
ApplicationCommandInfo* ApplicationCommandManager::getMutableCommandForID (CommandID commandID) const noexcept
{
for (int i = commands.size(); --i >= 0;)
if (commands.getUnchecked (i)->commandID == commandID)
return commands.getUnchecked (i);
return nullptr;
}
const ApplicationCommandInfo* ApplicationCommandManager::getCommandForID (CommandID commandID) const noexcept
{
return getMutableCommandForID (commandID);
}
String ApplicationCommandManager::getNameOfCommand (CommandID commandID) const noexcept
{
if (auto* ci = getCommandForID (commandID))
return ci->shortName;
return {};
}
String ApplicationCommandManager::getDescriptionOfCommand (CommandID commandID) const noexcept
{
if (auto* ci = getCommandForID (commandID))
return ci->description.isNotEmpty() ? ci->description
: ci->shortName;
return {};
}
StringArray ApplicationCommandManager::getCommandCategories() const
{
StringArray s;
for (int i = 0; i < commands.size(); ++i)
s.addIfNotAlreadyThere (commands.getUnchecked (i)->categoryName, false);
return s;
}
Array<CommandID> ApplicationCommandManager::getCommandsInCategory (const String& categoryName) const
{
Array<CommandID> results;
for (int i = 0; i < commands.size(); ++i)
if (commands.getUnchecked (i)->categoryName == categoryName)
results.add (commands.getUnchecked (i)->commandID);
return results;
}
//==============================================================================
bool ApplicationCommandManager::invokeDirectly (CommandID commandID, bool asynchronously)
{
ApplicationCommandTarget::InvocationInfo info (commandID);
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::direct;
return invoke (info, asynchronously);
}
bool ApplicationCommandManager::invoke (const ApplicationCommandTarget::InvocationInfo& inf, bool asynchronously)
{
// This call isn't thread-safe for use from a non-UI thread without locking the message
// manager first..
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
bool ok = false;
ApplicationCommandInfo commandInfo (0);
if (auto* target = getTargetForCommand (inf.commandID, commandInfo))
{
ApplicationCommandTarget::InvocationInfo info (inf);
info.commandFlags = commandInfo.flags;
sendListenerInvokeCallback (info);
ok = target->invoke (info, asynchronously);
commandStatusChanged();
}
return ok;
}
//==============================================================================
ApplicationCommandTarget* ApplicationCommandManager::getFirstCommandTarget (CommandID)
{
return firstTarget != nullptr ? firstTarget
: findDefaultComponentTarget();
}
void ApplicationCommandManager::setFirstCommandTarget (ApplicationCommandTarget* newTarget) noexcept
{
firstTarget = newTarget;
}
ApplicationCommandTarget* ApplicationCommandManager::getTargetForCommand (CommandID commandID,
ApplicationCommandInfo& upToDateInfo)
{
auto* target = getFirstCommandTarget (commandID);
if (target == nullptr)
target = JUCEApplication::getInstance();
if (target != nullptr)
target = target->getTargetForCommand (commandID);
if (target != nullptr)
{
upToDateInfo.commandID = commandID;
target->getCommandInfo (commandID, upToDateInfo);
}
return target;
}
//==============================================================================
ApplicationCommandTarget* ApplicationCommandManager::findTargetForComponent (Component* c)
{
auto* target = dynamic_cast<ApplicationCommandTarget*> (c);
if (target == nullptr && c != nullptr)
target = c->findParentComponentOfClass<ApplicationCommandTarget>();
return target;
}
ApplicationCommandTarget* ApplicationCommandManager::findDefaultComponentTarget()
{
auto* c = Component::getCurrentlyFocusedComponent();
if (c == nullptr)
{
if (auto* activeWindow = TopLevelWindow::getActiveTopLevelWindow())
{
if (auto* peer = activeWindow->getPeer())
{
c = peer->getLastFocusedSubcomponent();
if (c == nullptr)
c = activeWindow;
}
}
}
if (c == nullptr)
{
auto& desktop = Desktop::getInstance();
// getting a bit desperate now: try all desktop comps..
for (int i = desktop.getNumComponents(); --i >= 0;)
if (auto* component = desktop.getComponent (i))
if (detail::WindowingHelpers::isForegroundOrEmbeddedProcess (component))
if (auto* peer = component->getPeer())
if (auto* target = findTargetForComponent (peer->getLastFocusedSubcomponent()))
return target;
}
if (c != nullptr)
{
// if we're focused on a ResizableWindow, chances are that it's the content
// component that really should get the event. And if not, the event will
// still be passed up to the top level window anyway, so let's send it to the
// content comp.
if (auto* resizableWindow = dynamic_cast<ResizableWindow*> (c))
if (auto* content = resizableWindow->getContentComponent())
c = content;
if (auto* target = findTargetForComponent (c))
return target;
}
return JUCEApplication::getInstance();
}
//==============================================================================
void ApplicationCommandManager::addListener (ApplicationCommandManagerListener* listener)
{
listeners.add (listener);
}
void ApplicationCommandManager::removeListener (ApplicationCommandManagerListener* listener)
{
listeners.remove (listener);
}
void ApplicationCommandManager::sendListenerInvokeCallback (const ApplicationCommandTarget::InvocationInfo& info)
{
listeners.call ([&] (ApplicationCommandManagerListener& l) { l.applicationCommandInvoked (info); });
}
void ApplicationCommandManager::handleAsyncUpdate()
{
listeners.call ([] (ApplicationCommandManagerListener& l) { l.applicationCommandListChanged(); });
}
void ApplicationCommandManager::globalFocusChanged (Component*)
{
commandStatusChanged();
}
} // namespace juce
@@ -0,0 +1,349 @@
/*
==============================================================================
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
{
//==============================================================================
/**
One of these objects holds a list of all the commands your app can perform,
and despatches these commands when needed.
Application commands are a good way to trigger actions in your app, e.g. "Quit",
"Copy", "Paste", etc. Menus, buttons and keypresses can all be given commands
to invoke automatically, which means you don't have to handle the result of a menu
or button click manually. Commands are despatched to ApplicationCommandTarget objects
which can choose which events they want to handle.
This architecture also allows for nested ApplicationCommandTargets, so that for example
you could have two different objects, one inside the other, both of which can respond to
a "delete" command. Depending on which one has focus, the command will be sent to the
appropriate place, regardless of whether it was triggered by a menu, keypress or some other
method.
To set up your app to use commands, you'll need to do the following:
- Create a global ApplicationCommandManager to hold the list of all possible
commands. (This will also manage a set of key-mappings for them).
- Make some of your UI components (or other objects) inherit from ApplicationCommandTarget.
This allows the object to provide a list of commands that it can perform, and
to handle them.
- Register each type of command using ApplicationCommandManager::registerAllCommandsForTarget(),
or ApplicationCommandManager::registerCommand().
- If you want key-presses to trigger your commands, use the ApplicationCommandManager::getKeyMappings()
method to access the key-mapper object, which you will need to register as a key-listener
in whatever top-level component you're using. See the KeyPressMappingSet class for more help
about setting this up.
- Use methods such as PopupMenu::addCommandItem() or Button::setCommandToTrigger() to
cause these commands to be invoked automatically.
- Commands can be invoked directly by your code using ApplicationCommandManager::invokeDirectly().
When a command is invoked, the ApplicationCommandManager will try to choose the best
ApplicationCommandTarget to receive the specified command. To do this it will use the
current keyboard focus to see which component might be interested, and will search the
component hierarchy for those that also implement the ApplicationCommandTarget interface.
If an ApplicationCommandTarget isn't interested in the command that is being invoked, then
the next one in line will be tried (see the ApplicationCommandTarget::getNextCommandTarget()
method), and so on until ApplicationCommandTarget::getNextCommandTarget() returns nullptr.
At this point if the command still hasn't been performed, it will be passed to the current
JUCEApplication object (which is itself an ApplicationCommandTarget).
To exert some custom control over which ApplicationCommandTarget is chosen to invoke a command,
you can override the ApplicationCommandManager::getFirstCommandTarget() method and choose
the object yourself.
@see ApplicationCommandTarget, ApplicationCommandInfo
@tags{GUI}
*/
class JUCE_API ApplicationCommandManager : private AsyncUpdater,
private FocusChangeListener
{
public:
//==============================================================================
/** Creates an ApplicationCommandManager.
Once created, you'll need to register all your app's commands with it, using
ApplicationCommandManager::registerAllCommandsForTarget() or
ApplicationCommandManager::registerCommand().
*/
ApplicationCommandManager();
/** Destructor.
Make sure that you don't delete this if pointers to it are still being used by
objects such as PopupMenus or Buttons.
*/
~ApplicationCommandManager() override;
//==============================================================================
/** Clears the current list of all commands.
Note that this will also clear the contents of the KeyPressMappingSet.
*/
void clearCommands();
/** Adds a command to the list of registered commands.
@see registerAllCommandsForTarget
*/
void registerCommand (const ApplicationCommandInfo& newCommand);
/** Adds all the commands that this target publishes to the manager's list.
This will use ApplicationCommandTarget::getAllCommands() and ApplicationCommandTarget::getCommandInfo()
to get details about all the commands that this target can do, and will call
registerCommand() to add each one to the manger's list.
@see registerCommand
*/
void registerAllCommandsForTarget (ApplicationCommandTarget* target);
/** Removes the command with a specified ID.
Note that this will also remove any key mappings that are mapped to the command.
*/
void removeCommand (CommandID commandID);
/** This should be called to tell the manager that one of its registered commands may have changed
its active status.
Because the command manager only finds out whether a command is active or inactive by querying
the current ApplicationCommandTarget, this is used to tell it that things may have changed. It
allows things like buttons to update their enablement, etc.
This method will cause an asynchronous call to ApplicationCommandManagerListener::applicationCommandListChanged()
for any registered listeners.
*/
void commandStatusChanged();
//==============================================================================
/** Returns the number of commands that have been registered.
@see registerCommand
*/
int getNumCommands() const noexcept { return commands.size(); }
/** Returns the details about one of the registered commands.
The index is between 0 and (getNumCommands() - 1).
*/
const ApplicationCommandInfo* getCommandForIndex (int index) const noexcept { return commands [index]; }
/** Returns the details about a given command ID.
This will search the list of registered commands for one with the given command
ID number, and return its associated info. If no matching command is found, this
will return nullptr.
*/
const ApplicationCommandInfo* getCommandForID (CommandID commandID) const noexcept;
/** Returns the name field for a command.
An empty string is returned if no command with this ID has been registered.
@see getDescriptionOfCommand
*/
String getNameOfCommand (CommandID commandID) const noexcept;
/** Returns the description field for a command.
An empty string is returned if no command with this ID has been registered. If the
command has no description, this will return its short name field instead.
@see getNameOfCommand
*/
String getDescriptionOfCommand (CommandID commandID) const noexcept;
/** Returns the list of categories.
This will go through all registered commands, and return a list of all the distinct
categoryName values from their ApplicationCommandInfo structure.
@see getCommandsInCategory()
*/
StringArray getCommandCategories() const;
/** Returns a list of all the command UIDs in a particular category.
@see getCommandCategories()
*/
Array<CommandID> getCommandsInCategory (const String& categoryName) const;
//==============================================================================
/** Returns the manager's internal set of key mappings.
This object can be used to edit the keypresses. To actually link this object up
to invoke commands when a key is pressed, see the comments for the KeyPressMappingSet
class.
@see KeyPressMappingSet
*/
KeyPressMappingSet* getKeyMappings() const noexcept { return keyMappings.get(); }
//==============================================================================
/** Invokes the given command directly, sending it to the default target.
This is just an easy way to call invoke() without having to fill out the InvocationInfo
structure.
*/
bool invokeDirectly (CommandID commandID, bool asynchronously);
/** Sends a command to the default target.
This will choose a target using getFirstCommandTarget(), and send the specified command
to it using the ApplicationCommandTarget::invoke() method. This means that if the
first target can't handle the command, it will be passed on to targets further down the
chain (see ApplicationCommandTarget::invoke() for more info).
@param invocationInfo this must be correctly filled-in, describing the context for
the invocation.
@param asynchronously if false, the command will be performed before this method returns.
If true, a message will be posted so that the command will be performed
later on the message thread, and this method will return immediately.
@see ApplicationCommandTarget::invoke
*/
bool invoke (const ApplicationCommandTarget::InvocationInfo& invocationInfo,
bool asynchronously);
//==============================================================================
/** Chooses the ApplicationCommandTarget to which a command should be sent.
Whenever the manager needs to know which target a command should be sent to, it calls
this method to determine the first one to try.
By default, this method will return the target that was set by calling setFirstCommandTarget().
If no target is set, it will return the result of findDefaultComponentTarget().
If you need to make sure all commands go via your own custom target, then you can
either use setFirstCommandTarget() to specify a single target, or override this method
if you need more complex logic to choose one.
It may return nullptr if no targets are available.
@see getTargetForCommand, invoke, invokeDirectly
*/
virtual ApplicationCommandTarget* getFirstCommandTarget (CommandID commandID);
/** Sets a target to be returned by getFirstCommandTarget().
If this is set to nullptr, then getFirstCommandTarget() will by default return the
result of findDefaultComponentTarget().
If you use this to set a target, make sure you call setFirstCommandTarget (nullptr)
before deleting the target object.
*/
void setFirstCommandTarget (ApplicationCommandTarget* newTarget) noexcept;
/** Tries to find the best target to use to perform a given command.
This will call getFirstCommandTarget() to find the preferred target, and will
check whether that target can handle the given command. If it can't, then it'll use
ApplicationCommandTarget::getNextCommandTarget() to find the next one to try, and
so on until no more are available.
If no targets are found that can perform the command, this method will return nullptr.
If a target is found, then it will get the target to fill-in the upToDateInfo
structure with the latest info about that command, so that the caller can see
whether the command is disabled, ticked, etc.
*/
ApplicationCommandTarget* getTargetForCommand (CommandID commandID,
ApplicationCommandInfo& upToDateInfo);
//==============================================================================
/** Registers a listener that will be called when various events occur. */
void addListener (ApplicationCommandManagerListener* listener);
/** Deregisters a previously-added listener. */
void removeListener (ApplicationCommandManagerListener* listener);
//==============================================================================
/** Looks for a suitable command target based on which Components have the keyboard focus.
This is used by the default implementation of ApplicationCommandTarget::getFirstCommandTarget(),
but is exposed here in case it's useful.
It tries to pick the best ApplicationCommandTarget by looking at focused components, top level
windows, etc., and using the findTargetForComponent() method.
*/
static ApplicationCommandTarget* findDefaultComponentTarget();
/** Examines this component and all its parents in turn, looking for the first one
which is an ApplicationCommandTarget.
Returns the first ApplicationCommandTarget that it finds, or nullptr if none of them
implement that class.
*/
static ApplicationCommandTarget* findTargetForComponent (Component*);
private:
//==============================================================================
OwnedArray<ApplicationCommandInfo> commands;
ListenerList<ApplicationCommandManagerListener> listeners;
std::unique_ptr<KeyPressMappingSet> keyMappings;
ApplicationCommandTarget* firstTarget = nullptr;
void sendListenerInvokeCallback (const ApplicationCommandTarget::InvocationInfo&);
void handleAsyncUpdate() override;
void globalFocusChanged (Component*) override;
ApplicationCommandInfo* getMutableCommandForID (CommandID) const noexcept;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ApplicationCommandManager)
};
//==============================================================================
/**
A listener that receives callbacks from an ApplicationCommandManager when
commands are invoked or the command list is changed.
@see ApplicationCommandManager::addListener, ApplicationCommandManager::removeListener
@tags{GUI}
*/
class JUCE_API ApplicationCommandManagerListener
{
public:
//==============================================================================
/** Destructor. */
virtual ~ApplicationCommandManagerListener() = default;
/** Called when an app command is about to be invoked. */
virtual void applicationCommandInvoked (const ApplicationCommandTarget::InvocationInfo&) = 0;
/** Called when commands are registered or deregistered from the
command manager, or when commands are made active or inactive.
Note that if you're using this to watch for changes to whether a command is disabled,
you'll need to make sure that ApplicationCommandManager::commandStatusChanged() is called
whenever the status of your command might have changed.
*/
virtual void applicationCommandListChanged() = 0;
};
} // namespace juce
@@ -0,0 +1,186 @@
/*
==============================================================================
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 ApplicationCommandTarget::CommandMessage final : public MessageManager::MessageBase
{
public:
CommandMessage (ApplicationCommandTarget* const target, const InvocationInfo& inf)
: owner (target), info (inf)
{
}
void messageCallback() override
{
if (ApplicationCommandTarget* const target = owner)
target->tryToInvoke (info, false);
}
private:
WeakReference<ApplicationCommandTarget> owner;
const InvocationInfo info;
JUCE_DECLARE_NON_COPYABLE (CommandMessage)
};
//==============================================================================
ApplicationCommandTarget::ApplicationCommandTarget() {}
ApplicationCommandTarget::~ApplicationCommandTarget() {}
//==============================================================================
bool ApplicationCommandTarget::tryToInvoke (const InvocationInfo& info, const bool async)
{
if (isCommandActive (info.commandID))
{
if (async)
{
(new CommandMessage (this, info))->post();
return true;
}
if (perform (info))
return true;
// Hmm.. your target claimed that it could perform this command, but failed to do so.
// If it can't do it at the moment for some reason, it should clear the 'isActive' flag
// when it returns the command's info.
jassertfalse;
}
return false;
}
ApplicationCommandTarget* ApplicationCommandTarget::findFirstTargetParentComponent()
{
if (Component* const c = dynamic_cast<Component*> (this))
return c->findParentComponentOfClass<ApplicationCommandTarget>();
return nullptr;
}
ApplicationCommandTarget* ApplicationCommandTarget::getTargetForCommand (const CommandID commandID)
{
ApplicationCommandTarget* target = this;
int depth = 0;
while (target != nullptr)
{
Array<CommandID> commandIDs;
target->getAllCommands (commandIDs);
if (commandIDs.contains (commandID))
return target;
target = target->getNextCommandTarget();
++depth;
jassert (depth < 100); // could be a recursive command chain??
jassert (target != this); // definitely a recursive command chain!
if (depth > 100 || target == this)
break;
}
if (target == nullptr)
{
target = JUCEApplication::getInstance();
if (target != nullptr)
{
Array<CommandID> commandIDs;
target->getAllCommands (commandIDs);
if (commandIDs.contains (commandID))
return target;
}
}
return nullptr;
}
bool ApplicationCommandTarget::isCommandActive (const CommandID commandID)
{
ApplicationCommandInfo info (commandID);
info.flags = ApplicationCommandInfo::isDisabled;
getCommandInfo (commandID, info);
return (info.flags & ApplicationCommandInfo::isDisabled) == 0;
}
//==============================================================================
bool ApplicationCommandTarget::invoke (const InvocationInfo& info, const bool async)
{
ApplicationCommandTarget* target = this;
int depth = 0;
while (target != nullptr)
{
if (target->tryToInvoke (info, async))
return true;
target = target->getNextCommandTarget();
++depth;
jassert (depth < 100); // could be a recursive command chain??
jassert (target != this); // definitely a recursive command chain!
if (depth > 100 || target == this)
break;
}
if (target == nullptr)
{
target = JUCEApplication::getInstance();
if (target != nullptr)
return target->tryToInvoke (info, async);
}
return false;
}
bool ApplicationCommandTarget::invokeDirectly (const CommandID commandID, const bool asynchronously)
{
ApplicationCommandTarget::InvocationInfo info (commandID);
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::direct;
return invoke (info, asynchronously);
}
//==============================================================================
ApplicationCommandTarget::InvocationInfo::InvocationInfo (const CommandID command)
: commandID (command),
commandFlags (0),
invocationMethod (direct),
originatingComponent (nullptr),
isKeyDown (false),
millisecsSinceKeyPressed (0)
{
}
} // namespace juce
@@ -0,0 +1,244 @@
/*
==============================================================================
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 command target publishes a list of command IDs that it can perform.
An ApplicationCommandManager despatches commands to targets, which must be
able to provide information about what commands they can handle.
To create a target, you'll need to inherit from this class, implementing all of
its pure virtual methods.
For info about how a target is chosen to receive a command, see
ApplicationCommandManager::getFirstCommandTarget().
@see ApplicationCommandManager, ApplicationCommandInfo
@tags{GUI}
*/
class JUCE_API ApplicationCommandTarget
{
public:
//==============================================================================
/** Creates a command target. */
ApplicationCommandTarget();
/** Destructor. */
virtual ~ApplicationCommandTarget();
//==============================================================================
/**
Contains contextual details about the invocation of a command.
*/
struct JUCE_API InvocationInfo
{
//==============================================================================
InvocationInfo (CommandID commandID);
//==============================================================================
/** The UID of the command that should be performed. */
CommandID commandID;
/** The command's flags.
See ApplicationCommandInfo for a description of these flag values.
*/
int commandFlags;
//==============================================================================
/** The types of context in which the command might be called. */
enum InvocationMethod
{
direct = 0, /**< The command is being invoked directly by a piece of code. */
fromKeyPress, /**< The command is being invoked by a key-press. */
fromMenu, /**< The command is being invoked by a menu selection. */
fromButton /**< The command is being invoked by a button click. */
};
/** The type of event that triggered this command. */
InvocationMethod invocationMethod;
//==============================================================================
/** If triggered by a keypress or menu, this will be the component that had the
keyboard focus at the time.
If triggered by a button, it may be set to that component, or it may be null.
*/
Component* originatingComponent;
//==============================================================================
/** The keypress that was used to invoke it.
Note that this will be an invalid keypress if the command was invoked
by some other means than a keyboard shortcut.
*/
KeyPress keyPress;
/** True if the callback is being invoked when the key is pressed,
false if the key is being released.
@see KeyPressMappingSet::addCommand()
*/
bool isKeyDown;
/** If the key is being released, this indicates how long it had been held
down for.
(Only relevant if isKeyDown is false.)
*/
int millisecsSinceKeyPressed;
};
//==============================================================================
/** This must return the next target to try after this one.
When a command is being sent, and the first target can't handle
that command, this method is used to determine the next target that should
be tried.
It may return nullptr if it doesn't know of another target.
If your target is a Component, you would usually use the findFirstTargetParentComponent()
method to return a parent component that might want to handle it.
@see invoke
*/
virtual ApplicationCommandTarget* getNextCommandTarget() = 0;
/** This must return a complete list of commands that this target can handle.
Your target should add all the command IDs that it handles to the array that is
passed-in.
*/
virtual void getAllCommands (Array<CommandID>& commands) = 0;
/** This must provide details about one of the commands that this target can perform.
This will be called with one of the command IDs that the target provided in its
getAllCommands() methods.
It should fill-in all appropriate fields of the ApplicationCommandInfo structure with
suitable information about the command. (The commandID field will already have been filled-in
by the caller).
The easiest way to set the info is using the ApplicationCommandInfo::setInfo() method to
set all the fields at once.
If the command is currently inactive for some reason, this method must use
ApplicationCommandInfo::setActive() to make that clear, (or it should set the isDisabled
bit of the ApplicationCommandInfo::flags field).
Any default key-presses for the command should be appended to the
ApplicationCommandInfo::defaultKeypresses field.
Note that if you change something that affects the status of the commands
that would be returned by this method (e.g. something that makes some commands
active or inactive), you should call ApplicationCommandManager::commandStatusChanged()
to cause the manager to refresh its status.
*/
virtual void getCommandInfo (CommandID commandID, ApplicationCommandInfo& result) = 0;
/** This must actually perform the specified command.
If this target is able to perform the command specified by the commandID field of the
InvocationInfo structure, then it should do so, and must return true.
If it can't handle this command, it should return false, which tells the caller to pass
the command on to the next target in line.
@see invoke, ApplicationCommandManager::invoke
*/
virtual bool perform (const InvocationInfo& info) = 0;
//==============================================================================
/** Makes this target invoke a command.
Your code can call this method to invoke a command on this target, but normally
you'd call it indirectly via ApplicationCommandManager::invoke() or
ApplicationCommandManager::invokeDirectly().
If this target can perform the given command, it will call its perform() method to
do so. If not, then getNextCommandTarget() will be used to determine the next target
to try, and the command will be passed along to it.
@param invocationInfo this must be correctly filled-in, describing the context for
the invocation.
@param asynchronously if false, the command will be performed before this method returns.
If true, a message will be posted so that the command will be performed
later on the message thread, and this method will return immediately.
@see perform, ApplicationCommandManager::invoke
*/
bool invoke (const InvocationInfo& invocationInfo,
bool asynchronously);
/** Invokes a given command directly on this target.
This is just an easy way to call invoke() without having to fill out the InvocationInfo
structure.
*/
bool invokeDirectly (CommandID commandID,
bool asynchronously);
//==============================================================================
/** Searches this target and all subsequent ones for the first one that can handle
the specified command.
This will use getNextCommandTarget() to determine the chain of targets to try
after this one.
*/
ApplicationCommandTarget* getTargetForCommand (CommandID commandID);
/** Checks whether this command can currently be performed by this target.
This will return true only if a call to getCommandInfo() doesn't set the
isDisabled flag to indicate that the command is inactive.
*/
bool isCommandActive (CommandID commandID);
/** If this object is a Component, this method will search upwards in its current
UI hierarchy for the next parent component that implements the
ApplicationCommandTarget class.
If your target is a Component, this is a very handy method to use in your
getNextCommandTarget() implementation.
*/
ApplicationCommandTarget* findFirstTargetParentComponent();
private:
//==============================================================================
class CommandMessage;
friend class CommandMessage;
bool tryToInvoke (const InvocationInfo&, bool async);
JUCE_DECLARE_WEAK_REFERENCEABLE (ApplicationCommandTarget)
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ApplicationCommandTarget)
};
} // 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
{
KeyPressMappingSet::KeyPressMappingSet (ApplicationCommandManager& cm)
: commandManager (cm)
{
Desktop::getInstance().addFocusChangeListener (this);
}
KeyPressMappingSet::KeyPressMappingSet (const KeyPressMappingSet& other)
: KeyListener(), ChangeBroadcaster(), FocusChangeListener(), commandManager (other.commandManager)
{
Desktop::getInstance().addFocusChangeListener (this);
}
KeyPressMappingSet::~KeyPressMappingSet()
{
Desktop::getInstance().removeFocusChangeListener (this);
}
//==============================================================================
Array<KeyPress> KeyPressMappingSet::getKeyPressesAssignedToCommand (const CommandID commandID) const
{
for (int i = 0; i < mappings.size(); ++i)
if (mappings.getUnchecked (i)->commandID == commandID)
return mappings.getUnchecked (i)->keypresses;
return {};
}
void KeyPressMappingSet::addKeyPress (const CommandID commandID, const KeyPress& newKeyPress, int insertIndex)
{
// If you specify an upper-case letter but no shift key, how is the user supposed to press it!?
// Stick to lower-case letters when defining a keypress, to avoid ambiguity.
jassert (! (CharacterFunctions::isUpperCase (newKeyPress.getTextCharacter())
&& ! newKeyPress.getModifiers().isShiftDown()));
if (findCommandForKeyPress (newKeyPress) != commandID)
{
if (newKeyPress.isValid())
{
for (int i = mappings.size(); --i >= 0;)
{
if (mappings.getUnchecked (i)->commandID == commandID)
{
mappings.getUnchecked (i)->keypresses.insert (insertIndex, newKeyPress);
sendChangeMessage();
return;
}
}
if (const ApplicationCommandInfo* const ci = commandManager.getCommandForID (commandID))
{
CommandMapping* const cm = new CommandMapping();
cm->commandID = commandID;
cm->keypresses.add (newKeyPress);
cm->wantsKeyUpDownCallbacks = (ci->flags & ApplicationCommandInfo::wantsKeyUpDownCallbacks) != 0;
mappings.add (cm);
sendChangeMessage();
}
else
{
// If you hit this, you're trying to attach a keypress to a command ID that
// doesn't exist, so the key is not being attached.
jassertfalse;
}
}
}
}
static void addKeyPresses (KeyPressMappingSet& set, const ApplicationCommandInfo* const ci)
{
for (int j = 0; j < ci->defaultKeypresses.size(); ++j)
set.addKeyPress (ci->commandID, ci->defaultKeypresses.getReference (j));
}
void KeyPressMappingSet::resetToDefaultMappings()
{
mappings.clear();
for (int i = 0; i < commandManager.getNumCommands(); ++i)
addKeyPresses (*this, commandManager.getCommandForIndex (i));
sendChangeMessage();
}
void KeyPressMappingSet::resetToDefaultMapping (const CommandID commandID)
{
clearAllKeyPresses (commandID);
if (const ApplicationCommandInfo* const ci = commandManager.getCommandForID (commandID))
addKeyPresses (*this, ci);
}
void KeyPressMappingSet::clearAllKeyPresses()
{
if (mappings.size() > 0)
{
sendChangeMessage();
mappings.clear();
}
}
void KeyPressMappingSet::clearAllKeyPresses (const CommandID commandID)
{
for (int i = mappings.size(); --i >= 0;)
{
if (mappings.getUnchecked (i)->commandID == commandID)
{
mappings.remove (i);
sendChangeMessage();
}
}
}
void KeyPressMappingSet::removeKeyPress (const KeyPress& keypress)
{
if (keypress.isValid())
{
for (int i = mappings.size(); --i >= 0;)
{
CommandMapping& cm = *mappings.getUnchecked (i);
for (int j = cm.keypresses.size(); --j >= 0;)
{
if (keypress == cm.keypresses [j])
{
cm.keypresses.remove (j);
sendChangeMessage();
}
}
}
}
}
void KeyPressMappingSet::removeKeyPress (const CommandID commandID, const int keyPressIndex)
{
for (int i = mappings.size(); --i >= 0;)
{
if (mappings.getUnchecked (i)->commandID == commandID)
{
mappings.getUnchecked (i)->keypresses.remove (keyPressIndex);
sendChangeMessage();
break;
}
}
}
//==============================================================================
CommandID KeyPressMappingSet::findCommandForKeyPress (const KeyPress& keyPress) const noexcept
{
for (int i = 0; i < mappings.size(); ++i)
if (mappings.getUnchecked (i)->keypresses.contains (keyPress))
return mappings.getUnchecked (i)->commandID;
return 0;
}
bool KeyPressMappingSet::containsMapping (const CommandID commandID, const KeyPress& keyPress) const noexcept
{
for (int i = mappings.size(); --i >= 0;)
if (mappings.getUnchecked (i)->commandID == commandID)
return mappings.getUnchecked (i)->keypresses.contains (keyPress);
return false;
}
void KeyPressMappingSet::invokeCommand (const CommandID commandID,
const KeyPress& key,
const bool isKeyDown,
const int millisecsSinceKeyPressed,
Component* const originatingComponent) const
{
ApplicationCommandTarget::InvocationInfo info (commandID);
info.invocationMethod = ApplicationCommandTarget::InvocationInfo::fromKeyPress;
info.isKeyDown = isKeyDown;
info.keyPress = key;
info.millisecsSinceKeyPressed = millisecsSinceKeyPressed;
info.originatingComponent = originatingComponent;
commandManager.invoke (info, false);
}
//==============================================================================
bool KeyPressMappingSet::restoreFromXml (const XmlElement& xmlVersion)
{
if (xmlVersion.hasTagName ("KEYMAPPINGS"))
{
if (xmlVersion.getBoolAttribute ("basedOnDefaults", true))
{
// if the XML was created as a set of differences from the default mappings,
// (i.e. by calling createXml (true)), then we need to first restore the defaults.
resetToDefaultMappings();
}
else
{
// if the XML was created calling createXml (false), then we need to clear all
// the keys and treat the xml as describing the entire set of mappings.
clearAllKeyPresses();
}
for (auto* map : xmlVersion.getChildIterator())
{
const CommandID commandId = map->getStringAttribute ("commandId").getHexValue32();
if (commandId != 0)
{
auto key = KeyPress::createFromDescription (map->getStringAttribute ("key"));
if (map->hasTagName ("MAPPING"))
{
addKeyPress (commandId, key);
}
else if (map->hasTagName ("UNMAPPING"))
{
for (auto& m : mappings)
if (m->commandID == commandId)
m->keypresses.removeAllInstancesOf (key);
}
}
}
return true;
}
return false;
}
std::unique_ptr<XmlElement> KeyPressMappingSet::createXml (const bool saveDifferencesFromDefaultSet) const
{
std::unique_ptr<KeyPressMappingSet> defaultSet;
if (saveDifferencesFromDefaultSet)
{
defaultSet = std::make_unique<KeyPressMappingSet> (commandManager);
defaultSet->resetToDefaultMappings();
}
auto doc = std::make_unique<XmlElement> ("KEYMAPPINGS");
doc->setAttribute ("basedOnDefaults", saveDifferencesFromDefaultSet);
for (int i = 0; i < mappings.size(); ++i)
{
auto& cm = *mappings.getUnchecked (i);
for (int j = 0; j < cm.keypresses.size(); ++j)
{
if (defaultSet == nullptr
|| ! defaultSet->containsMapping (cm.commandID, cm.keypresses.getReference (j)))
{
auto map = doc->createNewChildElement ("MAPPING");
map->setAttribute ("commandId", String::toHexString ((int) cm.commandID));
map->setAttribute ("description", commandManager.getDescriptionOfCommand (cm.commandID));
map->setAttribute ("key", cm.keypresses.getReference (j).getTextDescription());
}
}
}
if (defaultSet != nullptr)
{
for (int i = 0; i < defaultSet->mappings.size(); ++i)
{
auto& cm = *defaultSet->mappings.getUnchecked (i);
for (int j = 0; j < cm.keypresses.size(); ++j)
{
if (! containsMapping (cm.commandID, cm.keypresses.getReference (j)))
{
auto map = doc->createNewChildElement ("UNMAPPING");
map->setAttribute ("commandId", String::toHexString ((int) cm.commandID));
map->setAttribute ("description", commandManager.getDescriptionOfCommand (cm.commandID));
map->setAttribute ("key", cm.keypresses.getReference (j).getTextDescription());
}
}
}
}
return doc;
}
//==============================================================================
bool KeyPressMappingSet::keyPressed (const KeyPress& key, Component* const originatingComponent)
{
bool commandWasDisabled = false;
for (int i = 0; i < mappings.size(); ++i)
{
CommandMapping& cm = *mappings.getUnchecked (i);
if (cm.keypresses.contains (key))
{
if (const ApplicationCommandInfo* const ci = commandManager.getCommandForID (cm.commandID))
{
if ((ci->flags & ApplicationCommandInfo::wantsKeyUpDownCallbacks) == 0)
{
ApplicationCommandInfo info (0);
if (commandManager.getTargetForCommand (cm.commandID, info) != nullptr)
{
if ((info.flags & ApplicationCommandInfo::isDisabled) == 0)
{
invokeCommand (cm.commandID, key, true, 0, originatingComponent);
return true;
}
commandWasDisabled = true;
}
}
}
}
}
if (originatingComponent != nullptr && commandWasDisabled)
originatingComponent->getLookAndFeel().playAlertSound();
return false;
}
bool KeyPressMappingSet::keyStateChanged (const bool /*isKeyDown*/, Component* originatingComponent)
{
bool used = false;
const uint32 now = Time::getMillisecondCounter();
for (int i = mappings.size(); --i >= 0;)
{
CommandMapping& cm = *mappings.getUnchecked (i);
if (cm.wantsKeyUpDownCallbacks)
{
for (int j = cm.keypresses.size(); --j >= 0;)
{
const KeyPress key (cm.keypresses.getReference (j));
const bool isDown = key.isCurrentlyDown();
int keyPressEntryIndex = 0;
bool wasDown = false;
for (int k = keysDown.size(); --k >= 0;)
{
if (key == keysDown.getUnchecked (k)->key)
{
keyPressEntryIndex = k;
wasDown = true;
used = true;
break;
}
}
if (isDown != wasDown)
{
int millisecs = 0;
if (isDown)
{
KeyPressTime* const k = new KeyPressTime();
k->key = key;
k->timeWhenPressed = now;
keysDown.add (k);
}
else
{
const uint32 pressTime = keysDown.getUnchecked (keyPressEntryIndex)->timeWhenPressed;
if (now > pressTime)
millisecs = (int) (now - pressTime);
keysDown.remove (keyPressEntryIndex);
}
invokeCommand (cm.commandID, key, isDown, millisecs, originatingComponent);
used = true;
}
}
}
}
return used;
}
void KeyPressMappingSet::globalFocusChanged (Component* focusedComponent)
{
if (focusedComponent != nullptr)
focusedComponent->keyStateChanged (false);
}
} // namespace juce
@@ -0,0 +1,244 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Manages and edits a list of keypresses, which it uses to invoke the appropriate
command in an ApplicationCommandManager.
Normally, you won't actually create a KeyPressMappingSet directly, because
each ApplicationCommandManager contains its own KeyPressMappingSet, so typically
you'd create yourself an ApplicationCommandManager, and call its
ApplicationCommandManager::getKeyMappings() method to get a pointer to its
KeyPressMappingSet.
For one of these to actually use keypresses, you'll need to add it as a KeyListener
to the top-level component for which you want to handle keystrokes. So for example:
@code
class MyMainWindow : public Component
{
ApplicationCommandManager* myCommandManager;
public:
MyMainWindow()
{
myCommandManager = new ApplicationCommandManager();
// first, make sure the command manager has registered all the commands that its
// targets can perform..
myCommandManager->registerAllCommandsForTarget (myCommandTarget1);
myCommandManager->registerAllCommandsForTarget (myCommandTarget2);
// this will use the command manager to initialise the KeyPressMappingSet with
// the default keypresses that were specified when the targets added their commands
// to the manager.
myCommandManager->getKeyMappings()->resetToDefaultMappings();
// having set up the default key-mappings, you might now want to load the last set
// of mappings that the user configured.
myCommandManager->getKeyMappings()->restoreFromXml (lastSavedKeyMappingsXML);
// Now tell our top-level window to send any keypresses that arrive to the
// KeyPressMappingSet, which will use them to invoke the appropriate commands.
addKeyListener (myCommandManager->getKeyMappings());
}
...
}
@endcode
KeyPressMappingSet derives from ChangeBroadcaster so that interested parties can
register to be told when a command or mapping is added, removed, etc.
There's also a UI component called KeyMappingEditorComponent that can be used
to easily edit the key mappings.
@see Component::addKeyListener(), KeyMappingEditorComponent, ApplicationCommandManager
@tags{GUI}
*/
class JUCE_API KeyPressMappingSet : public KeyListener,
public ChangeBroadcaster,
private FocusChangeListener
{
public:
//==============================================================================
/** Creates a KeyPressMappingSet for a given command manager.
Normally, you won't actually create a KeyPressMappingSet directly, because
each ApplicationCommandManager contains its own KeyPressMappingSet, so the
best thing to do is to create your ApplicationCommandManager, and use the
ApplicationCommandManager::getKeyMappings() method to access its mappings.
When a suitable keypress happens, the manager's invoke() method will be
used to invoke the appropriate command.
@see ApplicationCommandManager
*/
explicit KeyPressMappingSet (ApplicationCommandManager&);
/** Creates an copy of a KeyPressMappingSet. */
KeyPressMappingSet (const KeyPressMappingSet&);
/** Destructor. */
~KeyPressMappingSet() override;
//==============================================================================
ApplicationCommandManager& getCommandManager() const noexcept { return commandManager; }
//==============================================================================
/** Returns a list of keypresses that are assigned to a particular command.
@param commandID the command's ID
*/
Array<KeyPress> getKeyPressesAssignedToCommand (CommandID commandID) const;
/** Assigns a keypress to a command.
If the keypress is already assigned to a different command, it will first be
removed from that command, to avoid it triggering multiple functions.
@param commandID the ID of the command that you want to add a keypress to. If
this is 0, the keypress will be removed from anything that it
was previously assigned to, but not re-assigned
@param newKeyPress the new key-press
@param insertIndex if this is less than zero, the key will be appended to the
end of the list of keypresses; otherwise the new keypress will
be inserted into the existing list at this index
*/
void addKeyPress (CommandID commandID,
const KeyPress& newKeyPress,
int insertIndex = -1);
/** Reset all mappings to the defaults, as dictated by the ApplicationCommandManager.
@see resetToDefaultMapping
*/
void resetToDefaultMappings();
/** Resets all key-mappings to the defaults for a particular command.
@see resetToDefaultMappings
*/
void resetToDefaultMapping (CommandID commandID);
/** Removes all keypresses that are assigned to any commands. */
void clearAllKeyPresses();
/** Removes all keypresses that are assigned to a particular command. */
void clearAllKeyPresses (CommandID commandID);
/** Removes one of the keypresses that are assigned to a command.
See the getKeyPressesAssignedToCommand() for the list of keypresses to
which the keyPressIndex refers.
*/
void removeKeyPress (CommandID commandID, int keyPressIndex);
/** Removes a keypress from any command that it may be assigned to. */
void removeKeyPress (const KeyPress& keypress);
/** Returns true if the given command is linked to this key. */
bool containsMapping (CommandID commandID, const KeyPress& keyPress) const noexcept;
//==============================================================================
/** Looks for a command that corresponds to a keypress.
@returns the UID of the command or 0 if none was found
*/
CommandID findCommandForKeyPress (const KeyPress& keyPress) const noexcept;
//==============================================================================
/** Tries to recreate the mappings from a previously stored state.
The XML passed in must have been created by the createXml() method.
If the stored state makes any reference to commands that aren't
currently available, these will be ignored.
If the set of mappings being loaded was a set of differences (using createXml (true)),
then this will call resetToDefaultMappings() and then merge the saved mappings
on top. If the saved set was created with createXml (false), then this method
will first clear all existing mappings and load the saved ones as a complete set.
@returns true if it manages to load the XML correctly
@see createXml
*/
bool restoreFromXml (const XmlElement& xmlVersion);
/** Creates an XML representation of the current mappings.
This will produce a lump of XML that can be later reloaded using
restoreFromXml() to recreate the current mapping state.
@param saveDifferencesFromDefaultSet if this is false, then all keypresses
will be saved into the XML. If it's true, then the XML will
only store the differences between the current mappings and
the default mappings you'd get from calling resetToDefaultMappings().
The advantage of saving a set of differences from the default is that
if you change the default mappings (in a new version of your app, for
example), then these will be merged into a user's saved preferences.
@see restoreFromXml
*/
std::unique_ptr<XmlElement> createXml (bool saveDifferencesFromDefaultSet) const;
//==============================================================================
/** @internal */
bool keyPressed (const KeyPress&, Component*) override;
/** @internal */
bool keyStateChanged (bool isKeyDown, Component*) override;
/** @internal */
void globalFocusChanged (Component*) override;
private:
//==============================================================================
ApplicationCommandManager& commandManager;
struct CommandMapping
{
CommandID commandID;
Array<KeyPress> keypresses;
bool wantsKeyUpDownCallbacks;
};
OwnedArray<CommandMapping> mappings;
struct KeyPressTime
{
KeyPress key;
uint32 timeWhenPressed;
};
OwnedArray<KeyPressTime> keysDown;
void invokeCommand (CommandID, const KeyPress&, bool isKeyDown,
int millisecsSinceKeyPressed, Component* originator) const;
KeyPressMappingSet& operator= (const KeyPressMappingSet&);
JUCE_LEAK_DETECTOR (KeyPressMappingSet)
};
} // namespace juce
@@ -0,0 +1,72 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Base class used internally for structures that can store cached images of
component state.
Most people are unlikely to ever need to know about this class - it's really
only for power-users!
@see Component::setCachedComponentImage
@tags{GUI}
*/
class JUCE_API CachedComponentImage
{
public:
CachedComponentImage() = default;
virtual ~CachedComponentImage() = default;
//==============================================================================
/** Called as part of the parent component's paint method, this must draw
the given component into the target graphics context, using the cached
version where possible.
*/
virtual void paint (Graphics&) = 0;
/** Invalidates all cached image data.
@returns true if the peer should also be repainted, or false if this object
handles all repaint work internally.
*/
virtual bool invalidateAll() = 0;
/** Invalidates a section of the cached image data.
@returns true if the peer should also be repainted, or false if this object
handles all repaint work internally.
*/
virtual bool invalidate (const Rectangle<int>& area) = 0;
/** Called to indicate that the component is no longer active, so
any cached data should be released if possible.
*/
virtual void releaseResources() = 0;
};
} // namespace juce
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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
{
void ComponentListener::componentMovedOrResized (Component&, bool, bool) {}
void ComponentListener::componentBroughtToFront (Component&) {}
void ComponentListener::componentVisibilityChanged (Component&) {}
void ComponentListener::componentChildrenChanged (Component&) {}
void ComponentListener::componentParentHierarchyChanged (Component&) {}
void ComponentListener::componentNameChanged (Component&) {}
void ComponentListener::componentBeingDeleted (Component&) {}
void ComponentListener::componentEnablementChanged (Component&) {}
} // 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
{
//==============================================================================
/**
Gets informed about changes to a component's hierarchy or position.
To monitor a component for changes, register a subclass of ComponentListener
with the component using Component::addComponentListener().
Be sure to deregister listeners before you delete them!
@see Component::addComponentListener, Component::removeComponentListener
@tags{GUI}
*/
class JUCE_API ComponentListener
{
public:
/** Destructor. */
virtual ~ComponentListener() = default;
/** Called when the component's position or size changes.
@param component the component that was moved or resized
@param wasMoved true if the component's top-left corner has just moved
@param wasResized true if the component's width or height has just changed
@see Component::setBounds, Component::resized, Component::moved
*/
virtual void componentMovedOrResized (Component& component,
bool wasMoved,
bool wasResized);
/** Called when the component is brought to the top of the z-order.
@param component the component that was moved
@see Component::toFront, Component::broughtToFront
*/
virtual void componentBroughtToFront (Component& component);
/** Called when the component is made visible or invisible.
@param component the component that changed
@see Component::setVisible
*/
virtual void componentVisibilityChanged (Component& component);
/** Called when the component has children added or removed, or their z-order
changes.
@param component the component whose children have changed
@see Component::childrenChanged, Component::addChildComponent,
Component::removeChildComponent
*/
virtual void componentChildrenChanged (Component& component);
/** Called to indicate that the component's parents have changed.
When a component is added or removed from its parent, all of its children
will produce this notification (recursively - so all children of its
children will also be called as well).
@param component the component that this listener is registered with
@see Component::parentHierarchyChanged
*/
virtual void componentParentHierarchyChanged (Component& component);
/** Called when the component's name is changed.
@param component the component that had its name changed
@see Component::setName, Component::getName
*/
virtual void componentNameChanged (Component& component);
/** Called when the component is in the process of being deleted.
This callback is made from inside the destructor, so be very, very cautious
about what you do in here.
In particular, bear in mind that it's the Component base class's destructor that calls
this - so if the object that's being deleted is a subclass of Component, then the
subclass layers of the object will already have been destructed when it gets to this
point!
@param component the component that was deleted
*/
virtual void componentBeingDeleted (Component& component);
/* Called when the component's enablement is changed.
@param component the component that had its enablement changed
@see Component::setEnabled, Component::isEnabled, Component::enablementChanged
*/
virtual void componentEnablementChanged (Component& component);
};
} // namespace juce
@@ -0,0 +1,72 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Base class for traversing components.
If you need custom focus or keyboard focus traversal for a component you can
create a subclass of ComponentTraverser and return it from
Component::createFocusTraverser() or Component::createKeyboardFocusTraverser().
@see Component::createFocusTraverser, Component::createKeyboardFocusTraverser
@tags{GUI}
*/
class JUCE_API ComponentTraverser
{
public:
/** Destructor. */
virtual ~ComponentTraverser() = default;
/** Returns the component that should be used as the traversal entry point
within the given parent component.
This must return nullptr if there is no default component.
*/
virtual Component* getDefaultComponent (Component* parentComponent) = 0;
/** Returns the component that comes after the specified one when moving "forwards".
This must return nullptr if there is no next component.
*/
virtual Component* getNextComponent (Component* current) = 0;
/** Returns the component that comes after the specified one when moving "backwards".
This must return nullptr if there is no previous component.
*/
virtual Component* getPreviousComponent (Component* current) = 0;
/** Returns all of the traversable components within the given parent component in
traversal order.
*/
virtual std::vector<Component*> getAllComponents (Component* parentComponent) = 0;
};
} // namespace juce
@@ -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
{
//==============================================================================
Component* FocusTraverser::getNextComponent (Component* current)
{
jassert (current != nullptr);
return detail::FocusHelpers::navigateFocus (current,
current->findFocusContainer(),
detail::FocusHelpers::NavigationDirection::forwards,
&Component::isFocusContainer);
}
Component* FocusTraverser::getPreviousComponent (Component* current)
{
jassert (current != nullptr);
return detail::FocusHelpers::navigateFocus (current,
current->findFocusContainer(),
detail::FocusHelpers::NavigationDirection::backwards,
&Component::isFocusContainer);
}
Component* FocusTraverser::getDefaultComponent (Component* parentComponent)
{
if (parentComponent != nullptr)
{
std::vector<Component*> components;
detail::FocusHelpers::findAllComponents (parentComponent,
components,
&Component::isFocusContainer);
if (! components.empty())
return components.front();
}
return nullptr;
}
std::vector<Component*> FocusTraverser::getAllComponents (Component* parentComponent)
{
std::vector<Component*> components;
detail::FocusHelpers::findAllComponents (parentComponent,
components,
&Component::isFocusContainer);
return components;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct FocusTraverserTests final : public UnitTest
{
FocusTraverserTests()
: UnitTest ("FocusTraverser", UnitTestCategories::gui)
{}
void runTest() override
{
ScopedJuceInitialiser_GUI libraryInitialiser;
const MessageManagerLock mml;
beginTest ("Basic traversal");
{
TestComponent parent;
expect (traverser.getDefaultComponent (&parent) == &parent.children.front());
for (auto iter = parent.children.begin(); iter != parent.children.end(); ++iter)
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (parent.children.cend()) ? nullptr
: &(*std::next (iter))));
for (auto iter = parent.children.rbegin(); iter != parent.children.rend(); ++iter)
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (parent.children.rend()) ? nullptr
: &(*std::next (iter))));
auto allComponents = traverser.getAllComponents (&parent);
expect (std::equal (allComponents.cbegin(), allComponents.cend(), parent.children.cbegin(),
[] (const Component* c1, const Component& c2) { return c1 == &c2; }));
}
beginTest ("Disabled components are ignored");
{
checkIgnored ([] (Component& c) { c.setEnabled (false); });
}
beginTest ("Invisible components are ignored");
{
checkIgnored ([] (Component& c) { c.setVisible (false); });
}
beginTest ("Explicit focus order comes before unspecified");
{
TestComponent parent;
auto& explicitFocusComponent = parent.children[2];
explicitFocusComponent.setExplicitFocusOrder (1);
expect (traverser.getDefaultComponent (&parent) == &explicitFocusComponent);
expect (traverser.getAllComponents (&parent).front() == &explicitFocusComponent);
}
beginTest ("Explicit focus order comparison");
{
checkComponentProperties ([this] (Component& child) { child.setExplicitFocusOrder (getRandom().nextInt ({ 1, 100 })); },
[] (const Component& c1, const Component& c2) { return c1.getExplicitFocusOrder()
<= c2.getExplicitFocusOrder(); });
}
beginTest ("Left to right");
{
checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (getRandom().nextInt ({ 0, 100 }), 0); },
[] (const Component& c1, const Component& c2) { return c1.getX() <= c2.getX(); });
}
beginTest ("Top to bottom");
{
checkComponentProperties ([this] (Component& child) { child.setTopLeftPosition (0, getRandom().nextInt ({ 0, 100 })); },
[] (const Component& c1, const Component& c2) { return c1.getY() <= c2.getY(); });
}
beginTest ("Focus containers have their own focus");
{
Component root;
TestComponent container;
container.setFocusContainerType (Component::FocusContainerType::focusContainer);
root.addAndMakeVisible (container);
expect (traverser.getDefaultComponent (&root) == &container);
expect (traverser.getNextComponent (&container) == nullptr);
expect (traverser.getPreviousComponent (&container) == nullptr);
expect (traverser.getDefaultComponent (&container) == &container.children.front());
for (auto iter = container.children.begin(); iter != container.children.end(); ++iter)
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr
: &(*std::next (iter))));
for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter)
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? nullptr
: &(*std::next (iter))));
expect (traverser.getAllComponents (&root).size() == 1);
auto allContainerComponents = traverser.getAllComponents (&container);
expect (std::equal (allContainerComponents.cbegin(), allContainerComponents.cend(), container.children.cbegin(),
[] (const Component* c1, const Component& c2) { return c1 == &c2; }));
}
beginTest ("Non-focus containers pass-through focus");
{
Component root;
TestComponent container;
container.setFocusContainerType (Component::FocusContainerType::none);
root.addAndMakeVisible (container);
expect (traverser.getDefaultComponent (&root) == &container);
expect (traverser.getNextComponent (&container) == &container.children.front());
expect (traverser.getPreviousComponent (&container) == nullptr);
expect (traverser.getDefaultComponent (&container) == &container.children.front());
for (auto iter = container.children.begin(); iter != container.children.end(); ++iter)
expect (traverser.getNextComponent (&(*iter)) == (iter == std::prev (container.children.cend()) ? nullptr
: &(*std::next (iter))));
for (auto iter = container.children.rbegin(); iter != container.children.rend(); ++iter)
expect (traverser.getPreviousComponent (&(*iter)) == (iter == std::prev (container.children.rend()) ? &container
: &(*std::next (iter))));
expect (traverser.getAllComponents (&root).size() == container.children.size() + 1);
}
}
private:
struct TestComponent final : public Component
{
TestComponent()
{
for (auto& child : children)
addAndMakeVisible (child);
}
std::array<Component, 10> children;
};
void checkComponentProperties (std::function<void (Component&)>&& childFn,
std::function<bool (const Component&, const Component&)>&& testProperty)
{
TestComponent parent;
for (auto& child : parent.children)
childFn (child);
auto* comp = traverser.getDefaultComponent (&parent);
for (const auto& child : parent.children)
if (&child != comp)
expect (testProperty (*comp, child));
for (;;)
{
auto* next = traverser.getNextComponent (comp);
if (next == nullptr)
break;
expect (testProperty (*comp, *next));
comp = next;
}
}
void checkIgnored (const std::function<void(Component&)>& makeIgnored)
{
TestComponent parent;
auto iter = parent.children.begin();
makeIgnored (*iter);
expect (traverser.getDefaultComponent (&parent) == std::addressof (*std::next (iter)));
iter += 5;
makeIgnored (*iter);
expect (traverser.getNextComponent (std::addressof (*std::prev (iter))) == std::addressof (*std::next (iter)));
expect (traverser.getPreviousComponent (std::addressof (*std::next (iter))) == std::addressof (*std::prev (iter)));
auto allComponents = traverser.getAllComponents (&parent);
expect (std::find (allComponents.cbegin(), allComponents.cend(), &parent.children.front()) == allComponents.cend());
expect (std::find (allComponents.cbegin(), allComponents.cend(), std::addressof (*iter)) == allComponents.cend());
}
FocusTraverser traverser;
};
static FocusTraverserTests focusTraverserTests;
#endif
} // namespace juce
@@ -0,0 +1,93 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Controls the order in which focus moves between components.
The algorithm used by this class to work out the order of traversal is as
follows:
- Only visible and enabled components are considered focusable.
- If two components both have an explicit focus order specified then the
one with the lowest number comes first (see the
Component::setExplicitFocusOrder() method).
- Any component with an explicit focus order greater than 0 comes before ones
that don't have an order specified.
- Components with their 'always on top' flag set come before those without.
- Any unspecified components are traversed in a left-to-right, then
top-to-bottom order.
If you need focus traversal in a more customised way you can create a
ComponentTraverser subclass that uses your own algorithm and return it
from Component::createFocusTraverser().
@see ComponentTraverser, Component::createFocusTraverser
@tags{GUI}
*/
class JUCE_API FocusTraverser : public ComponentTraverser
{
public:
/** Destructor. */
~FocusTraverser() override = default;
/** Returns the component that should receive focus by default within the given
parent component.
The default implementation will just return the foremost visible and enabled
child component, and will return nullptr if there is no suitable component.
*/
Component* getDefaultComponent (Component* parentComponent) override;
/** Returns the component that should be given focus after the specified one when
moving "forwards".
The default implementation will return the next visible and enabled component
which is to the right of or below this one, and will return nullptr if there
is no suitable component.
*/
Component* getNextComponent (Component* current) override;
/** Returns the component that should be given focus after the specified one when
moving "backwards".
The default implementation will return the previous visible and enabled component
which is to the left of or above this one, and will return nullptr if there
is no suitable component.
*/
Component* getPreviousComponent (Component* current) override;
/** Returns all of the components that can receive focus within the given parent
component in traversal order.
The default implementation will return all visible and enabled child components.
*/
std::vector<Component*> getAllComponents (Component* parentComponent) override;
};
} // namespace juce
@@ -0,0 +1,297 @@
/*
==============================================================================
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 ModalComponentManager::ModalItem final : public ComponentMovementWatcher
{
ModalItem (Component* comp, bool shouldAutoDelete)
: ComponentMovementWatcher (comp),
component (comp), autoDelete (shouldAutoDelete)
{
jassert (comp != nullptr);
}
~ModalItem() override
{
if (autoDelete)
std::unique_ptr<Component> componentDeleter (component);
}
void componentMovedOrResized (bool, bool) override {}
using ComponentMovementWatcher::componentMovedOrResized;
void componentPeerChanged() override
{
componentVisibilityChanged();
}
void componentVisibilityChanged() override
{
if (! component->isShowing())
cancel();
}
using ComponentMovementWatcher::componentVisibilityChanged;
void componentBeingDeleted (Component& comp) override
{
ComponentMovementWatcher::componentBeingDeleted (comp);
if (component == &comp || comp.isParentOf (component))
{
autoDelete = false;
cancel();
}
}
void cancel()
{
if (isActive)
{
isActive = false;
if (auto* mcm = ModalComponentManager::getInstanceWithoutCreating())
mcm->triggerAsyncUpdate();
}
}
Component* component;
OwnedArray<Callback> callbacks;
int returnValue = 0;
bool isActive = true, autoDelete;
JUCE_DECLARE_NON_COPYABLE (ModalItem)
};
//==============================================================================
ModalComponentManager::ModalComponentManager()
{
}
ModalComponentManager::~ModalComponentManager()
{
stack.clear();
clearSingletonInstance();
}
JUCE_IMPLEMENT_SINGLETON (ModalComponentManager)
//==============================================================================
void ModalComponentManager::startModal (Component* component, bool autoDelete)
{
if (component != nullptr)
{
stack.add (new ModalItem (component, autoDelete));
detail::ComponentHelpers::ModalComponentManagerChangeNotifier::getInstance().modalComponentManagerChanged();
}
}
void ModalComponentManager::attachCallback (Component* component, Callback* callback)
{
if (callback != nullptr)
{
std::unique_ptr<Callback> callbackDeleter (callback);
for (int i = stack.size(); --i >= 0;)
{
auto* item = stack.getUnchecked (i);
if (item->component == component)
{
item->callbacks.add (callback);
callbackDeleter.release();
break;
}
}
}
}
void ModalComponentManager::endModal (Component* component)
{
for (int i = stack.size(); --i >= 0;)
{
auto* item = stack.getUnchecked (i);
if (item->component == component)
item->cancel();
}
}
void ModalComponentManager::endModal (Component* component, int returnValue)
{
for (int i = stack.size(); --i >= 0;)
{
auto* item = stack.getUnchecked (i);
if (item->component == component)
{
item->returnValue = returnValue;
item->cancel();
}
}
}
int ModalComponentManager::getNumModalComponents() const
{
int n = 0;
for (auto* item : stack)
if (item->isActive)
++n;
return n;
}
Component* ModalComponentManager::getModalComponent (int index) const
{
int n = 0;
for (int i = stack.size(); --i >= 0;)
{
auto* item = stack.getUnchecked (i);
if (item->isActive)
if (n++ == index)
return item->component;
}
return nullptr;
}
bool ModalComponentManager::isModal (const Component* comp) const
{
for (auto* item : stack)
if (item->isActive && item->component == comp)
return true;
return false;
}
bool ModalComponentManager::isFrontModalComponent (const Component* comp) const
{
return comp == getModalComponent (0);
}
void ModalComponentManager::handleAsyncUpdate()
{
for (int i = stack.size(); --i >= 0;)
{
auto* item = stack.getUnchecked (i);
if (! item->isActive)
{
std::unique_ptr<ModalItem> deleter (stack.removeAndReturn (i));
Component::SafePointer<Component> compToDelete (item->autoDelete ? item->component : nullptr);
for (int j = item->callbacks.size(); --j >= 0;)
item->callbacks.getUnchecked (j)->modalStateFinished (item->returnValue);
compToDelete.deleteAndZero();
detail::ComponentHelpers::ModalComponentManagerChangeNotifier::getInstance().modalComponentManagerChanged();
}
}
}
void ModalComponentManager::bringModalComponentsToFront (bool topOneShouldGrabFocus)
{
ComponentPeer* lastOne = nullptr;
for (int i = 0; i < getNumModalComponents(); ++i)
{
auto* c = getModalComponent (i);
if (c == nullptr)
break;
if (auto* peer = c->getPeer())
{
if (peer != lastOne)
{
if (lastOne == nullptr)
{
peer->toFront (topOneShouldGrabFocus);
if (topOneShouldGrabFocus)
peer->grabFocus();
}
else
{
peer->toBehind (lastOne);
}
lastOne = peer;
}
}
}
}
bool ModalComponentManager::cancelAllModalComponents()
{
auto numModal = getNumModalComponents();
for (int i = numModal; --i >= 0;)
if (auto* c = getModalComponent (i))
c->exitModalState (0);
return numModal > 0;
}
//==============================================================================
#if JUCE_MODAL_LOOPS_PERMITTED
int ModalComponentManager::runEventLoopForCurrentComponent()
{
// This can only be run from the message thread!
JUCE_ASSERT_MESSAGE_THREAD
int returnValue = 0;
if (auto* currentlyModal = getModalComponent (0))
{
detail::FocusRestorer focusRestorer;
bool finished = false;
attachCallback (currentlyModal, ModalCallbackFunction::create ([&] (int r) { returnValue = r; finished = true; }));
JUCE_TRY
{
while (! finished)
{
if (! MessageManager::getInstance()->runDispatchLoopUntil (20))
break;
}
}
JUCE_CATCH_EXCEPTION
}
return returnValue;
}
#endif
} // namespace juce
@@ -0,0 +1,324 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Manages the system's stack of modal components.
Normally you'll just use the Component methods to invoke modal states in components,
and won't have to deal with this class directly, but this is the singleton object that's
used internally to manage the stack.
@see Component::enterModalState, Component::exitModalState, Component::isCurrentlyModal,
Component::getCurrentlyModalComponent, Component::isCurrentlyBlockedByAnotherModalComponent
@tags{GUI}
*/
class JUCE_API ModalComponentManager : private AsyncUpdater,
private DeletedAtShutdown
{
public:
//==============================================================================
/** Receives callbacks when a modal component is dismissed.
You can register a callback using Component::enterModalState() or
ModalComponentManager::attachCallback().
For some quick ways of creating callback objects, see the ModalCallbackFunction class.
@see ModalCallbackFunction
*/
class JUCE_API Callback
{
public:
/** */
Callback() = default;
/** Destructor. */
virtual ~Callback() = default;
/** Called to indicate that a modal component has been dismissed.
You can register a callback using Component::enterModalState() or
ModalComponentManager::attachCallback().
The returnValue parameter is the value that was passed to Component::exitModalState()
when the component was dismissed.
The callback object will be deleted shortly after this method is called.
*/
virtual void modalStateFinished (int returnValue) = 0;
};
//==============================================================================
#ifndef DOXYGEN
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (ModalComponentManager)
#endif
//==============================================================================
/** Returns the number of components currently being shown modally.
@see getModalComponent
*/
int getNumModalComponents() const;
/** Returns one of the components being shown modally.
An index of 0 is the most recently-shown, topmost component.
*/
Component* getModalComponent (int index) const;
/** Returns true if the specified component is in a modal state. */
bool isModal (const Component* component) const;
/** Returns true if the specified component is currently the topmost modal component. */
bool isFrontModalComponent (const Component* component) const;
/** Adds a new callback that will be called when the specified modal component is dismissed.
If the component is modal, then when it is dismissed, either by being hidden, or by calling
Component::exitModalState(), then the Callback::modalStateFinished() method will be
called.
Each component can have any number of callbacks associated with it, and this one is added
to that list.
The object that is passed in will be deleted by the manager when it's no longer needed. If
the given component is not currently modal, the callback object is deleted immediately and
no action is taken.
*/
void attachCallback (Component* component, Callback* callback);
/** Brings any modal components to the front. */
void bringModalComponentsToFront (bool topOneShouldGrabFocus = true);
/** Calls exitModalState (0) on any components that are currently modal.
@returns true if any components were modal; false if nothing needed cancelling
*/
bool cancelAllModalComponents();
#if JUCE_MODAL_LOOPS_PERMITTED
/** Runs the event loop until the currently topmost modal component is dismissed, and
returns the exit code for that component.
*/
int runEventLoopForCurrentComponent();
#endif
protected:
/** Creates a ModalComponentManager.
You shouldn't ever call the constructor - it's a singleton, so use ModalComponentManager::getInstance()
*/
ModalComponentManager();
/** Destructor. */
~ModalComponentManager() override;
/** @internal */
void handleAsyncUpdate() override;
private:
//==============================================================================
friend class Component;
struct ModalItem;
OwnedArray<ModalItem> stack;
void startModal (Component*, bool autoDelete);
void endModal (Component*, int returnValue);
void endModal (Component*);
JUCE_DECLARE_NON_COPYABLE (ModalComponentManager)
};
//==============================================================================
/**
This class provides some handy utility methods for creating ModalComponentManager::Callback
objects that will invoke a static function with some parameters when a modal component is dismissed.
@tags{GUI}
*/
class JUCE_API ModalCallbackFunction
{
public:
/** This is a utility function to create a ModalComponentManager::Callback that will
call a callable object.
The function that you supply must take an integer parameter, which is the result code that
was returned when the modal component was dismissed.
@see ModalComponentManager::Callback
*/
template <typename CallbackFn>
static ModalComponentManager::Callback* create (CallbackFn&& fn)
{
struct Callable : public ModalComponentManager::Callback
{
explicit Callable (CallbackFn&& f) : fn (std::forward<CallbackFn> (f)) {}
void modalStateFinished (int result) override { NullCheckedInvocation::invoke (std::move (fn), result); }
std::remove_reference_t<CallbackFn> fn;
};
return new Callable (std::forward<CallbackFn> (fn));
}
//==============================================================================
/** This is a utility function to create a ModalComponentManager::Callback that will
call a static function with a parameter.
The function that you supply must take two parameters - the first being an int, which is
the result code that was used when the modal component was dismissed, and the second
can be a custom type. Note that this custom value will be copied and stored, so it must
be a primitive type or a class that provides copy-by-value semantics.
E.g. @code
static void myCallbackFunction (int modalResult, double customValue)
{
if (modalResult == 1)
doSomethingWith (customValue);
}
Component* someKindOfComp;
...
someKindOfComp->enterModalState (true, ModalCallbackFunction::create (myCallbackFunction, 3.0));
@endcode
@see ModalComponentManager::Callback
*/
template <typename ParamType>
static ModalComponentManager::Callback* create (void (*functionToCall) (int, ParamType),
ParamType parameterValue)
{
return create ([functionToCall, parameterValue] (int r)
{
functionToCall (r, parameterValue);
});
}
//==============================================================================
/** This is a utility function to create a ModalComponentManager::Callback that will
call a static function with two custom parameters.
The function that you supply must take three parameters - the first being an int, which is
the result code that was used when the modal component was dismissed, and the next two are
your custom types. Note that these custom values will be copied and stored, so they must
be primitive types or classes that provide copy-by-value semantics.
E.g. @code
static void myCallbackFunction (int modalResult, double customValue1, String customValue2)
{
if (modalResult == 1)
doSomethingWith (customValue1, customValue2);
}
Component* someKindOfComp;
...
someKindOfComp->enterModalState (true, ModalCallbackFunction::create (myCallbackFunction, 3.0, String ("xyz")));
@endcode
@see ModalComponentManager::Callback
*/
template <typename ParamType1, typename ParamType2>
static ModalComponentManager::Callback* withParam (void (*functionToCall) (int, ParamType1, ParamType2),
ParamType1 parameterValue1,
ParamType2 parameterValue2)
{
return create ([functionToCall, parameterValue1, parameterValue2] (int r)
{
functionToCall (r, parameterValue1, parameterValue2);
});
}
//==============================================================================
/** This is a utility function to create a ModalComponentManager::Callback that will
call a static function with a component.
The function that you supply must take two parameters - the first being an int, which is
the result code that was used when the modal component was dismissed, and the second
can be a Component class. The component will be stored as a WeakReference, so that if it gets
deleted before this callback is invoked, the pointer that is passed to the function will be null.
E.g. @code
static void myCallbackFunction (int modalResult, Slider* mySlider)
{
if (modalResult == 1 && mySlider != nullptr) // (must check that mySlider isn't null in case it was deleted..)
mySlider->setValue (0.0);
}
Component* someKindOfComp;
Slider* mySlider;
...
someKindOfComp->enterModalState (true, ModalCallbackFunction::forComponent (myCallbackFunction, mySlider));
@endcode
@see ModalComponentManager::Callback
*/
template <class ComponentType>
static ModalComponentManager::Callback* forComponent (void (*functionToCall) (int, ComponentType*),
ComponentType* component)
{
return create ([functionToCall, comp = WeakReference<Component> { component }] (int r)
{
functionToCall (r, static_cast<ComponentType*> (comp.get()));
});
}
//==============================================================================
/** Creates a ModalComponentManager::Callback that will call a static function with a component.
The function that you supply must take three parameters - the first being an int, which is
the result code that was used when the modal component was dismissed, the second being a Component
class, and the third being a custom type (which must be a primitive type or have copy-by-value semantics).
The component will be stored as a WeakReference, so that if it gets deleted before this callback is
invoked, the pointer that is passed into the function will be null.
E.g. @code
static void myCallbackFunction (int modalResult, Slider* mySlider, String customParam)
{
if (modalResult == 1 && mySlider != nullptr) // (must check that mySlider isn't null in case it was deleted..)
mySlider->setName (customParam);
}
Component* someKindOfComp;
Slider* mySlider;
...
someKindOfComp->enterModalState (true, ModalCallbackFunction::forComponent (myCallbackFunction, mySlider, String ("hello")));
@endcode
@see ModalComponentManager::Callback
*/
template <class ComponentType, typename ParamType>
static ModalComponentManager::Callback* forComponent (void (*functionToCall) (int, ComponentType*, ParamType),
ComponentType* component,
ParamType param)
{
return create ([functionToCall, param, comp = WeakReference<Component> { component }] (int r)
{
functionToCall (r, static_cast<ComponentType*> (comp.get()), param);
});
}
private:
ModalCallbackFunction() = delete;
~ModalCallbackFunction() = delete;
};
} // 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
{
Desktop::Desktop()
: mouseSources (new detail::MouseInputSourceList()),
masterScaleFactor ((float) getDefaultMasterScale()),
nativeDarkModeChangeDetectorImpl (createNativeDarkModeChangeDetectorImpl())
{
displays.reset (new Displays (*this));
}
Desktop::~Desktop()
{
setScreenSaverEnabled (true);
animator.cancelAllAnimations (false);
jassert (instance == this);
instance = nullptr;
// doh! If you don't delete all your windows before exiting, you're going to
// be leaking memory!
jassert (desktopComponents.size() == 0);
}
Desktop& JUCE_CALLTYPE Desktop::getInstance()
{
if (instance == nullptr)
instance = new Desktop();
return *instance;
}
Desktop* Desktop::instance = nullptr;
//==============================================================================
int Desktop::getNumComponents() const noexcept
{
return desktopComponents.size();
}
Component* Desktop::getComponent (int index) const noexcept
{
return desktopComponents [index];
}
Component* Desktop::findComponentAt (Point<int> screenPosition) const
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
for (int i = desktopComponents.size(); --i >= 0;)
{
auto* c = desktopComponents.getUnchecked (i);
if (c->isVisible())
{
auto relative = c->getLocalPoint (nullptr, screenPosition);
if (c->contains (relative))
return c->getComponentAt (relative);
}
}
return nullptr;
}
//==============================================================================
LookAndFeel& Desktop::getDefaultLookAndFeel() noexcept
{
if (auto lf = currentLookAndFeel.get())
return *lf;
if (defaultLookAndFeel == nullptr)
defaultLookAndFeel.reset (new LookAndFeel_V4());
auto lf = defaultLookAndFeel.get();
jassert (lf != nullptr);
currentLookAndFeel = lf;
return *lf;
}
void Desktop::setDefaultLookAndFeel (LookAndFeel* newDefaultLookAndFeel)
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
currentLookAndFeel = newDefaultLookAndFeel;
for (int i = getNumComponents(); --i >= 0;)
if (auto* c = getComponent (i))
c->sendLookAndFeelChange();
}
//==============================================================================
void Desktop::addDesktopComponent (Component* c)
{
jassert (c != nullptr);
jassert (! desktopComponents.contains (c));
desktopComponents.addIfNotAlreadyThere (c);
}
void Desktop::removeDesktopComponent (Component* c)
{
desktopComponents.removeFirstMatchingValue (c);
}
void Desktop::componentBroughtToFront (Component* c)
{
auto index = desktopComponents.indexOf (c);
jassert (index >= 0);
if (index >= 0)
{
int newIndex = -1;
if (! c->isAlwaysOnTop())
{
newIndex = desktopComponents.size();
while (newIndex > 0 && desktopComponents.getUnchecked (newIndex - 1)->isAlwaysOnTop())
--newIndex;
--newIndex;
}
desktopComponents.move (index, newIndex);
}
}
//==============================================================================
Point<int> Desktop::getMousePosition()
{
return getMousePositionFloat().roundToInt();
}
Point<float> Desktop::getMousePositionFloat()
{
return getInstance().getMainMouseSource().getScreenPosition();
}
void Desktop::setMousePosition (Point<int> newPosition)
{
getInstance().getMainMouseSource().setScreenPosition (newPosition.toFloat());
}
Point<int> Desktop::getLastMouseDownPosition()
{
return getInstance().getMainMouseSource().getLastMouseDownPosition().roundToInt();
}
int Desktop::getMouseButtonClickCounter() const noexcept { return mouseClickCounter; }
int Desktop::getMouseWheelMoveCounter() const noexcept { return mouseWheelCounter; }
void Desktop::incrementMouseClickCounter() noexcept { ++mouseClickCounter; }
void Desktop::incrementMouseWheelCounter() noexcept { ++mouseWheelCounter; }
const Array<MouseInputSource>& Desktop::getMouseSources() const noexcept { return mouseSources->sourceArray; }
int Desktop::getNumMouseSources() const noexcept { return mouseSources->sources.size(); }
int Desktop::getNumDraggingMouseSources() const noexcept { return mouseSources->getNumDraggingMouseSources(); }
MouseInputSource* Desktop::getMouseSource (int index) const noexcept { return mouseSources->getMouseSource (index); }
MouseInputSource* Desktop::getDraggingMouseSource (int index) const noexcept { return mouseSources->getDraggingMouseSource (index); }
MouseInputSource Desktop::getMainMouseSource() const noexcept { return MouseInputSource (mouseSources->sources.getUnchecked (0)); }
void Desktop::beginDragAutoRepeat (int interval) { mouseSources->beginDragAutoRepeat (interval); }
//==============================================================================
void Desktop::addFocusChangeListener (FocusChangeListener* l) { focusListeners.add (l); }
void Desktop::removeFocusChangeListener (FocusChangeListener* l) { focusListeners.remove (l); }
void Desktop::triggerFocusCallback() { triggerAsyncUpdate(); }
void Desktop::updateFocusOutline()
{
if (auto* currentFocus = Component::getCurrentlyFocusedComponent())
{
if (currentFocus->hasFocusOutline())
{
focusOutline = currentFocus->getLookAndFeel().createFocusOutlineForComponent (*currentFocus);
if (focusOutline != nullptr)
focusOutline->setOwner (currentFocus);
return;
}
}
focusOutline = nullptr;
}
void Desktop::handleAsyncUpdate()
{
// The component may be deleted during this operation, but we'll use a SafePointer rather than a
// BailOutChecker so that any remaining listeners will still get a callback (with a null pointer).
focusListeners.call ([currentFocus = WeakReference<Component> { Component::getCurrentlyFocusedComponent() }] (FocusChangeListener& l)
{
l.globalFocusChanged (currentFocus.get());
});
updateFocusOutline();
}
//==============================================================================
void Desktop::addDarkModeSettingListener (DarkModeSettingListener* l) { darkModeSettingListeners.add (l); }
void Desktop::removeDarkModeSettingListener (DarkModeSettingListener* l) { darkModeSettingListeners.remove (l); }
void Desktop::darkModeChanged() { darkModeSettingListeners.call ([] (auto& l) { l.darkModeSettingChanged(); }); }
//==============================================================================
void Desktop::resetTimer()
{
if (mouseListeners.size() == 0)
stopTimer();
else
startTimer (100);
lastFakeMouseMove = getMousePositionFloat();
}
ListenerList<MouseListener>& Desktop::getMouseListeners()
{
resetTimer();
return mouseListeners;
}
void Desktop::addGlobalMouseListener (MouseListener* listener)
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
mouseListeners.add (listener);
resetTimer();
}
void Desktop::removeGlobalMouseListener (MouseListener* listener)
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
mouseListeners.remove (listener);
resetTimer();
}
void Desktop::timerCallback()
{
if (lastFakeMouseMove != getMousePositionFloat())
sendMouseMove();
}
void Desktop::sendMouseMove()
{
if (! mouseListeners.isEmpty())
{
startTimer (20);
lastFakeMouseMove = getMousePositionFloat();
if (auto* target = findComponentAt (lastFakeMouseMove.roundToInt()))
{
Component::BailOutChecker checker (target);
auto pos = target->getLocalPoint (nullptr, lastFakeMouseMove);
auto now = Time::getCurrentTime();
const MouseEvent me (getMainMouseSource(), pos, ModifierKeys::currentModifiers, MouseInputSource::defaultPressure,
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
target, target, now, pos, now, 0, false);
if (me.mods.isAnyMouseButtonDown())
mouseListeners.callChecked (checker, [&] (MouseListener& l) { l.mouseDrag (me); });
else
mouseListeners.callChecked (checker, [&] (MouseListener& l) { l.mouseMove (me); });
}
}
}
//==============================================================================
void Desktop::setKioskModeComponent (Component* componentToUse, bool allowMenusAndBars)
{
if (kioskModeReentrant)
return;
const ScopedValueSetter<bool> setter (kioskModeReentrant, true, false);
if (kioskModeComponent != componentToUse)
{
// agh! Don't delete or remove a component from the desktop while it's still the kiosk component!
jassert (kioskModeComponent == nullptr || ComponentPeer::getPeerFor (kioskModeComponent) != nullptr);
if (auto* oldKioskComp = kioskModeComponent)
{
kioskModeComponent = nullptr; // (to make sure that isKioskMode() returns false when resizing the old one)
setKioskComponent (oldKioskComp, false, allowMenusAndBars);
oldKioskComp->setBounds (kioskComponentOriginalBounds);
}
kioskModeComponent = componentToUse;
if (kioskModeComponent != nullptr)
{
// Only components that are already on the desktop can be put into kiosk mode!
jassert (ComponentPeer::getPeerFor (kioskModeComponent) != nullptr);
kioskComponentOriginalBounds = kioskModeComponent->getBounds();
setKioskComponent (kioskModeComponent, true, allowMenusAndBars);
}
}
}
//==============================================================================
void Desktop::setOrientationsEnabled (int newOrientations)
{
if (allowedOrientations != newOrientations)
{
// Dodgy set of flags being passed here! Make sure you specify at least one permitted orientation.
jassert (newOrientations != 0 && (newOrientations & ~allOrientations) == 0);
allowedOrientations = newOrientations;
allowedOrientationsChanged();
}
}
int Desktop::getOrientationsEnabled() const noexcept
{
return allowedOrientations;
}
bool Desktop::isOrientationEnabled (DisplayOrientation orientation) const noexcept
{
// Make sure you only pass one valid flag in here...
jassert (orientation == upright || orientation == upsideDown
|| orientation == rotatedClockwise || orientation == rotatedAntiClockwise);
return (allowedOrientations & orientation) != 0;
}
void Desktop::setGlobalScaleFactor (float newScaleFactor) noexcept
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
if (! approximatelyEqual (masterScaleFactor, newScaleFactor))
{
masterScaleFactor = newScaleFactor;
displays->refresh();
}
}
bool Desktop::isHeadless() const noexcept
{
return displays->displays.isEmpty();
}
} // namespace juce
@@ -0,0 +1,484 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Classes can implement this interface and register themselves with the Desktop class
to receive callbacks when the currently focused component changes.
@see Desktop::addFocusChangeListener, Desktop::removeFocusChangeListener
@tags{GUI}
*/
class JUCE_API FocusChangeListener
{
public:
/** Destructor. */
virtual ~FocusChangeListener() = default;
/** Callback to indicate that the currently focused component has changed. */
virtual void globalFocusChanged (Component* focusedComponent) = 0;
};
//==============================================================================
/**
Classes can implement this interface and register themselves with the Desktop class
to receive callbacks when the operating system dark mode setting changes. The
Desktop::isDarkModeActive() method can then be used to query the current setting.
@see Desktop::addDarkModeSettingListener, Desktop::removeDarkModeSettingListener,
Desktop::isDarkModeActive
@tags{GUI}
*/
class JUCE_API DarkModeSettingListener
{
public:
/** Destructor. */
virtual ~DarkModeSettingListener() = default;
/** Callback to indicate that the dark mode setting has changed. */
virtual void darkModeSettingChanged() = 0;
};
//==============================================================================
/**
Describes and controls aspects of the computer's desktop.
@tags{GUI}
*/
class JUCE_API Desktop : private DeletedAtShutdown,
private Timer,
private AsyncUpdater
{
public:
//==============================================================================
/** There's only one desktop object, and this method will return it. */
static Desktop& JUCE_CALLTYPE getInstance();
//==============================================================================
/** Returns the mouse position.
The coordinates are relative to the top-left of the main monitor.
Note that this is just a shortcut for calling getMainMouseSource().getScreenPosition(), and
you should only resort to grabbing the global mouse position if there's really no
way to get the coordinates via a mouse event callback instead.
*/
static Point<int> getMousePosition();
/** Makes the mouse pointer jump to a given location.
The coordinates are relative to the top-left of the main monitor.
Note that this is a pretty old method, kept around mainly for backwards-compatibility,
and you should use the MouseInputSource class directly in new code.
*/
static void setMousePosition (Point<int> newPosition);
/** Returns the last position at which a mouse button was pressed.
Note that this is just a shortcut for calling getMainMouseSource().getLastMouseDownPosition(),
and in a multi-touch environment, it doesn't make much sense. ALWAYS prefer to
get this information via other means, such as MouseEvent::getMouseDownScreenPosition()
if possible, and only ever call this as a last resort.
*/
static Point<int> getLastMouseDownPosition();
/** Returns the number of times the mouse button has been clicked since the app started.
Each mouse-down event increments this number by 1.
@see getMouseWheelMoveCounter
*/
int getMouseButtonClickCounter() const noexcept;
/** Returns the number of times the mouse wheel has been moved since the app started.
Each mouse-wheel event increments this number by 1.
@see getMouseButtonClickCounter
*/
int getMouseWheelMoveCounter() const noexcept;
//==============================================================================
/** This lets you prevent the screensaver from becoming active.
Handy if you're running some sort of presentation app where having a screensaver
appear would be annoying.
Pass false to disable the screensaver, and true to re-enable it. (Note that this
won't enable a screensaver unless the user has actually set one up).
The disablement will only happen while the JUCE application is the foreground
process - if another task is running in front of it, then the screensaver will
be unaffected.
@see isScreenSaverEnabled
*/
static void setScreenSaverEnabled (bool isEnabled);
/** Returns true if the screensaver has not been turned off.
This will return the last value passed into setScreenSaverEnabled(). Note that
it won't tell you whether the user is actually using a screen saver, just
whether this app is deliberately preventing one from running.
@see setScreenSaverEnabled
*/
static bool isScreenSaverEnabled();
//==============================================================================
/** Registers a MouseListener that will receive all mouse events that occur on
any component.
@see removeGlobalMouseListener
*/
void addGlobalMouseListener (MouseListener* listener);
/** Unregisters a MouseListener that was added with addGlobalMouseListener().
@see addGlobalMouseListener
*/
void removeGlobalMouseListener (MouseListener* listener);
//==============================================================================
/** Registers a FocusChangeListener that will receive a callback whenever the focused
component changes.
@see removeFocusChangeListener
*/
void addFocusChangeListener (FocusChangeListener* listener);
/** Unregisters a FocusChangeListener that was added with addFocusChangeListener().
@see addFocusChangeListener
*/
void removeFocusChangeListener (FocusChangeListener* listener);
//==============================================================================
/** Registers a DarkModeSettingListener that will receive a callback when the
operating system dark mode setting changes. To query whether dark mode is on
use the isDarkModeActive() method.
@see isDarkModeActive, removeDarkModeSettingListener
*/
void addDarkModeSettingListener (DarkModeSettingListener* listener);
/** Unregisters a DarkModeSettingListener that was added with addDarkModeSettingListener().
@see addDarkModeSettingListener
*/
void removeDarkModeSettingListener (DarkModeSettingListener* listener);
/** True if the operating system "dark mode" is active.
To receive a callback when this setting changes implement the DarkModeSettingListener
interface and use the addDarkModeSettingListener() to register a listener.
@see addDarkModeSettingListener, removeDarkModeSettingListener
*/
bool isDarkModeActive() const;
//==============================================================================
/** Takes a component and makes it full-screen, removing the taskbar, dock, etc.
The component must already be on the desktop for this method to work. It will
be resized to completely fill the screen and any extraneous taskbars, menu bars,
etc will be hidden.
To exit kiosk mode, just call setKioskModeComponent (nullptr). When this is called,
the component that's currently being used will be resized back to the size
and position it was in before being put into this mode.
If allowMenusAndBars is true, things like the menu and dock (on mac) are still
allowed to pop up when the mouse moves onto them. If this is false, it'll try
to hide as much on-screen paraphernalia as possible.
*/
void setKioskModeComponent (Component* componentToUse,
bool allowMenusAndBars = true);
/** Returns the component that is currently being used in kiosk-mode.
This is the component that was last set by setKioskModeComponent(). If none
has been set, this returns nullptr.
*/
Component* getKioskModeComponent() const noexcept { return kioskModeComponent; }
//==============================================================================
/** Returns the number of components that are currently active as top-level
desktop windows.
@see getComponent, Component::addToDesktop
*/
int getNumComponents() const noexcept;
/** Returns one of the top-level desktop window components.
The index is from 0 to getNumComponents() - 1. This could return 0 if the
index is out-of-range.
@see getNumComponents, Component::addToDesktop
*/
Component* getComponent (int index) const noexcept;
/** Finds the component at a given screen location.
This will drill down into top-level windows to find the child component at
the given position.
Returns nullptr if the coordinates are inside a non-JUCE window.
*/
Component* findComponentAt (Point<int> screenPosition) const;
/** The Desktop object has a ComponentAnimator instance which can be used for performing
your animations.
Having a single shared ComponentAnimator object makes it more efficient when multiple
components are being moved around simultaneously. It's also more convenient than having
to manage your own instance of one.
@see ComponentAnimator
*/
ComponentAnimator& getAnimator() noexcept { return animator; }
//==============================================================================
/** Returns the current default look-and-feel for components which don't have one
explicitly set.
@see setDefaultLookAndFeel
*/
LookAndFeel& getDefaultLookAndFeel() noexcept;
/** Changes the default look-and-feel.
@param newDefaultLookAndFeel the new look-and-feel object to use - if this is
set to nullptr, it will revert to using the system's
default one. The object passed-in must be deleted by the
caller when it's no longer needed.
@see getDefaultLookAndFeel
*/
void setDefaultLookAndFeel (LookAndFeel* newDefaultLookAndFeel);
//==============================================================================
/** Provides access to the array of mouse sources, for iteration.
In a traditional single-mouse system, there might be only one MouseInputSource. On a
multi-touch system, there could be one input source per potential finger. The number
of mouse sources returned here may increase dynamically as the program runs.
To find out how many mouse events are currently happening, use getNumDraggingMouseSources().
*/
const Array<MouseInputSource>& getMouseSources() const noexcept;
/** Returns the number of MouseInputSource objects the system has at its disposal.
In a traditional single-mouse system, there might be only one MouseInputSource. On a
multi-touch system, there could be one input source per potential finger. The number
of mouse sources returned here may increase dynamically as the program runs.
To find out how many mouse events are currently happening, use getNumDraggingMouseSources().
@see getMouseSource
*/
int getNumMouseSources() const noexcept;
/** Returns one of the system's MouseInputSource objects.
The index should be from 0 to getNumMouseSources() - 1. Out-of-range indexes will return
a null pointer.
In a traditional single-mouse system, there might be only one object. On a multi-touch
system, there could be one input source per potential finger.
*/
MouseInputSource* getMouseSource (int index) const noexcept;
/** Returns the main mouse input device that the system is using.
@see getNumMouseSources()
*/
MouseInputSource getMainMouseSource() const noexcept;
/** Returns the number of mouse-sources that are currently being dragged.
In a traditional single-mouse system, this will be 0 or 1, depending on whether a
JUCE component has the button down on it. In a multi-touch system, this could
be any number from 0 to the number of simultaneous touches that can be detected.
*/
int getNumDraggingMouseSources() const noexcept;
/** Returns one of the mouse sources that's currently being dragged.
The index should be between 0 and getNumDraggingMouseSources() - 1. If the index is
out of range, or if no mice or fingers are down, this will return a null pointer.
*/
MouseInputSource* getDraggingMouseSource (int index) const noexcept;
/** Ensures that a non-stop stream of mouse-drag events will be sent during the
current mouse-drag operation.
This allows you to make sure that mouseDrag() events are sent continuously, even
when the mouse isn't moving. This can be useful for things like auto-scrolling
components when the mouse is near an edge.
Call this method during a mouseDown() or mouseDrag() callback, specifying the
minimum interval between consecutive mouse drag callbacks. The callbacks
will continue until the mouse is released, and then the interval will be reset,
so you need to make sure it's called every time you begin a drag event.
Passing an interval of 0 or less will cancel the auto-repeat.
@see mouseDrag
*/
void beginDragAutoRepeat (int millisecondsBetweenCallbacks);
//==============================================================================
/** In a tablet/mobile device which can be turned around, this is used to indicate the orientation. */
enum DisplayOrientation
{
upright = 1, /**< Indicates that the device is the normal way up. */
upsideDown = 2, /**< Indicates that the device is upside-down. */
rotatedClockwise = 4, /**< Indicates that the device is turned 90 degrees clockwise from its upright position. */
rotatedAntiClockwise = 8, /**< Indicates that the device is turned 90 degrees anti-clockwise from its upright position. */
allOrientations = 1 + 2 + 4 + 8 /**< A combination of all the orientation values */
};
/** In a tablet device which can be turned around, this returns the current orientation. */
DisplayOrientation getCurrentOrientation() const;
/** Sets which orientations the display is allowed to auto-rotate to.
For devices that support rotating desktops, this lets you specify which of the orientations your app can use.
The parameter is a bitwise or-ed combination of the values in DisplayOrientation, and must contain at least one
set bit.
*/
void setOrientationsEnabled (int allowedOrientations);
/** Returns the set of orientations the display is allowed to rotate to.
@see setOrientationsEnabled
*/
int getOrientationsEnabled() const noexcept;
/** Returns whether the display is allowed to auto-rotate to the given orientation.
Each orientation can be enabled using setOrientationEnabled(). By default, all orientations are allowed.
*/
bool isOrientationEnabled (DisplayOrientation orientation) const noexcept;
//==============================================================================
/** Returns the Displays object representing the connected displays.
@see Displays
*/
const Displays& getDisplays() const noexcept { return *displays; }
//==============================================================================
/** Sets a global scale factor to be used for all desktop windows.
Setting this will also scale the monitor sizes that are returned by getDisplays().
*/
void setGlobalScaleFactor (float newScaleFactor) noexcept;
/** Returns the current global scale factor, as set by setGlobalScaleFactor().
@see setGlobalScaleFactor
*/
float getGlobalScaleFactor() const noexcept { return masterScaleFactor; }
//==============================================================================
/** True if the OS supports semitransparent windows */
static bool canUseSemiTransparentWindows() noexcept;
#if JUCE_MAC && ! defined (DOXYGEN)
[[deprecated ("This macOS-specific method has been deprecated in favour of the cross-platform "
" isDarkModeActive() method.")]]
static bool isOSXDarkModeActive() { return Desktop::getInstance().isDarkModeActive(); }
#endif
//==============================================================================
/** Returns true on a headless system where there are no connected displays. */
bool isHeadless() const noexcept;
private:
//==============================================================================
static Desktop* instance;
friend class Component;
friend class ComponentPeer;
friend class detail::MouseInputSourceImpl;
friend class DeletedAtShutdown;
friend class detail::TopLevelWindowManager;
friend class Displays;
std::unique_ptr<detail::MouseInputSourceList> mouseSources;
ListenerList<MouseListener> mouseListeners;
ListenerList<FocusChangeListener> focusListeners;
ListenerList<DarkModeSettingListener> darkModeSettingListeners;
Array<Component*> desktopComponents;
Array<ComponentPeer*> peers;
std::unique_ptr<Displays> displays;
Point<float> lastFakeMouseMove;
void sendMouseMove();
int mouseClickCounter = 0, mouseWheelCounter = 0;
void incrementMouseClickCounter() noexcept;
void incrementMouseWheelCounter() noexcept;
std::unique_ptr<LookAndFeel> defaultLookAndFeel;
WeakReference<LookAndFeel> currentLookAndFeel;
std::unique_ptr<FocusOutline> focusOutline;
Component* kioskModeComponent = nullptr;
Rectangle<int> kioskComponentOriginalBounds;
bool kioskModeReentrant = false;
int allowedOrientations = allOrientations;
void allowedOrientationsChanged();
float masterScaleFactor;
ComponentAnimator animator;
void timerCallback() override;
void resetTimer();
ListenerList<MouseListener>& getMouseListeners();
void addDesktopComponent (Component*);
void removeDesktopComponent (Component*);
void componentBroughtToFront (Component*);
void setKioskComponent (Component*, bool shouldBeEnabled, bool allowMenusAndBars);
void triggerFocusCallback();
void updateFocusOutline();
void handleAsyncUpdate() override;
static Point<float> getMousePositionFloat();
static double getDefaultMasterScale();
Desktop();
~Desktop() override;
//==============================================================================
class NativeDarkModeChangeDetectorImpl;
std::unique_ptr<NativeDarkModeChangeDetectorImpl> nativeDarkModeChangeDetectorImpl;
static std::unique_ptr<NativeDarkModeChangeDetectorImpl> createNativeDarkModeChangeDetectorImpl();
void darkModeChanged();
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Desktop)
};
} // namespace juce
@@ -0,0 +1,438 @@
/*
==============================================================================
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
{
Displays::Displays (Desktop& desktop)
{
init (desktop);
}
void Displays::init (Desktop& desktop)
{
findDisplays (desktop.getGlobalScaleFactor());
}
const Displays::Display* Displays::getDisplayForRect (Rectangle<int> rect, bool isPhysical) const noexcept
{
int maxArea = -1;
const Display* foundDisplay = nullptr;
for (auto& display : displays)
{
auto displayArea = display.totalArea;
if (isPhysical)
displayArea = (displayArea.withZeroOrigin() * display.scale) + display.topLeftPhysical;
displayArea = displayArea.getIntersection (rect);
auto area = displayArea.getWidth() * displayArea.getHeight();
if (area >= maxArea)
{
maxArea = area;
foundDisplay = &display;
}
}
return foundDisplay;
}
const Displays::Display* Displays::getDisplayForPoint (Point<int> point, bool isPhysical) const noexcept
{
auto minDistance = std::numeric_limits<int>::max();
const Display* foundDisplay = nullptr;
for (auto& display : displays)
{
auto displayArea = display.totalArea;
if (isPhysical)
displayArea = (displayArea.withZeroOrigin() * display.scale) + display.topLeftPhysical;
if (displayArea.contains (point))
return &display;
auto distance = displayArea.getCentre().getDistanceFrom (point);
if (distance <= minDistance)
{
minDistance = distance;
foundDisplay = &display;
}
}
return foundDisplay;
}
Rectangle<int> Displays::physicalToLogical (Rectangle<int> rect, const Display* useScaleFactorOfDisplay) const noexcept
{
return physicalToLogical (rect.toFloat(), useScaleFactorOfDisplay).toNearestInt();
}
Rectangle<float> Displays::physicalToLogical (Rectangle<float> rect, const Display* useScaleFactorOfDisplay) const noexcept
{
const auto* display = useScaleFactorOfDisplay != nullptr ? useScaleFactorOfDisplay
: getDisplayForRect (rect.toNearestInt(), true);
if (display == nullptr)
return rect;
auto globalScale = Desktop::getInstance().getGlobalScaleFactor();
return ((rect - display->topLeftPhysical.toFloat()) / (display->scale / globalScale))
+ (display->totalArea.getTopLeft().toFloat() * globalScale);
}
Rectangle<int> Displays::logicalToPhysical (Rectangle<int> rect, const Display* useScaleFactorOfDisplay) const noexcept
{
return logicalToPhysical (rect.toFloat(), useScaleFactorOfDisplay).toNearestInt();
}
Rectangle<float> Displays::logicalToPhysical (Rectangle<float> rect, const Display* useScaleFactorOfDisplay) const noexcept
{
const auto* display = useScaleFactorOfDisplay != nullptr ? useScaleFactorOfDisplay
: getDisplayForRect (rect.toNearestInt(), false);
if (display == nullptr)
return rect;
auto globalScale = Desktop::getInstance().getGlobalScaleFactor();
return ((rect.toFloat() - (display->totalArea.getTopLeft().toFloat() * globalScale)) * (display->scale / globalScale))
+ display->topLeftPhysical.toFloat();
}
template <typename ValueType>
Point<ValueType> Displays::physicalToLogical (Point<ValueType> point, const Display* useScaleFactorOfDisplay) const noexcept
{
const auto* display = useScaleFactorOfDisplay != nullptr ? useScaleFactorOfDisplay
: getDisplayForPoint (point.roundToInt(), true);
if (display == nullptr)
return point;
auto globalScale = Desktop::getInstance().getGlobalScaleFactor();
Point<ValueType> logicalTopLeft (static_cast<ValueType> (display->totalArea.getX()), static_cast<ValueType> (display->totalArea.getY()));
Point<ValueType> physicalTopLeft (static_cast<ValueType> (display->topLeftPhysical.getX()), static_cast<ValueType> (display->topLeftPhysical.getY()));
return ((point - physicalTopLeft) / (display->scale / globalScale)) + (logicalTopLeft * globalScale);
}
template <typename ValueType>
Point<ValueType> Displays::logicalToPhysical (Point<ValueType> point, const Display* useScaleFactorOfDisplay) const noexcept
{
const auto* display = useScaleFactorOfDisplay != nullptr ? useScaleFactorOfDisplay
: getDisplayForPoint (point.roundToInt(), false);
if (display == nullptr)
return point;
auto globalScale = Desktop::getInstance().getGlobalScaleFactor();
Point<ValueType> logicalTopLeft (static_cast<ValueType> (display->totalArea.getX()), static_cast<ValueType> (display->totalArea.getY()));
Point<ValueType> physicalTopLeft (static_cast<ValueType> (display->topLeftPhysical.getX()), static_cast<ValueType> (display->topLeftPhysical.getY()));
return ((point - (logicalTopLeft * globalScale)) * (display->scale / globalScale)) + physicalTopLeft;
}
const Displays::Display* Displays::getPrimaryDisplay() const noexcept
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
const auto iter = std::find_if (displays.begin(), displays.end(), [] (auto& d) { return d.isMain; });
return iter != displays.end() ? iter : nullptr;
}
RectangleList<int> Displays::getRectangleList (bool userAreasOnly) const
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
RectangleList<int> rl;
for (auto& d : displays)
rl.addWithoutMerging (userAreasOnly ? d.userArea : d.totalArea);
return rl;
}
Rectangle<int> Displays::getTotalBounds (bool userAreasOnly) const
{
return getRectangleList (userAreasOnly).getBounds();
}
void Displays::refresh()
{
Array<Display> oldDisplays;
oldDisplays.swapWith (displays);
init (Desktop::getInstance());
if (oldDisplays != displays)
{
for (auto i = ComponentPeer::getNumPeers(); --i >= 0;)
if (auto* peer = ComponentPeer::getPeer (i))
peer->handleScreenSizeChange();
}
}
static auto tie (const Displays::Display& d)
{
return std::tie (d.dpi,
d.isMain,
d.keyboardInsets,
d.safeAreaInsets,
d.scale,
d.topLeftPhysical,
d.totalArea,
d.userArea);
}
static bool operator== (const Displays::Display& d1, const Displays::Display& d2) noexcept
{
return tie (d1) == tie (d2);
}
//==============================================================================
// These methods are used for converting the totalArea and userArea Rectangles in Display from physical to logical
// pixels. We do this by constructing a graph of connected displays where the root node has position (0, 0); this can be
// safely converted to logical pixels using its scale factor and we can then traverse the graph and work out the logical pixels
// for all the other connected displays. We need to do this as the logical bounds of a display depend not only on its scale
// factor but also the scale factor of the displays connected to it.
/**
Represents a node in our graph of displays.
*/
struct DisplayNode
{
/** The Display object that this represents. */
Displays::Display* display;
/** True if this represents the 'root' display with position (0, 0). */
bool isRoot = false;
/** The parent node of this node in our display graph. This will have a correct logicalArea. */
DisplayNode* parent = nullptr;
/** The logical area to be calculated. This will be valid after processDisplay() has
been called on this node.
*/
Rectangle<double> logicalArea;
};
/** Recursive - will calculate and set the logicalArea member of current. */
static void processDisplay (DisplayNode* currentNode, Array<DisplayNode>& allNodes)
{
const auto physicalArea = currentNode->display->totalArea.toDouble();
const auto scale = currentNode->display->scale;
if (! currentNode->isRoot)
{
const auto logicalWidth = physicalArea.getWidth() / scale;
const auto logicalHeight = physicalArea.getHeight() / scale;
const auto physicalParentArea = currentNode->parent->display->totalArea.toDouble();
const auto logicalParentArea = currentNode->parent->logicalArea; // logical area of parent has already been calculated
const auto parentScale = currentNode->parent->display->scale;
Rectangle<double> logicalArea (0.0, 0.0, logicalWidth, logicalHeight);
if (approximatelyEqual (physicalArea.getRight(), physicalParentArea.getX())) logicalArea.setPosition ({ logicalParentArea.getX() - logicalWidth, physicalArea.getY() / parentScale }); // on left
else if (approximatelyEqual (physicalArea.getX(), physicalParentArea.getRight())) logicalArea.setPosition ({ logicalParentArea.getRight(), physicalArea.getY() / parentScale }); // on right
else if (approximatelyEqual (physicalArea.getBottom(), physicalParentArea.getY())) logicalArea.setPosition ({ physicalArea.getX() / parentScale, logicalParentArea.getY() - logicalHeight }); // on top
else if (approximatelyEqual (physicalArea.getY(), physicalParentArea.getBottom())) logicalArea.setPosition ({ physicalArea.getX() / parentScale, logicalParentArea.getBottom() }); // on bottom
else jassertfalse;
currentNode->logicalArea = logicalArea;
}
else
{
// If currentNode is the root (position (0, 0)) then we can just scale the physical area
currentNode->logicalArea = physicalArea / scale;
currentNode->parent = currentNode;
}
// Find child nodes
Array<DisplayNode*> children;
for (auto& node : allNodes)
{
// Already calculated
if (node.parent != nullptr)
continue;
const auto otherPhysicalArea = node.display->totalArea.toDouble();
// If the displays are touching on any side
if (approximatelyEqual (otherPhysicalArea.getX(), physicalArea.getRight()) || approximatelyEqual (otherPhysicalArea.getRight(), physicalArea.getX())
|| approximatelyEqual (otherPhysicalArea.getY(), physicalArea.getBottom()) || approximatelyEqual (otherPhysicalArea.getBottom(), physicalArea.getY()))
{
node.parent = currentNode;
children.add (&node);
}
}
// Recursively process all child nodes
for (auto child : children)
processDisplay (child, allNodes);
}
/** This is called when the displays Array has been filled out with the info for all connected displays and the
totalArea and userArea Rectangles need to be converted from physical to logical coordinates.
*/
void Displays::updateToLogical()
{
if (displays.size() == 1)
{
auto& display = displays.getReference (0);
display.totalArea = (display.totalArea.toDouble() / display.scale).toNearestInt();
display.userArea = (display.userArea.toDouble() / display.scale).toNearestInt();
return;
}
Array<DisplayNode> displayNodes;
for (auto& d : displays)
{
DisplayNode node;
node.display = &d;
if (d.totalArea.getTopLeft() == Point<int>())
node.isRoot = true;
displayNodes.add (node);
}
auto* root = [&displayNodes]() -> DisplayNode*
{
for (auto& node : displayNodes)
if (node.isRoot)
return &node;
auto minDistance = std::numeric_limits<int>::max();
DisplayNode* retVal = nullptr;
for (auto& node : displayNodes)
{
auto distance = node.display->totalArea.getTopLeft().getDistanceFrom ({});
if (distance < minDistance)
{
minDistance = distance;
retVal = &node;
}
}
if (retVal != nullptr)
retVal->isRoot = true;
return retVal;
}();
// Must have a root node!
jassert (root != nullptr);
// Recursively traverse the display graph from the root and work out logical bounds
processDisplay (root, displayNodes);
for (auto& node : displayNodes)
{
// All of the nodes should have a parent
jassert (node.parent != nullptr);
auto relativeUserArea = (node.display->userArea.toDouble() - node.display->totalArea.toDouble().getTopLeft()) / node.display->scale;
// Now set Display::totalArea and ::userArea using the logical area that we have calculated
node.display->topLeftPhysical = node.display->totalArea.getTopLeft();
node.display->totalArea = node.logicalArea.toNearestInt();
node.display->userArea = (relativeUserArea + node.logicalArea.getTopLeft()).toNearestInt();
}
}
#ifndef DOXYGEN
// explicit template instantiations
template Point<int> Displays::physicalToLogical (Point<int>, const Display*) const noexcept;
template Point<float> Displays::physicalToLogical (Point<float>, const Display*) const noexcept;
template Point<int> Displays::logicalToPhysical (Point<int>, const Display*) const noexcept;
template Point<float> Displays::logicalToPhysical (Point<float>, const Display*) const noexcept;
#endif
//==============================================================================
// Deprecated methods
const Displays::Display& Displays::getDisplayContaining (Point<int> position) const noexcept
{
JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED
const auto* best = &displays.getReference (0);
auto bestDistance = std::numeric_limits<int>::max();
for (auto& d : displays)
{
if (d.totalArea.contains (position))
{
best = &d;
break;
}
auto distance = d.totalArea.getCentre().getDistanceFrom (position);
if (distance < bestDistance)
{
bestDistance = distance;
best = &d;
}
}
return *best;
}
const Displays::Display& Displays::findDisplayForRect (Rectangle<int> rect, bool isPhysical) const noexcept
{
if (auto* display = getDisplayForRect (rect, isPhysical))
return *display;
return emptyDisplay;
}
const Displays::Display& Displays::findDisplayForPoint (Point<int> point, bool isPhysical) const noexcept
{
if (auto* display = getDisplayForPoint (point, isPhysical))
return *display;
return emptyDisplay;
}
const Displays::Display& Displays::getMainDisplay() const noexcept
{
if (auto* display = getPrimaryDisplay())
return *display;
return emptyDisplay;
}
} // namespace juce
@@ -0,0 +1,209 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Manages details about connected display devices.
@tags{GUI}
*/
class JUCE_API Displays
{
private:
Displays (Desktop&);
public:
//==============================================================================
/** Represents a connected display device. */
struct JUCE_API Display
{
/** This will be true if this is the user's main display device. */
bool isMain;
/** The total area of this display in logical pixels including any OS-dependent objects
like the taskbar, menu bar, etc.
*/
Rectangle<int> totalArea;
/** The total area of this display in logical pixels which isn't covered by OS-dependent
objects like the taskbar, menu bar, etc.
*/
Rectangle<int> userArea;
/** Represents the area of this display in logical pixels that is not functional for
displaying content.
On mobile devices this may be the area covered by display cutouts and notches, where
you still want to draw a background but should not position important content.
*/
BorderSize<int> safeAreaInsets;
/** Represents the area of this display in logical pixels that is obscured by an
onscreen keyboard.
This is currently only supported on iOS, and on Android 11+.
This will only return the bounds of the keyboard when it is in 'docked' mode.
If the keyboard is floating (e.g. on an iPad using the split keyboard mode),
no insets will be reported.
*/
BorderSize<int> keyboardInsets;
/** The top-left of this display in physical coordinates. */
Point<int> topLeftPhysical;
/** The scale factor of this display.
For higher-resolution displays, or displays with a user-defined scale factor set,
this may be a value other than 1.0.
This value is used to convert between physical and logical pixels. For example, a Component
with size 10x10 will use 20x20 physical pixels on a display with a scale factor of 2.0.
*/
double scale;
/** The DPI of the display.
This is the number of physical pixels per inch. To get the number of logical
pixels per inch, divide this by the Display::scale value.
*/
double dpi;
/** The vertical refresh rate of the display if applicable.
Currently this is only used on Linux for display rate repainting.
*/
std::optional<double> verticalFrequencyHz;
};
//==============================================================================
/** Converts an integer Rectangle from physical to logical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Rectangle to be converted is on.
*/
Rectangle<int> physicalToLogical (Rectangle<int> physicalRect,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Converts a floating-point Rectangle from physical to logical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Rectangle to be converted is on.
*/
Rectangle<float> physicalToLogical (Rectangle<float> physicalRect,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Converts an integer Rectangle from logical to physical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Rectangle to be converted is on.
*/
Rectangle<int> logicalToPhysical (Rectangle<int> logicalRect,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Converts a floating-point Rectangle from logical to physical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Rectangle to be converted is on.
*/
Rectangle<float> logicalToPhysical (Rectangle<float> logicalRect,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Converts a Point from physical to logical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Point to be converted is on.
*/
template <typename ValueType>
Point<ValueType> physicalToLogical (Point<ValueType> physicalPoint,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Converts a Point from logical to physical pixels.
If useScaleFactorOfDisplay is not null then its scale factor will be used for the conversion
regardless of the display that the Point to be converted is on.
*/
template <typename ValueType>
Point<ValueType> logicalToPhysical (Point<ValueType> logicalPoint,
const Display* useScaleFactorOfDisplay = nullptr) const noexcept;
/** Returns the Display object representing the display containing a given Rectangle (either
in logical or physical pixels), or nullptr if there are no connected displays.
If the Rectangle lies outside all the displays then the nearest one will be returned.
*/
const Display* getDisplayForRect (Rectangle<int> rect, bool isPhysical = false) const noexcept;
/** Returns the Display object representing the display containing a given Point (either
in logical or physical pixels), or nullptr if there are no connected displays.
If the Point lies outside all the displays then the nearest one will be returned.
*/
const Display* getDisplayForPoint (Point<int> point, bool isPhysical = false) const noexcept;
/** Returns the Display object representing the display acting as the user's main screen, or nullptr
if there are no connected displays.
*/
const Display* getPrimaryDisplay() const noexcept;
/** Returns a RectangleList made up of all the displays in LOGICAL pixels. */
RectangleList<int> getRectangleList (bool userAreasOnly) const;
/** Returns the smallest bounding box which contains all the displays in LOGICAL pixels. */
Rectangle<int> getTotalBounds (bool userAreasOnly) const;
/** An Array containing the Display objects for all of the connected displays. */
Array<Display> displays;
#ifndef DOXYGEN
/** @internal */
void refresh();
[[deprecated ("Use the getDisplayForPoint or getDisplayForRect methods instead "
"as they can deal with converting between logical and physical pixels.")]]
const Display& getDisplayContaining (Point<int> position) const noexcept;
// These methods have been deprecated - use the methods which return a Display* instead as they will return
// nullptr on headless systems with no connected displays
[[deprecated]] const Display& findDisplayForRect (Rectangle<int>, bool isPhysical = false) const noexcept;
[[deprecated]] const Display& findDisplayForPoint (Point<int>, bool isPhysical = false) const noexcept;
[[deprecated]] const Display& getMainDisplay() const noexcept;
#endif
private:
friend class Desktop;
void init (Desktop&);
void findDisplays (float masterScale);
void updateToLogical();
Display emptyDisplay;
};
} // namespace juce
@@ -0,0 +1,33 @@
/*
==============================================================================
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_NATIVE_ACCESSIBILITY_INCLUDED
void AccessibilityHelpers::notifyAccessibilityEvent (const AccessibilityHandler&, Event) {}
#endif
} // namespace juce::detail
@@ -0,0 +1,70 @@
/*
==============================================================================
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
{
struct AccessibilityHelpers
{
AccessibilityHelpers() = delete;
enum class Event
{
elementCreated,
elementDestroyed,
elementMovedOrResized,
focusChanged,
windowOpened,
windowClosed
};
static void notifyAccessibilityEvent (const AccessibilityHandler&, Event);
static String getApplicationOrPluginName()
{
#if defined (JucePlugin_Name)
return JucePlugin_Name;
#else
if (auto* app = JUCEApplicationBase::getInstance())
return app->getApplicationName();
return "JUCE Application";
#endif
}
template <typename MemberFn>
static const AccessibilityHandler* getEnclosingHandlerWithInterface (const AccessibilityHandler* handler, MemberFn fn)
{
if (handler == nullptr)
return nullptr;
if ((handler->*fn)() != nullptr)
return handler;
return getEnclosingHandlerWithInterface (handler->getParent(), fn);
}
};
} // namespace juce::detail
@@ -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
{
struct AlertWindowHelpers
{
AlertWindowHelpers() = delete;
static std::unique_ptr<ScopedMessageBoxInterface> create (const MessageBoxOptions& opts)
{
class AlertWindowImpl : public detail::ScopedMessageBoxInterface
{
public:
explicit AlertWindowImpl (const MessageBoxOptions& opts) : options (opts) {}
void runAsync (std::function<void (int)> recipient) override
{
if (auto* comp = setUpAlert())
comp->enterModalState (true, ModalCallbackFunction::create (std::move (recipient)), true);
else
NullCheckedInvocation::invoke (recipient, 0);
}
int runSync() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
if (auto comp = rawToUniquePtr (setUpAlert()))
return comp->runModalLoop();
#endif
jassertfalse;
return 0;
}
void close() override
{
if (alert != nullptr)
if (alert->isCurrentlyModal())
alert->exitModalState();
alert = nullptr;
}
private:
Component* setUpAlert()
{
auto* component = options.getAssociatedComponent();
auto& lf = component != nullptr ? component->getLookAndFeel()
: LookAndFeel::getDefaultLookAndFeel();
alert = lf.createAlertWindow (options.getTitle(),
options.getMessage(),
options.getButtonText (0),
options.getButtonText (1),
options.getButtonText (2),
options.getIconType(),
options.getNumButtons(),
component);
if (alert == nullptr)
{
// You have to return an alert box!
jassertfalse;
return nullptr;
}
if (auto* parent = options.getParentComponent())
{
parent->addAndMakeVisible (alert);
if (options.getAssociatedComponent() == nullptr)
alert->setCentrePosition (parent->getLocalBounds().getCentre());
}
alert->setAlwaysOnTop (WindowUtils::areThereAnyAlwaysOnTopWindows());
return alert;
}
const MessageBoxOptions options;
Component::SafePointer<AlertWindow> alert;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlertWindowImpl)
};
return std::make_unique<AlertWindowImpl> (opts);
}
};
} // namespace juce::detail
@@ -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::detail
{
//==============================================================================
class ButtonAccessibilityHandler : public AccessibilityHandler
{
public:
ButtonAccessibilityHandler (Button& buttonToWrap, AccessibilityRole roleIn)
: AccessibilityHandler (buttonToWrap,
isRadioButton (buttonToWrap) ? AccessibilityRole::radioButton : roleIn,
getAccessibilityActions (buttonToWrap),
getAccessibilityInterfaces (buttonToWrap)),
button (buttonToWrap)
{}
AccessibleState getCurrentState() const override
{
auto state = AccessibilityHandler::getCurrentState();
if (button.isToggleable())
{
state = state.withCheckable();
if (button.getToggleState())
state = state.withChecked();
}
return state;
}
String getTitle() const override
{
auto title = AccessibilityHandler::getTitle();
if (title.isEmpty())
return button.getButtonText();
return title;
}
String getHelp() const override
{
return button.getTooltip();
}
private:
class ButtonValueInterface : public AccessibilityTextValueInterface
{
public:
explicit ButtonValueInterface (Button& buttonToWrap)
: button (buttonToWrap)
{
}
bool isReadOnly() const override { return true; }
String getCurrentValueAsString() const override { return button.getToggleState() ? "On" : "Off"; }
void setValueAsString (const String&) override {}
private:
Button& button;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonValueInterface)
};
static bool isRadioButton (const Button& button) noexcept
{
return button.getRadioGroupId() != 0;
}
static AccessibilityActions getAccessibilityActions (Button& button)
{
auto actions = AccessibilityActions().addAction (AccessibilityActionType::press,
[&button] { button.triggerClick(); });
if (button.isToggleable())
actions = actions.addAction (AccessibilityActionType::toggle,
[&button] { button.setToggleState (! button.getToggleState(), sendNotification); });
return actions;
}
static Interfaces getAccessibilityInterfaces (Button& button)
{
if (button.isToggleable())
return { std::make_unique<ButtonValueInterface> (button) };
return {};
}
Button& button;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonAccessibilityHandler)
};
} // namespace juce::detail
@@ -0,0 +1,280 @@
/*
==============================================================================
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
{
constexpr char colourPropertyPrefix[] = "jcclr_";
//==============================================================================
struct ComponentHelpers
{
using SH = ScalingHelpers;
#if JUCE_MODAL_LOOPS_PERMITTED
static void* runModalLoopCallback (void* userData)
{
return (void*) (pointer_sized_int) static_cast<Component*> (userData)->runModalLoop();
}
#endif
static Identifier getColourPropertyID (int colourID)
{
char buffer[32];
auto* end = buffer + numElementsInArray (buffer) - 1;
auto* t = end;
*t = 0;
for (auto v = (uint32) colourID;;)
{
*--t = "0123456789abcdef" [v & 15];
v >>= 4;
if (v == 0)
break;
}
for (int i = (int) sizeof (colourPropertyPrefix) - 1; --i >= 0;)
*--t = colourPropertyPrefix[i];
return t;
}
//==============================================================================
static bool hitTest (Component& comp, Point<float> localPoint)
{
const auto intPoint = localPoint.roundToInt();
return Rectangle<int> { comp.getWidth(), comp.getHeight() }.contains (intPoint)
&& comp.hitTest (intPoint.x, intPoint.y);
}
// converts an unscaled position within a peer to the local position within that peer's component
template <typename PointOrRect>
static PointOrRect rawPeerPositionToLocal (const Component& comp, PointOrRect pos) noexcept
{
if (comp.isTransformed())
pos = pos.transformedBy (comp.getTransform().inverted());
return SH::unscaledScreenPosToScaled (comp, pos);
}
// converts a position within a peer's component to the unscaled position within the peer
template <typename PointOrRect>
static PointOrRect localPositionToRawPeerPos (const Component& comp, PointOrRect pos) noexcept
{
if (comp.isTransformed())
pos = pos.transformedBy (comp.getTransform());
return SH::scaledScreenPosToUnscaled (comp, pos);
}
template <typename PointOrRect>
static PointOrRect convertFromParentSpace (const Component& comp, const PointOrRect pointInParentSpace)
{
const auto transformed = comp.affineTransform != nullptr ? pointInParentSpace.transformedBy (comp.affineTransform->inverted())
: pointInParentSpace;
if (comp.isOnDesktop())
{
if (auto* peer = comp.getPeer())
return SH::unscaledScreenPosToScaled (comp, peer->globalToLocal (SH::scaledScreenPosToUnscaled (transformed)));
jassertfalse;
return transformed;
}
if (comp.getParentComponent() == nullptr)
return SH::subtractPosition (SH::unscaledScreenPosToScaled (comp, SH::scaledScreenPosToUnscaled (transformed)), comp);
return SH::subtractPosition (transformed, comp);
}
template <typename PointOrRect>
static PointOrRect convertToParentSpace (const Component& comp, const PointOrRect pointInLocalSpace)
{
const auto preTransform = [&]
{
if (comp.isOnDesktop())
{
if (auto* peer = comp.getPeer())
return SH::unscaledScreenPosToScaled (peer->localToGlobal (SH::scaledScreenPosToUnscaled (comp, pointInLocalSpace)));
jassertfalse;
return pointInLocalSpace;
}
if (comp.getParentComponent() == nullptr)
return SH::unscaledScreenPosToScaled (SH::scaledScreenPosToUnscaled (comp, SH::addPosition (pointInLocalSpace, comp)));
return SH::addPosition (pointInLocalSpace, comp);
}();
return comp.affineTransform != nullptr ? preTransform.transformedBy (*comp.affineTransform)
: preTransform;
}
template <typename PointOrRect>
static PointOrRect convertFromDistantParentSpace (const Component* parent, const Component& target, PointOrRect coordInParent)
{
auto* directParent = target.getParentComponent();
jassert (directParent != nullptr);
if (directParent == parent)
return convertFromParentSpace (target, coordInParent);
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6011)
return convertFromParentSpace (target, convertFromDistantParentSpace (parent, *directParent, coordInParent));
JUCE_END_IGNORE_WARNINGS_MSVC
}
template <typename PointOrRect>
static PointOrRect convertCoordinate (const Component* target, const Component* source, PointOrRect p)
{
while (source != nullptr)
{
if (source == target)
return p;
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6011)
if (source->isParentOf (target))
return convertFromDistantParentSpace (source, *target, p);
JUCE_END_IGNORE_WARNINGS_MSVC
p = convertToParentSpace (*source, p);
source = source->getParentComponent();
}
jassert (source == nullptr);
if (target == nullptr)
return p;
auto* topLevelComp = target->getTopLevelComponent();
p = convertFromParentSpace (*topLevelComp, p);
if (topLevelComp == target)
return p;
return convertFromDistantParentSpace (topLevelComp, *target, p);
}
static bool clipObscuredRegions (const Component& comp, Graphics& g,
const Rectangle<int> clipRect, Point<int> delta)
{
bool wasClipped = false;
for (int i = comp.childComponentList.size(); --i >= 0;)
{
auto& child = *comp.childComponentList.getUnchecked (i);
if (child.isVisible() && ! child.isTransformed())
{
auto newClip = clipRect.getIntersection (child.boundsRelativeToParent);
if (! newClip.isEmpty())
{
if (child.isOpaque() && child.componentTransparency == 0)
{
g.excludeClipRegion (newClip + delta);
wasClipped = true;
}
else
{
auto childPos = child.getPosition();
if (clipObscuredRegions (child, g, newClip - childPos, childPos + delta))
wasClipped = true;
}
}
}
}
return wasClipped;
}
static Rectangle<int> getParentOrMainMonitorBounds (const Component& comp)
{
if (auto* p = comp.getParentComponent())
return p->getLocalBounds();
return Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
}
static void releaseAllCachedImageResources (Component& c)
{
if (auto* cached = c.getCachedComponentImage())
cached->releaseResources();
for (auto* child : c.childComponentList)
releaseAllCachedImageResources (*child);
}
//==============================================================================
static bool modalWouldBlockComponent (const Component& maybeBlocked, Component* modal)
{
return modal != nullptr
&& modal != &maybeBlocked
&& ! modal->isParentOf (&maybeBlocked)
&& ! modal->canModalEventBeSentToComponent (&maybeBlocked);
}
template <typename Function>
static void sendMouseEventToComponentsThatAreBlockedByModal (Component& modal, Function&& function)
{
for (auto& ms : Desktop::getInstance().getMouseSources())
if (auto* c = ms.getComponentUnderMouse())
if (modalWouldBlockComponent (*c, &modal))
(c->*function) (ms, SH::screenPosToLocalPos (*c, ms.getScreenPosition()), Time::getCurrentTime());
}
class ModalComponentManagerChangeNotifier
{
public:
static auto& getInstance()
{
static ModalComponentManagerChangeNotifier instance;
return instance;
}
ErasedScopeGuard addListener (std::function<void()> l)
{
return listeners.addListener (std::move (l));
}
void modalComponentManagerChanged()
{
listeners.call();
}
private:
ModalComponentManagerChangeNotifier() = default;
detail::CallbackListenerList<> listeners;
};
};
} // namespace juce::detail
@@ -0,0 +1,35 @@
/*
==============================================================================
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
{
struct CustomMouseCursorInfo
{
ScaledImage image;
Point<int> hotspot;
};
} // namespace juce::detail
@@ -0,0 +1,117 @@
/*
==============================================================================
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
{
struct FocusHelpers
{
FocusHelpers() = delete;
static int getOrder (const Component* c)
{
auto order = c->getExplicitFocusOrder();
return order > 0 ? order : std::numeric_limits<int>::max();
}
template <typename FocusContainerFn>
static void findAllComponents (Component* parent,
std::vector<Component*>& components,
FocusContainerFn isFocusContainer)
{
if (parent == nullptr || parent->getNumChildComponents() == 0)
return;
std::vector<Component*> localComponents;
for (auto* c : parent->getChildren())
if (c->isVisible() && c->isEnabled())
localComponents.push_back (c);
const auto compareComponents = [&] (const Component* a, const Component* b)
{
const auto getComponentOrderAttributes = [] (const Component* c)
{
return std::make_tuple (getOrder (c),
c->isAlwaysOnTop() ? 0 : 1,
c->getY(),
c->getX());
};
return getComponentOrderAttributes (a) < getComponentOrderAttributes (b);
};
// This will sort so that they are ordered in terms of explicit focus,
// always on top, left-to-right, and then top-to-bottom.
std::stable_sort (localComponents.begin(), localComponents.end(), compareComponents);
for (auto* c : localComponents)
{
components.push_back (c);
if (! (c->*isFocusContainer)())
findAllComponents (c, components, isFocusContainer);
}
}
enum class NavigationDirection { forwards, backwards };
template <typename FocusContainerFn>
static Component* navigateFocus (Component* current,
Component* focusContainer,
NavigationDirection direction,
FocusContainerFn isFocusContainer)
{
if (focusContainer != nullptr)
{
std::vector<Component*> components;
findAllComponents (focusContainer, components, isFocusContainer);
const auto iter = std::find (components.cbegin(), components.cend(), current);
if (iter == components.cend())
return nullptr;
switch (direction)
{
case NavigationDirection::forwards:
if (iter != std::prev (components.cend()))
return *std::next (iter);
break;
case NavigationDirection::backwards:
if (iter != components.cbegin())
return *std::prev (iter);
break;
}
}
return nullptr;
}
};
} // namespace juce::detail
@@ -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.
==============================================================================
*/
namespace juce::detail
{
struct FocusRestorer
{
FocusRestorer() : lastFocus (Component::getCurrentlyFocusedComponent()) {}
~FocusRestorer()
{
if (lastFocus != nullptr
&& lastFocus->isShowing()
&& ! lastFocus->isCurrentlyBlockedByAnotherModalComponent())
lastFocus->grabKeyboardFocus();
}
WeakReference<Component> lastFocus;
JUCE_DECLARE_NON_COPYABLE (FocusRestorer)
};
} // namespace juce::detail
@@ -0,0 +1,62 @@
/*
==============================================================================
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
{
struct LookAndFeelHelpers
{
LookAndFeelHelpers() = delete;
static Colour createBaseColour (Colour buttonColour,
bool hasKeyboardFocus,
bool shouldDrawButtonAsHighlighted,
bool shouldDrawButtonAsDown) noexcept
{
const float sat = hasKeyboardFocus ? 1.3f : 0.9f;
const Colour baseColour (buttonColour.withMultipliedSaturation (sat));
if (shouldDrawButtonAsDown) return baseColour.contrasting (0.2f);
if (shouldDrawButtonAsHighlighted) return baseColour.contrasting (0.1f);
return baseColour;
}
static TextLayout layoutTooltipText (const String& text, Colour colour) noexcept
{
const float tooltipFontSize = 13.0f;
const int maxToolTipWidth = 400;
AttributedString s;
s.setJustification (Justification::centred);
s.append (text, Font (tooltipFontSize, Font::bold), colour);
TextLayout tl;
tl.createLayoutWithBalancedLineLengths (s, (float) maxToolTipWidth);
return tl;
}
};
} // namespace juce::detail
@@ -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::detail
{
class MouseInputSourceImpl : private AsyncUpdater
{
public:
using SH = ScalingHelpers;
MouseInputSourceImpl (int i, MouseInputSource::InputSourceType type)
: index (i),
inputType (type)
{}
//==============================================================================
bool isDragging() const noexcept
{
return buttonState.isAnyMouseButtonDown();
}
Component* getComponentUnderMouse() const noexcept
{
return componentUnderMouse.get();
}
ModifierKeys getCurrentModifiers() const noexcept
{
return ModifierKeys::currentModifiers
.withoutMouseButtons()
.withFlags (buttonState.getRawFlags());
}
ComponentPeer* getPeer() noexcept
{
if (! ComponentPeer::isValidPeer (lastPeer))
lastPeer = nullptr;
return lastPeer;
}
static Component* findComponentAt (Point<float> screenPos, ComponentPeer* peer)
{
if (! ComponentPeer::isValidPeer (peer))
return nullptr;
auto relativePos = SH::unscaledScreenPosToScaled (peer->getComponent(),
peer->globalToLocal (screenPos));
auto& comp = peer->getComponent();
// (the contains() call is needed to test for overlapping desktop windows)
if (comp.contains (relativePos))
return comp.getComponentAt (relativePos);
return nullptr;
}
Point<float> getScreenPosition() const noexcept
{
// This needs to return the live position if possible, but it mustn't update the lastScreenPos
// value, because that can cause continuity problems.
return SH::unscaledScreenPosToScaled (getRawScreenPosition());
}
Point<float> getRawScreenPosition() const noexcept
{
return unboundedMouseOffset + (inputType != MouseInputSource::InputSourceType::touch ? MouseInputSource::getCurrentRawMousePosition()
: lastPointerState.position);
}
void setScreenPosition (Point<float> p)
{
MouseInputSource::setRawMousePosition (SH::scaledScreenPosToUnscaled (p));
}
//==============================================================================
#if JUCE_DUMP_MOUSE_EVENTS
#define JUCE_MOUSE_EVENT_DBG(desc, screenPos) DBG ("Mouse " << desc << " #" << index \
<< ": " << SH::screenPosToLocalPos (comp, screenPos).toString() \
<< " - Comp: " << String::toHexString ((pointer_sized_int) &comp));
#else
#define JUCE_MOUSE_EVENT_DBG(desc, screenPos)
#endif
void sendMouseEnter (Component& comp, const detail::PointerState& pointerState, Time time)
{
JUCE_MOUSE_EVENT_DBG ("enter", pointerState.position)
comp.internalMouseEnter (MouseInputSource (this),
SH::screenPosToLocalPos (comp, pointerState.position),
time);
}
void sendMouseExit (Component& comp, const detail::PointerState& pointerState, Time time)
{
JUCE_MOUSE_EVENT_DBG ("exit", pointerState.position)
comp.internalMouseExit (MouseInputSource (this),
SH::screenPosToLocalPos (comp, pointerState.position),
time);
}
void sendMouseMove (Component& comp, const detail::PointerState& pointerState, Time time)
{
JUCE_MOUSE_EVENT_DBG ("move", pointerState.position)
comp.internalMouseMove (MouseInputSource (this),
SH::screenPosToLocalPos (comp, pointerState.position),
time);
}
void sendMouseDown (Component& comp, const detail::PointerState& pointerState, Time time)
{
JUCE_MOUSE_EVENT_DBG ("down", pointerState.position)
comp.internalMouseDown (MouseInputSource (this),
pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)),
time);
}
void sendMouseDrag (Component& comp, const detail::PointerState& pointerState, Time time)
{
JUCE_MOUSE_EVENT_DBG ("drag", pointerState.position)
comp.internalMouseDrag (MouseInputSource (this),
pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)),
time);
}
void sendMouseUp (Component& comp, const detail::PointerState& pointerState, Time time, ModifierKeys oldMods)
{
JUCE_MOUSE_EVENT_DBG ("up", pointerState.position)
comp.internalMouseUp (MouseInputSource (this),
pointerState.withPosition (SH::screenPosToLocalPos (comp, pointerState.position)),
time,
oldMods);
}
void sendMouseWheel (Component& comp, Point<float> screenPos, Time time, const MouseWheelDetails& wheel)
{
JUCE_MOUSE_EVENT_DBG ("wheel", screenPos)
comp.internalMouseWheel (MouseInputSource (this),
SH::screenPosToLocalPos (comp, screenPos),
time,
wheel);
}
void sendMagnifyGesture (Component& comp, Point<float> screenPos, Time time, float amount)
{
JUCE_MOUSE_EVENT_DBG ("magnify", screenPos)
comp.internalMagnifyGesture (MouseInputSource (this),
SH::screenPosToLocalPos (comp, screenPos),
time,
amount);
}
#undef JUCE_MOUSE_EVENT_DBG
//==============================================================================
// (returns true if the button change caused a modal event loop)
bool setButtons (const detail::PointerState& pointerState, Time time, ModifierKeys newButtonState)
{
if (buttonState == newButtonState)
return false;
// (avoid sending a spurious mouse-drag when we receive a mouse-up)
if (! (isDragging() && ! newButtonState.isAnyMouseButtonDown()))
setPointerState (pointerState, time, false);
// (ignore secondary clicks when there's already a button down)
if (buttonState.isAnyMouseButtonDown() == newButtonState.isAnyMouseButtonDown())
{
buttonState = newButtonState;
return false;
}
auto lastCounter = mouseEventCounter;
if (buttonState.isAnyMouseButtonDown())
{
if (auto* current = getComponentUnderMouse())
{
auto oldMods = getCurrentModifiers();
buttonState = newButtonState; // must change this before calling sendMouseUp, in case it runs a modal loop
sendMouseUp (*current, pointerState.withPositionOffset (unboundedMouseOffset), time, oldMods);
if (lastCounter != mouseEventCounter)
return true; // if a modal loop happened, then newButtonState is no longer valid.
}
enableUnboundedMouseMovement (false, false);
}
buttonState = newButtonState;
if (buttonState.isAnyMouseButtonDown())
{
Desktop::getInstance().incrementMouseClickCounter();
if (auto* current = getComponentUnderMouse())
{
registerMouseDown (pointerState.position, time, *current, buttonState,
inputType == MouseInputSource::InputSourceType::touch);
sendMouseDown (*current, pointerState, time);
}
}
return lastCounter != mouseEventCounter;
}
void setComponentUnderMouse (Component* newComponent, const detail::PointerState& pointerState, Time time)
{
auto* current = getComponentUnderMouse();
if (newComponent != current)
{
WeakReference<Component> safeNewComp (newComponent);
auto originalButtonState = buttonState;
if (current != nullptr)
{
WeakReference<Component> safeOldComp (current);
setButtons (pointerState, time, ModifierKeys());
if (auto oldComp = safeOldComp.get())
{
componentUnderMouse = safeNewComp;
sendMouseExit (*oldComp, pointerState, time);
}
buttonState = originalButtonState;
}
componentUnderMouse = safeNewComp.get();
current = safeNewComp.get();
if (current != nullptr)
sendMouseEnter (*current, pointerState, time);
revealCursor (false);
setButtons (pointerState, time, originalButtonState);
}
}
void setPeer (ComponentPeer& newPeer, const detail::PointerState& pointerState, Time time)
{
if (&newPeer != lastPeer && ( findComponentAt (pointerState.position, &newPeer) != nullptr
|| findComponentAt (pointerState.position, lastPeer) == nullptr))
{
setComponentUnderMouse (nullptr, pointerState, time);
lastPeer = &newPeer;
setComponentUnderMouse (findComponentAt (pointerState.position, getPeer()), pointerState, time);
}
}
void setPointerState (const detail::PointerState& newPointerState, Time time, bool forceUpdate)
{
const auto& newScreenPos = newPointerState.position;
if (! isDragging())
setComponentUnderMouse (findComponentAt (newScreenPos, getPeer()), newPointerState, time);
if ((newPointerState != lastPointerState) || forceUpdate)
{
cancelPendingUpdate();
lastPointerState = newPointerState;
if (auto* current = getComponentUnderMouse())
{
if (isDragging())
{
registerMouseDrag (newScreenPos);
sendMouseDrag (*current, newPointerState.withPositionOffset (unboundedMouseOffset), time);
if (isUnboundedMouseModeOn)
handleUnboundedDrag (*current);
}
else
{
sendMouseMove (*current, newPointerState, time);
}
}
revealCursor (false);
}
}
//==============================================================================
void handleEvent (ComponentPeer& newPeer, Point<float> positionWithinPeer, Time time,
const ModifierKeys newMods, float newPressure, float newOrientation, PenDetails pen)
{
lastTime = time;
++mouseEventCounter;
const auto pointerState = detail::PointerState().withPosition (newPeer.localToGlobal (positionWithinPeer))
.withPressure (newPressure)
.withOrientation (newOrientation)
.withRotation (MouseInputSource::defaultRotation)
.withTiltX (pen.tiltX)
.withTiltY (pen.tiltY);
if (isDragging() && newMods.isAnyMouseButtonDown())
{
setPointerState (pointerState, time, false);
}
else
{
setPeer (newPeer, pointerState, time);
if (auto* peer = getPeer())
{
if (setButtons (pointerState, time, newMods))
return; // some modal events have been dispatched, so the current event is now out-of-date
peer = getPeer();
if (peer != nullptr)
setPointerState (pointerState, time, false);
}
}
}
Component* getTargetForGesture (ComponentPeer& peer, Point<float> positionWithinPeer,
Time time, Point<float>& screenPos)
{
lastTime = time;
++mouseEventCounter;
screenPos = peer.localToGlobal (positionWithinPeer);
const auto pointerState = lastPointerState.withPosition (screenPos);
setPeer (peer, pointerState, time);
setPointerState (pointerState, time, false);
triggerFakeMove();
return getComponentUnderMouse();
}
void handleWheel (ComponentPeer& peer, Point<float> positionWithinPeer,
Time time, const MouseWheelDetails& wheel)
{
Desktop::getInstance().incrementMouseWheelCounter();
Point<float> screenPos;
// This will make sure that when the wheel spins in its inertial phase, any events
// continue to be sent to the last component that the mouse was over when it was being
// actively controlled by the user. This avoids confusion when scrolling through nested
// scrollable components.
if (lastNonInertialWheelTarget == nullptr || ! wheel.isInertial)
lastNonInertialWheelTarget = getTargetForGesture (peer, positionWithinPeer, time, screenPos);
else
screenPos = peer.localToGlobal (positionWithinPeer);
if (auto target = lastNonInertialWheelTarget.get())
sendMouseWheel (*target, screenPos, time, wheel);
}
void handleMagnifyGesture (ComponentPeer& peer, Point<float> positionWithinPeer,
Time time, const float scaleFactor)
{
Point<float> screenPos;
if (auto* current = getTargetForGesture (peer, positionWithinPeer, time, screenPos))
sendMagnifyGesture (*current, screenPos, time, scaleFactor);
}
//==============================================================================
Time getLastMouseDownTime() const noexcept
{
return mouseDowns[0].time;
}
Point<float> getLastMouseDownPosition() const noexcept
{
return SH::unscaledScreenPosToScaled (mouseDowns[0].position);
}
int getNumberOfMultipleClicks() const noexcept
{
int numClicks = 1;
if (! isLongPressOrDrag())
{
for (int i = 1; i < numElementsInArray (mouseDowns); ++i)
{
if (mouseDowns[0].canBePartOfMultipleClickWith (mouseDowns[i], MouseEvent::getDoubleClickTimeout() * jmin (i, 2)))
++numClicks;
else
break;
}
}
return numClicks;
}
bool isLongPressOrDrag() const noexcept
{
return movedSignificantly ||
lastTime > (mouseDowns[0].time + RelativeTime::milliseconds (300));
}
bool hasMovedSignificantlySincePressed() const noexcept
{
return movedSignificantly;
}
// Deprecated method
bool hasMouseMovedSignificantlySincePressed() const noexcept
{
return isLongPressOrDrag();
}
//==============================================================================
void triggerFakeMove()
{
triggerAsyncUpdate();
}
void handleAsyncUpdate() override
{
setPointerState (lastPointerState,
jmax (lastTime, Time::getCurrentTime()), true);
}
//==============================================================================
void enableUnboundedMouseMovement (bool enable, bool keepCursorVisibleUntilOffscreen)
{
enable = enable && isDragging();
isCursorVisibleUntilOffscreen = keepCursorVisibleUntilOffscreen;
if (enable != isUnboundedMouseModeOn)
{
if ((! enable) && ((! isCursorVisibleUntilOffscreen) || ! unboundedMouseOffset.isOrigin()))
{
// when released, return the mouse to within the component's bounds
if (auto* current = getComponentUnderMouse())
setScreenPosition (current->getScreenBounds().toFloat()
.getConstrainedPoint (SH::unscaledScreenPosToScaled (lastPointerState.position)));
}
isUnboundedMouseModeOn = enable;
unboundedMouseOffset = {};
revealCursor (true);
}
}
void handleUnboundedDrag (Component& current)
{
auto componentScreenBounds = SH::scaledScreenPosToUnscaled (current.getParentMonitorArea()
.reduced (2, 2)
.toFloat());
if (! componentScreenBounds.contains (lastPointerState.position))
{
auto componentCentre = current.getScreenBounds().toFloat().getCentre();
unboundedMouseOffset += (lastPointerState.position - SH::scaledScreenPosToUnscaled (componentCentre));
setScreenPosition (componentCentre);
}
else if (isCursorVisibleUntilOffscreen
&& (! unboundedMouseOffset.isOrigin())
&& componentScreenBounds.contains (lastPointerState.position + unboundedMouseOffset))
{
MouseInputSource::setRawMousePosition (lastPointerState.position + unboundedMouseOffset);
unboundedMouseOffset = {};
}
}
//==============================================================================
void showMouseCursor (MouseCursor cursor, bool forcedUpdate)
{
if (isUnboundedMouseModeOn && ((! unboundedMouseOffset.isOrigin()) || ! isCursorVisibleUntilOffscreen))
{
cursor = MouseCursor::NoCursor;
forcedUpdate = true;
}
if (forcedUpdate || cursor.getHandle() != currentCursorHandle)
{
currentCursorHandle = cursor.getHandle();
cursor.showInWindow (getPeer());
}
}
void hideCursor()
{
showMouseCursor (MouseCursor::NoCursor, true);
}
void revealCursor (bool forcedUpdate)
{
MouseCursor mc (MouseCursor::NormalCursor);
if (auto* current = getComponentUnderMouse())
mc = current->getLookAndFeel().getMouseCursorFor (*current);
showMouseCursor (mc, forcedUpdate);
}
//==============================================================================
const int index;
const MouseInputSource::InputSourceType inputType;
Point<float> unboundedMouseOffset; // NB: these are unscaled coords
detail::PointerState lastPointerState;
ModifierKeys buttonState;
bool isUnboundedMouseModeOn = false, isCursorVisibleUntilOffscreen = false;
private:
WeakReference<Component> componentUnderMouse, lastNonInertialWheelTarget;
ComponentPeer* lastPeer = nullptr;
void* currentCursorHandle = nullptr;
int mouseEventCounter = 0;
struct RecentMouseDown
{
RecentMouseDown() = default;
Point<float> position;
Time time;
ModifierKeys buttons;
uint32 peerID = 0;
bool isTouch = false;
bool canBePartOfMultipleClickWith (const RecentMouseDown& other, int maxTimeBetweenMs) const noexcept
{
return time - other.time < RelativeTime::milliseconds (maxTimeBetweenMs)
&& std::abs (position.x - other.position.x) < (float) getPositionToleranceForInputType()
&& std::abs (position.y - other.position.y) < (float) getPositionToleranceForInputType()
&& buttons == other.buttons
&& peerID == other.peerID;
}
int getPositionToleranceForInputType() const noexcept { return isTouch ? 25 : 8; }
};
RecentMouseDown mouseDowns[4];
Time lastTime;
bool movedSignificantly = false;
void registerMouseDown (Point<float> screenPos, Time time, Component& component,
const ModifierKeys modifiers, bool isTouchSource) noexcept
{
for (int i = numElementsInArray (mouseDowns); --i > 0;)
mouseDowns[i] = mouseDowns[i - 1];
mouseDowns[0].position = screenPos;
mouseDowns[0].time = time;
mouseDowns[0].buttons = modifiers.withOnlyMouseButtons();
mouseDowns[0].isTouch = isTouchSource;
if (auto* peer = component.getPeer())
mouseDowns[0].peerID = peer->getUniqueID();
else
mouseDowns[0].peerID = 0;
movedSignificantly = false;
lastNonInertialWheelTarget = nullptr;
}
void registerMouseDrag (Point<float> screenPos) noexcept
{
movedSignificantly = movedSignificantly || mouseDowns[0].position.getDistanceFrom (screenPos) >= 4;
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MouseInputSourceImpl)
};
} // namespace juce::detail
@@ -0,0 +1,154 @@
/*
==============================================================================
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
{
class MouseInputSourceList : public Timer
{
public:
MouseInputSourceList()
{
#if JUCE_ANDROID || JUCE_IOS
auto mainMouseInputType = MouseInputSource::InputSourceType::touch;
#else
auto mainMouseInputType = MouseInputSource::InputSourceType::mouse;
#endif
addSource (0, mainMouseInputType);
}
MouseInputSource* addSource (int index, MouseInputSource::InputSourceType type)
{
auto* s = new MouseInputSourceImpl (index, type);
sources.add (s);
sourceArray.add (MouseInputSource (s));
return &sourceArray.getReference (sourceArray.size() - 1);
}
MouseInputSource* getMouseSource (int index) noexcept
{
return isPositiveAndBelow (index, sourceArray.size()) ? &sourceArray.getReference (index)
: nullptr;
}
MouseInputSource* getOrCreateMouseInputSource (MouseInputSource::InputSourceType type, int touchIndex = 0)
{
if (type == MouseInputSource::InputSourceType::mouse
|| type == MouseInputSource::InputSourceType::pen)
{
for (auto& m : sourceArray)
if (type == m.getType())
return &m;
addSource (0, type);
}
else if (type == MouseInputSource::InputSourceType::touch)
{
jassert (0 <= touchIndex && touchIndex < 100); // sanity-check on number of fingers
for (auto& m : sourceArray)
if (type == m.getType() && touchIndex == m.getIndex())
return &m;
if (canUseTouch())
return addSource (touchIndex, type);
}
return nullptr;
}
int getNumDraggingMouseSources() const noexcept
{
int num = 0;
for (auto* s : sources)
if (s->isDragging())
++num;
return num;
}
MouseInputSource* getDraggingMouseSource (int index) noexcept
{
int num = 0;
for (auto& s : sourceArray)
{
if (s.isDragging())
{
if (index == num)
return &s;
++num;
}
}
return nullptr;
}
void beginDragAutoRepeat (int interval)
{
if (interval > 0)
{
if (getTimerInterval() != interval)
startTimer (interval);
}
else
{
stopTimer();
}
}
void timerCallback() override
{
bool anyDragging = false;
for (auto* s : sources)
{
// NB: when doing auto-repeat, we need to force an update of the current position and button state,
// because on some OSes the queue can get overloaded with messages so that mouse-events don't get through..
if (s->isDragging() && ComponentPeer::getCurrentModifiersRealtime().isAnyMouseButtonDown())
{
s->lastPointerState.position = s->getRawScreenPosition();
s->triggerFakeMove();
anyDragging = true;
}
}
if (! anyDragging)
stopTimer();
}
OwnedArray<MouseInputSourceImpl> sources;
Array<MouseInputSource> sourceArray;
private:
bool addSource();
bool canUseTouch() const;
};
} // namespace juce::detail
@@ -0,0 +1,104 @@
/*
==============================================================================
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
{
class PointerState
{
auto tie() const noexcept
{
return std::tie (position, pressure, orientation, rotation, tiltX, tiltY);
}
public:
PointerState() = default;
bool operator== (const PointerState& other) const noexcept { return tie() == other.tie(); }
bool operator!= (const PointerState& other) const noexcept { return tie() != other.tie(); }
[[nodiscard]] PointerState withPositionOffset (Point<float> x) const noexcept { return with (&PointerState::position, position + x); }
[[nodiscard]] PointerState withPosition (Point<float> x) const noexcept { return with (&PointerState::position, x); }
[[nodiscard]] PointerState withPressure (float x) const noexcept { return with (&PointerState::pressure, x); }
[[nodiscard]] PointerState withOrientation (float x) const noexcept { return with (&PointerState::orientation, x); }
[[nodiscard]] PointerState withRotation (float x) const noexcept { return with (&PointerState::rotation, x); }
[[nodiscard]] PointerState withTiltX (float x) const noexcept { return with (&PointerState::tiltX, x); }
[[nodiscard]] PointerState withTiltY (float x) const noexcept { return with (&PointerState::tiltY, x); }
Point<float> position;
float pressure = MouseInputSource::defaultPressure;
float orientation = MouseInputSource::defaultOrientation;
float rotation = MouseInputSource::defaultRotation;
float tiltX = MouseInputSource::defaultTiltX;
float tiltY = MouseInputSource::defaultTiltY;
bool isPressureValid() const noexcept { return 0.0f <= pressure && pressure <= 1.0f; }
bool isOrientationValid() const noexcept { return 0.0f <= orientation && orientation <= MathConstants<float>::twoPi; }
bool isRotationValid() const noexcept { return 0.0f <= rotation && rotation <= MathConstants<float>::twoPi; }
bool isTiltValid (bool isX) const noexcept
{
return isX ? (-1.0f <= tiltX && tiltX <= 1.0f)
: (-1.0f <= tiltY && tiltY <= 1.0f);
}
private:
template <typename Value>
PointerState with (Value PointerState::* member, Value item) const
{
auto copy = *this;
copy.*member = std::move (item);
return copy;
}
};
inline auto makeMouseEvent (MouseInputSource source,
const PointerState& ps,
ModifierKeys modifiers,
Component* eventComponent,
Component* originator,
Time eventTime,
Point<float> mouseDownPos,
Time mouseDownTime,
int numberOfClicks,
bool mouseWasDragged)
{
return MouseEvent (source,
ps.position,
modifiers,
ps.pressure,
ps.orientation,
ps.rotation,
ps.tiltX,
ps.tiltY,
eventComponent,
originator,
eventTime,
mouseDownPos,
mouseDownTime,
numberOfClicks,
mouseWasDragged);
}
} // namespace juce::detail
@@ -0,0 +1,123 @@
/*
==============================================================================
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
{
struct ScalingHelpers
{
template <typename PointOrRect>
static PointOrRect unscaledScreenPosToScaled (float scale, PointOrRect pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? pos / scale : pos;
}
template <typename PointOrRect>
static PointOrRect scaledScreenPosToUnscaled (float scale, PointOrRect pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? pos * scale : pos;
}
// For these, we need to avoid getSmallestIntegerContainer being used, which causes
// judder when moving windows
static Rectangle<int> unscaledScreenPosToScaled (float scale, Rectangle<int> pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? Rectangle<int> (roundToInt ((float) pos.getX() / scale),
roundToInt ((float) pos.getY() / scale),
roundToInt ((float) pos.getWidth() / scale),
roundToInt ((float) pos.getHeight() / scale)) : pos;
}
static Rectangle<int> scaledScreenPosToUnscaled (float scale, Rectangle<int> pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? Rectangle<int> (roundToInt ((float) pos.getX() * scale),
roundToInt ((float) pos.getY() * scale),
roundToInt ((float) pos.getWidth() * scale),
roundToInt ((float) pos.getHeight() * scale)) : pos;
}
static Rectangle<float> unscaledScreenPosToScaled (float scale, Rectangle<float> pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? Rectangle<float> (pos.getX() / scale,
pos.getY() / scale,
pos.getWidth() / scale,
pos.getHeight() / scale) : pos;
}
static Rectangle<float> scaledScreenPosToUnscaled (float scale, Rectangle<float> pos) noexcept
{
return ! approximatelyEqual (scale, 1.0f) ? Rectangle<float> (pos.getX() * scale,
pos.getY() * scale,
pos.getWidth() * scale,
pos.getHeight() * scale) : pos;
}
template <typename PointOrRect>
static PointOrRect unscaledScreenPosToScaled (PointOrRect pos) noexcept
{
return unscaledScreenPosToScaled (Desktop::getInstance().getGlobalScaleFactor(), pos);
}
template <typename PointOrRect>
static PointOrRect scaledScreenPosToUnscaled (PointOrRect pos) noexcept
{
return scaledScreenPosToUnscaled (Desktop::getInstance().getGlobalScaleFactor(), pos);
}
template <typename PointOrRect>
static PointOrRect unscaledScreenPosToScaled (const Component& comp, PointOrRect pos) noexcept
{
return unscaledScreenPosToScaled (comp.getDesktopScaleFactor(), pos);
}
template <typename PointOrRect>
static PointOrRect scaledScreenPosToUnscaled (const Component& comp, PointOrRect pos) noexcept
{
return scaledScreenPosToUnscaled (comp.getDesktopScaleFactor(), pos);
}
static Point<int> addPosition (Point<int> p, const Component& c) noexcept { return p + c.getPosition(); }
static Rectangle<int> addPosition (Rectangle<int> p, const Component& c) noexcept { return p + c.getPosition(); }
static Point<float> addPosition (Point<float> p, const Component& c) noexcept { return p + c.getPosition().toFloat(); }
static Rectangle<float> addPosition (Rectangle<float> p, const Component& c) noexcept { return p + c.getPosition().toFloat(); }
static Point<int> subtractPosition (Point<int> p, const Component& c) noexcept { return p - c.getPosition(); }
static Rectangle<int> subtractPosition (Rectangle<int> p, const Component& c) noexcept { return p - c.getPosition(); }
static Point<float> subtractPosition (Point<float> p, const Component& c) noexcept { return p - c.getPosition().toFloat(); }
static Rectangle<float> subtractPosition (Rectangle<float> p, const Component& c) noexcept { return p - c.getPosition().toFloat(); }
static Point<float> screenPosToLocalPos (Component& comp, Point<float> pos)
{
if (auto* peer = comp.getPeer())
{
pos = peer->globalToLocal (pos);
auto& peerComp = peer->getComponent();
return comp.getLocalPoint (&peerComp, unscaledScreenPosToScaled (peerComp, pos));
}
return comp.getLocalPoint (nullptr, unscaledScreenPosToScaled (comp, pos));
}
};
} // namespace juce::detail
@@ -0,0 +1,98 @@
/*
==============================================================================
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
{
class ConcreteScopedContentSharerImpl : public ScopedMessageBoxImpl,
private AsyncUpdater
{
public:
static ScopedMessageBox show (std::unique_ptr<ScopedContentSharerInterface>&& native,
ContentSharer::Callback callback)
{
return ScopedMessageBox (runAsync (std::move (native), std::move (callback)));
}
~ConcreteScopedContentSharerImpl() override
{
cancelPendingUpdate();
}
void close() override
{
cancelPendingUpdate();
nativeImplementation->close();
self.reset();
}
private:
static std::shared_ptr<ConcreteScopedContentSharerImpl> runAsync (std::unique_ptr<ScopedContentSharerInterface>&& p,
ContentSharer::Callback&& c)
{
std::shared_ptr<ConcreteScopedContentSharerImpl> result (new ConcreteScopedContentSharerImpl (std::move (p), std::move (c)));
result->self = result;
result->triggerAsyncUpdate();
return result;
}
ConcreteScopedContentSharerImpl (std::unique_ptr<ScopedContentSharerInterface>&& p,
ContentSharer::Callback&& c)
: callback (std::move (c)), nativeImplementation (std::move (p)) {}
void handleAsyncUpdate() override
{
nativeImplementation->runAsync ([weakRecipient = std::weak_ptr<ConcreteScopedContentSharerImpl> (self)] (bool result, const String& error)
{
const auto notifyRecipient = [result, error, weakRecipient]
{
if (const auto locked = weakRecipient.lock())
{
NullCheckedInvocation::invoke (locked->callback, result, error);
locked->self.reset();
}
};
if (MessageManager::getInstance()->isThisTheMessageThread())
notifyRecipient();
else
MessageManager::callAsync (notifyRecipient);
});
}
ContentSharer::Callback callback;
std::unique_ptr<ScopedContentSharerInterface> nativeImplementation;
/* The 'old' native message box API doesn't have a concept of content sharer owners.
Instead, content sharers have to clean up after themselves, once they're done displaying.
To allow this mode of usage, the implementation keeps an owning reference to itself,
which is cleared once the content sharer is closed or asked to quit. To display a content
sharer box without a scoped lifetime, just create a Pimpl instance without using
the ScopedContentSharer wrapper, and the Pimpl will destroy itself after it is dismissed.
*/
std::shared_ptr<ConcreteScopedContentSharerImpl> self;
};
} // namespace juce::detail
@@ -0,0 +1,215 @@
/*
==============================================================================
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
{
/*
Instances of this type can show and dismiss a content sharer.
This is an interface rather than a concrete type so that platforms can pick an implementation at
runtime if necessary.
*/
struct ScopedContentSharerInterface
{
virtual ~ScopedContentSharerInterface() = default;
/* Shows the content sharer.
When the content sharer exits normally, it should send the result to the passed-in function.
The passed-in function is safe to call from any thread at any time.
*/
virtual void runAsync (ContentSharer::Callback callback)
{
jassertfalse;
NullCheckedInvocation::invoke (callback, false, "Content sharing not available on this platform!");
}
/* Forcefully closes the content sharer.
This will be called when the content sharer handle has fallen out of scope.
If the content sharer has already been closed by the user, this shouldn't do anything.
*/
virtual void close() {}
/* Implemented differently for each platform. */
static std::unique_ptr<ScopedContentSharerInterface> shareFiles (const Array<URL>&, Component*);
static std::unique_ptr<ScopedContentSharerInterface> shareText (const String&, Component*);
/* Implemented below. */
static std::unique_ptr<ScopedContentSharerInterface> shareImages (const Array<Image>&, std::unique_ptr<ImageFileFormat>, Component*);
static std::unique_ptr<ScopedContentSharerInterface> shareData (MemoryBlock, Component*);
};
class TemporaryFilesDecorator : public ScopedContentSharerInterface,
private AsyncUpdater
{
public:
explicit TemporaryFilesDecorator (Component* parentIn)
: parent (parentIn) {}
void runAsync (ContentSharer::Callback cb) override
{
callback = std::move (cb);
task = std::async (std::launch::async, [this]
{
std::tie (temporaryFiles, error) = prepareTemporaryFiles();
triggerAsyncUpdate();
});
}
void close() override
{
if (inner != nullptr)
inner->close();
}
private:
virtual std::tuple<Array<URL>, String> prepareTemporaryFiles() const = 0;
void handleAsyncUpdate() override
{
if (error.isNotEmpty())
{
NullCheckedInvocation::invoke (callback, false, error);
return;
}
inner = shareFiles (temporaryFiles, parent);
if (inner == nullptr)
{
NullCheckedInvocation::invoke (callback, false, TRANS ("Failed to create file sharer"));
return;
}
inner->runAsync (callback);
}
Array<URL> temporaryFiles;
String error;
std::unique_ptr<ScopedContentSharerInterface> inner;
ContentSharer::Callback callback;
std::future<void> task;
Component* parent = nullptr;
};
std::unique_ptr<ScopedContentSharerInterface> ScopedContentSharerInterface::shareImages (const Array<Image>& images,
std::unique_ptr<ImageFileFormat> format,
Component* parent)
{
class Decorator : public TemporaryFilesDecorator
{
public:
Decorator (Array<Image> imagesIn, std::unique_ptr<ImageFileFormat> formatIn, Component* parentIn)
: TemporaryFilesDecorator (parentIn), images (std::move (imagesIn)), format (std::move (formatIn)) {}
private:
std::tuple<Array<URL>, String> prepareTemporaryFiles() const override
{
const auto extension = format->getFormatName().toLowerCase();
Array<URL> result;
for (const auto& image : images)
{
File tempFile = File::createTempFile (extension);
if (! tempFile.create().wasOk())
return { Array<URL>{}, TRANS ("Failed to create temporary file") };
std::unique_ptr<FileOutputStream> outputStream (tempFile.createOutputStream());
if (outputStream == nullptr)
return { Array<URL>{}, TRANS ("Failed to open temporary file for writing") };
if (format->writeImageToStream (image, *outputStream))
result.add (URL (tempFile));
}
jassert (std::all_of (result.begin(),
result.end(),
[] (const auto& url)
{
return url.isLocalFile() && url.getLocalFile().existsAsFile();
}));
return { std::move (result), String{} };
}
Array<Image> images;
std::unique_ptr<ImageFileFormat> format;
};
return std::make_unique<Decorator> (images,
format == nullptr ? std::make_unique<PNGImageFormat>() : std::move (format),
parent);
}
std::unique_ptr<ScopedContentSharerInterface> ScopedContentSharerInterface::shareData (MemoryBlock mb, Component* parent)
{
class Decorator : public TemporaryFilesDecorator
{
public:
Decorator (MemoryBlock mbIn, Component* parentIn)
: TemporaryFilesDecorator (parentIn), mb (std::move (mbIn)) {}
private:
std::tuple<Array<URL>, String> prepareTemporaryFiles() const override
{
File tempFile = File::createTempFile ("data");
if (! tempFile.create().wasOk())
return { Array<URL>{}, TRANS ("Failed to create temporary file") };
std::unique_ptr<FileOutputStream> outputStream (tempFile.createOutputStream());
if (outputStream == nullptr)
return { Array<URL>{}, TRANS ("Failed to open temporary file for writing") };
size_t pos = 0;
size_t totalSize = mb.getSize();
while (pos < totalSize)
{
size_t numToWrite = std::min ((size_t) 8192, totalSize - pos);
if (! outputStream->write (mb.begin() + pos, numToWrite))
return { Array<URL>{}, TRANS ("Failed to write to temporary file") };
pos += numToWrite;
}
return { Array<URL> { URL (tempFile) }, String{} };
}
MemoryBlock mb;
};
return std::make_unique<Decorator> (std::move (mb), parent);
}
} // 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
{
//==============================================================================
class ScopedMessageBoxImpl
{
public:
virtual ~ScopedMessageBoxImpl() = default;
virtual void close() = 0;
};
//==============================================================================
class ConcreteScopedMessageBoxImpl : public ScopedMessageBoxImpl,
private AsyncUpdater
{
public:
static ScopedMessageBox show (std::unique_ptr<ScopedMessageBoxInterface>&& native,
std::function<void (int)> callback)
{
return ScopedMessageBox (runAsync (std::move (native),
rawToUniquePtr (ModalCallbackFunction::create (std::move (callback)))));
}
static int showUnmanaged (std::unique_ptr<ScopedMessageBoxInterface>&& native,
ModalComponentManager::Callback* cb)
{
#if JUCE_MODAL_LOOPS_PERMITTED
if (cb == nullptr)
return runSync (std::move (native));
#endif
runAsync (std::move (native), rawToUniquePtr (cb));
return 0;
}
~ConcreteScopedMessageBoxImpl() override
{
cancelPendingUpdate();
}
void close() override
{
cancelPendingUpdate();
nativeImplementation->close();
self.reset();
}
private:
static std::shared_ptr<ConcreteScopedMessageBoxImpl> runAsync (std::unique_ptr<ScopedMessageBoxInterface>&& p,
std::unique_ptr<ModalComponentManager::Callback>&& c)
{
std::shared_ptr<ConcreteScopedMessageBoxImpl> result (new ConcreteScopedMessageBoxImpl (std::move (p), std::move (c)));
result->self = result;
result->triggerAsyncUpdate();
return result;
}
static int runSync (std::unique_ptr<ScopedMessageBoxInterface>&& p)
{
auto local = std::move (p);
return local != nullptr ? local->runSync() : 0;
}
explicit ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p)
: ConcreteScopedMessageBoxImpl (std::move (p), nullptr) {}
ConcreteScopedMessageBoxImpl (std::unique_ptr<ScopedMessageBoxInterface>&& p,
std::unique_ptr<ModalComponentManager::Callback>&& c)
: callback (std::move (c)), nativeImplementation (std::move (p)) {}
void handleAsyncUpdate() override
{
nativeImplementation->runAsync ([weakRecipient = std::weak_ptr<ConcreteScopedMessageBoxImpl> (self)] (int result)
{
const auto notifyRecipient = [result, weakRecipient]
{
if (const auto locked = weakRecipient.lock())
{
if (auto* cb = locked->callback.get())
cb->modalStateFinished (result);
locked->self.reset();
}
};
if (MessageManager::getInstance()->isThisTheMessageThread())
notifyRecipient();
else
MessageManager::callAsync (notifyRecipient);
});
}
std::unique_ptr<ModalComponentManager::Callback> callback;
std::unique_ptr<ScopedMessageBoxInterface> nativeImplementation;
/* The 'old' native message box API doesn't have a concept of message box owners.
Instead, message boxes have to clean up after themselves, once they're done displaying.
To allow this mode of usage, the implementation keeps an owning reference to itself,
which is cleared once the message box is closed or asked to quit. To display a native
message box without a scoped lifetime, just create a Pimpl instance without using
the ScopedMessageBox wrapper, and the Pimpl will destroy itself after it is dismissed.
*/
std::shared_ptr<ConcreteScopedMessageBoxImpl> self;
};
} // namespace juce::detail
@@ -0,0 +1,60 @@
/*
==============================================================================
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
{
/*
Instances of this type can show and dismiss a message box.
This is an interface rather than a concrete type so that platforms can pick an implementation at
runtime if necessary.
*/
struct ScopedMessageBoxInterface
{
virtual ~ScopedMessageBoxInterface() = default;
/* Shows the message box.
When the message box exits normally, it should send the result to the passed-in function.
The passed-in function is safe to call from any thread at any time.
*/
virtual void runAsync (std::function<void (int)>) = 0;
/* Shows the message box and blocks. */
virtual int runSync() = 0;
/* Forcefully closes the message box.
This will be called when the message box handle has fallen out of scope.
If the message box has already been closed by the user, this shouldn't do anything.
*/
virtual void close() = 0;
/* Implemented differently for each platform. */
static std::unique_ptr<ScopedMessageBoxInterface> create (const MessageBoxOptions& options);
};
} // namespace juce::detail
@@ -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::detail
{
class ToolbarItemDragAndDropOverlayComponent : public Component
{
public:
ToolbarItemDragAndDropOverlayComponent()
: isDragging (false)
{
setAlwaysOnTop (true);
setRepaintsOnMouseActivity (true);
setMouseCursor (MouseCursor::DraggingHandCursor);
}
void paint (Graphics& g) override
{
if (ToolbarItemComponent* const tc = getToolbarItemComponent())
{
if (isMouseOverOrDragging()
&& tc->getEditingMode() == ToolbarItemComponent::editableOnToolbar)
{
g.setColour (findColour (Toolbar::editingModeOutlineColourId, true));
g.drawRect (getLocalBounds(), jmin (2, (getWidth() - 1) / 2,
(getHeight() - 1) / 2));
}
}
}
void mouseDown (const MouseEvent& e) override
{
isDragging = false;
if (ToolbarItemComponent* const tc = getToolbarItemComponent())
{
tc->dragOffsetX = e.x;
tc->dragOffsetY = e.y;
}
}
void mouseDrag (const MouseEvent& e) override
{
if (e.mouseWasDraggedSinceMouseDown() && ! isDragging)
{
isDragging = true;
if (DragAndDropContainer* const dnd = DragAndDropContainer::findParentDragContainerFor (this))
{
dnd->startDragging (Toolbar::toolbarDragDescriptor, getParentComponent(), ScaledImage(), true, nullptr, &e.source);
if (ToolbarItemComponent* const tc = getToolbarItemComponent())
{
tc->isBeingDragged = true;
if (tc->getEditingMode() == ToolbarItemComponent::editableOnToolbar)
tc->setVisible (false);
}
}
}
}
void mouseUp (const MouseEvent&) override
{
isDragging = false;
if (ToolbarItemComponent* const tc = getToolbarItemComponent())
{
tc->isBeingDragged = false;
if (Toolbar* const tb = tc->getToolbar())
tb->updateAllItemPositions (true);
else if (tc->getEditingMode() == ToolbarItemComponent::editableOnToolbar)
delete tc;
}
}
void parentSizeChanged() override
{
setBounds (0, 0, getParentWidth(), getParentHeight());
}
private:
//==============================================================================
bool isDragging;
ToolbarItemComponent* getToolbarItemComponent() const noexcept
{
return dynamic_cast<ToolbarItemComponent*> (getParentComponent());
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToolbarItemDragAndDropOverlayComponent)
};
} // namespace juce::detail
@@ -0,0 +1,136 @@
/*
==============================================================================
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
{
/* Keeps track of the active top level window. */
class TopLevelWindowManager : private Timer,
private DeletedAtShutdown
{
public:
TopLevelWindowManager() = default;
~TopLevelWindowManager() override
{
clearSingletonInstance();
}
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (TopLevelWindowManager)
static void checkCurrentlyFocusedTopLevelWindow()
{
if (auto* wm = TopLevelWindowManager::getInstanceWithoutCreating())
wm->checkFocusAsync();
}
void checkFocusAsync()
{
startTimer (10);
}
void checkFocus()
{
startTimer (jmin (1731, getTimerInterval() * 2));
auto* newActive = findCurrentlyActiveWindow();
if (newActive != currentActive)
{
currentActive = newActive;
for (int i = windows.size(); --i >= 0;)
if (auto* tlw = windows[i])
tlw->setWindowActive (isWindowActive (tlw));
Desktop::getInstance().triggerFocusCallback();
}
}
bool addWindow (TopLevelWindow* const w)
{
windows.add (w);
checkFocusAsync();
return isWindowActive (w);
}
void removeWindow (TopLevelWindow* const w)
{
checkFocusAsync();
if (currentActive == w)
currentActive = nullptr;
windows.removeFirstMatchingValue (w);
if (windows.isEmpty())
deleteInstance();
}
Array<TopLevelWindow*> windows;
private:
TopLevelWindow* currentActive = nullptr;
void timerCallback() override
{
checkFocus();
}
bool isWindowActive (TopLevelWindow* const tlw) const
{
return (tlw == currentActive
|| tlw->isParentOf (currentActive)
|| tlw->hasKeyboardFocus (true))
&& tlw->isShowing();
}
TopLevelWindow* findCurrentlyActiveWindow() const
{
if (Process::isForegroundProcess())
{
auto* focusedComp = Component::getCurrentlyFocusedComponent();
auto* w = dynamic_cast<TopLevelWindow*> (focusedComp);
if (w == nullptr && focusedComp != nullptr)
w = focusedComp->findParentComponentOfClass<TopLevelWindow>();
if (w == nullptr)
w = currentActive;
if (w != nullptr && w->isShowing())
return w;
}
return nullptr;
}
JUCE_DECLARE_NON_COPYABLE (TopLevelWindowManager)
};
JUCE_IMPLEMENT_SINGLETON (TopLevelWindowManager)
} // namespace juce::detail
@@ -0,0 +1,49 @@
/*
==============================================================================
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
{
struct ViewportHelpers
{
ViewportHelpers() = delete;
static bool wouldScrollOnEvent (const Viewport* vp, const MouseInputSource& src)
{
if (vp != nullptr)
{
switch (vp->getScrollOnDragMode())
{
case Viewport::ScrollOnDragMode::all: return true;
case Viewport::ScrollOnDragMode::nonHover: return ! src.canHover();
case Viewport::ScrollOnDragMode::never: return false;
}
}
return false;
}
};
} // namespace juce::detail
@@ -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::detail
{
struct WindowingHelpers
{
WindowingHelpers() = delete;
static Image createIconForFile (const File& file);
#if JUCE_WINDOWS
static bool isEmbeddedInForegroundProcess (Component* c);
static bool isWindowOnCurrentVirtualDesktop (void*);
#else
static bool isEmbeddedInForegroundProcess (Component*) { return false; }
static bool isWindowOnCurrentVirtualDesktop (void*) { return true; }
#endif
/* Returns true if this process is in the foreground, or if the viewComponent
is embedded into a window owned by the foreground process.
*/
static bool isForegroundOrEmbeddedProcess (Component* viewComponent)
{
return Process::isForegroundProcess() || isEmbeddedInForegroundProcess (viewComponent);
}
template <typename Value>
static BorderSize<int> roundToInt (BorderSize<Value> border)
{
return { ::juce::roundToInt (border.getTop()),
::juce::roundToInt (border.getLeft()),
::juce::roundToInt (border.getBottom()),
::juce::roundToInt (border.getRight()) };
}
};
} // namespace juce::detail
@@ -0,0 +1,219 @@
/*
==============================================================================
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
{
Drawable::Drawable()
{
setInterceptsMouseClicks (false, false);
setPaintingIsUnclipped (true);
setAccessible (false);
}
Drawable::Drawable (const Drawable& other)
: Component (other.getName())
{
setInterceptsMouseClicks (false, false);
setPaintingIsUnclipped (true);
setAccessible (false);
setComponentID (other.getComponentID());
setTransform (other.getTransform());
if (auto* clipPath = other.drawableClipPath.get())
setClipPath (clipPath->createCopy());
}
Drawable::~Drawable()
{
}
void Drawable::applyDrawableClipPath (Graphics& g)
{
if (drawableClipPath != nullptr)
{
auto clipPath = drawableClipPath->getOutlineAsPath();
if (! clipPath.isEmpty())
g.getInternalContext().clipToPath (clipPath, {});
}
}
//==============================================================================
void Drawable::draw (Graphics& g, float opacity, const AffineTransform& transform) const
{
const_cast<Drawable*> (this)->nonConstDraw (g, opacity, transform);
}
void Drawable::nonConstDraw (Graphics& g, float opacity, const AffineTransform& transform)
{
Graphics::ScopedSaveState ss (g);
g.addTransform (AffineTransform::translation ((float) -(originRelativeToComponent.x),
(float) -(originRelativeToComponent.y))
.followedBy (getTransform())
.followedBy (transform));
applyDrawableClipPath (g);
if (! g.isClipEmpty())
{
if (opacity < 1.0f)
{
g.beginTransparencyLayer (opacity);
paintEntireComponent (g, true);
g.endTransparencyLayer();
}
else
{
paintEntireComponent (g, true);
}
}
}
void Drawable::drawAt (Graphics& g, float x, float y, float opacity) const
{
draw (g, opacity, AffineTransform::translation (x, y));
}
void Drawable::drawWithin (Graphics& g, Rectangle<float> destArea,
RectanglePlacement placement, float opacity) const
{
draw (g, opacity, placement.getTransformToFit (getDrawableBounds(), destArea));
}
//==============================================================================
DrawableComposite* Drawable::getParent() const
{
return dynamic_cast<DrawableComposite*> (getParentComponent());
}
void Drawable::setClipPath (std::unique_ptr<Drawable> clipPath)
{
if (drawableClipPath != clipPath)
{
drawableClipPath = std::move (clipPath);
repaint();
}
}
void Drawable::transformContextToCorrectOrigin (Graphics& g)
{
g.setOrigin (originRelativeToComponent);
}
void Drawable::parentHierarchyChanged()
{
setBoundsToEnclose (getDrawableBounds());
}
void Drawable::setBoundsToEnclose (Rectangle<float> area)
{
Point<int> parentOrigin;
if (auto* parent = getParent())
parentOrigin = parent->originRelativeToComponent;
const auto smallestIntegerContainer = area.getSmallestIntegerContainer();
auto newBounds = smallestIntegerContainer + parentOrigin;
originRelativeToComponent = -smallestIntegerContainer.getPosition();
setBounds (newBounds);
}
//==============================================================================
bool Drawable::replaceColour (Colour original, Colour replacement)
{
bool changed = false;
for (auto* c : getChildren())
if (auto* d = dynamic_cast<Drawable*> (c))
changed = d->replaceColour (original, replacement) || changed;
return changed;
}
void Drawable::setDrawableTransform (const AffineTransform& transform)
{
drawableTransform = transform;
updateTransform();
}
void Drawable::updateTransform()
{
if (drawableTransform.isIdentity())
return;
const auto transformationOrigin = originRelativeToComponent + getPosition();
setTransform (AffineTransform::translation (transformationOrigin * (-1))
.followedBy (drawableTransform)
.followedBy (AffineTransform::translation (transformationOrigin)));
}
//==============================================================================
void Drawable::setOriginWithOriginalSize (Point<float> originWithinParent)
{
setTransform (AffineTransform::translation (originWithinParent.x, originWithinParent.y));
}
void Drawable::setTransformToFit (const Rectangle<float>& area, RectanglePlacement placement)
{
if (! area.isEmpty())
setTransform (placement.getTransformToFit (getDrawableBounds(), area));
}
//==============================================================================
std::unique_ptr<Drawable> Drawable::createFromImageData (const void* data, const size_t numBytes)
{
auto image = ImageFileFormat::loadFrom (data, numBytes);
if (image.isValid())
return std::make_unique<DrawableImage> (image);
if (auto svg = parseXMLIfTagMatches (String::createStringFromData (data, (int) numBytes), "svg"))
return Drawable::createFromSVG (*svg);
return {};
}
std::unique_ptr<Drawable> Drawable::createFromImageDataStream (InputStream& dataSource)
{
MemoryOutputStream mo;
mo << dataSource;
return createFromImageData (mo.getData(), mo.getDataSize());
}
std::unique_ptr<Drawable> Drawable::createFromImageFile (const File& file)
{
FileInputStream fin (file);
if (fin.openedOk())
return createFromImageDataStream (fin);
return {};
}
} // namespace juce
@@ -0,0 +1,262 @@
/*
==============================================================================
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
{
#ifndef DOXYGEN
namespace detail
{
class BoundsChangeListener final : private ComponentListener
{
public:
BoundsChangeListener (Component& c, std::function<void()> cb)
: callback (std::move (cb)),
componentListenerGuard { [comp = &c, this] { comp->removeComponentListener (this); } }
{
jassert (callback != nullptr);
c.addComponentListener (this);
}
private:
void componentMovedOrResized (Component&, bool, bool) override
{
callback();
}
std::function<void()> callback;
ErasedScopeGuard componentListenerGuard;
};
} // namespace detail
#endif
//==============================================================================
/**
The base class for objects which can draw themselves, e.g. polygons, images, etc.
@see DrawableComposite, DrawableImage, DrawablePath, DrawableText
@tags{GUI}
*/
class JUCE_API Drawable : public Component
{
protected:
//==============================================================================
/** The base class can't be instantiated directly.
@see DrawableComposite, DrawableImage, DrawablePath, DrawableText
*/
Drawable();
public:
/** Destructor. */
~Drawable() override;
//==============================================================================
/** Creates a deep copy of this Drawable object.
Use this to create a new copy of this and any sub-objects in the tree.
*/
virtual std::unique_ptr<Drawable> createCopy() const = 0;
/** Creates a path that describes the outline of this drawable. */
virtual Path getOutlineAsPath() const = 0;
//==============================================================================
/** Renders this Drawable object.
Note that the preferred way to render a drawable in future is by using it
as a component and adding it to a parent, so you might want to consider that
before using this method.
@see drawWithin
*/
void draw (Graphics& g, float opacity,
const AffineTransform& transform = AffineTransform()) const;
/** Renders the Drawable at a given offset within the Graphics context.
The coordinates passed-in are used to translate the object relative to its own
origin before drawing it - this is basically a quick way of saying:
@code
draw (g, AffineTransform::translation (x, y)).
@endcode
Note that the preferred way to render a drawable in future is by using it
as a component and adding it to a parent, so you might want to consider that
before using this method.
*/
void drawAt (Graphics& g, float x, float y, float opacity) const;
/** Renders the Drawable within a rectangle, scaling it to fit neatly inside without
changing its aspect-ratio.
The object can placed arbitrarily within the rectangle based on a Justification type,
and can either be made as big as possible, or just reduced to fit.
Note that the preferred way to render a drawable in future is by using it
as a component and adding it to a parent, so you might want to consider that
before using this method.
@param g the graphics context to render onto
@param destArea the target rectangle to fit the drawable into
@param placement defines the alignment and rescaling to use to fit
this object within the target rectangle.
@param opacity the opacity to use, in the range 0 to 1.0
*/
void drawWithin (Graphics& g,
Rectangle<float> destArea,
RectanglePlacement placement,
float opacity) const;
//==============================================================================
/** Resets any transformations on this drawable, and positions its origin within
its parent component.
*/
void setOriginWithOriginalSize (Point<float> originWithinParent);
/** Sets a transform for this drawable that will position it within the specified
area of its parent component.
*/
void setTransformToFit (const Rectangle<float>& areaInParent, RectanglePlacement placement);
/** Returns the DrawableComposite that contains this object, if there is one. */
DrawableComposite* getParent() const;
/** Sets a the clipping region of this drawable using another drawable.
The drawable passed in will be deleted when no longer needed.
*/
void setClipPath (std::unique_ptr<Drawable> drawableClipPath);
//==============================================================================
/** Tries to turn some kind of image file into a drawable.
The data could be an image that the ImageFileFormat class understands, or it
could be SVG.
*/
static std::unique_ptr<Drawable> createFromImageData (const void* data, size_t numBytes);
/** Tries to turn a stream containing some kind of image data into a drawable.
The data could be an image that the ImageFileFormat class understands, or it
could be SVG.
*/
static std::unique_ptr<Drawable> createFromImageDataStream (InputStream& dataSource);
/** Tries to turn a file containing some kind of image data into a drawable.
The data could be an image that the ImageFileFormat class understands, or it
could be SVG.
*/
static std::unique_ptr<Drawable> createFromImageFile (const File& file);
/** Attempts to parse an SVG (Scalable Vector Graphics) document, and to turn this
into a Drawable tree.
If something goes wrong while parsing, it may return nullptr.
SVG is a pretty large and complex spec, and this doesn't aim to be a full
implementation, but it can return the basic vector objects.
*/
static std::unique_ptr<Drawable> createFromSVG (const XmlElement& svgDocument);
/** Attempts to parse an SVG (Scalable Vector Graphics) document from a file,
and to turn this into a Drawable tree.
If something goes wrong while parsing, it may return nullptr.
SVG is a pretty large and complex spec, and this doesn't aim to be a full
implementation, but it can return the basic vector objects.
Any references to references to external image files will be relative to
the parent directory of the file passed.
*/
static std::unique_ptr<Drawable> createFromSVGFile (const File& svgFile);
/** Parses an SVG path string and returns it. */
static Path parseSVGPath (const String& svgPath);
//==============================================================================
/** Returns the area that this drawable covers.
The result is expressed in this drawable's own coordinate space, and does not take
into account any transforms that may be applied to the component.
*/
virtual Rectangle<float> getDrawableBounds() const = 0;
/** Recursively replaces a colour that might be used for filling or stroking.
return true if any instances of this colour were found.
*/
virtual bool replaceColour (Colour originalColour, Colour replacementColour);
/** Sets a transformation that applies to the same coordinate system in which the rest of the
draw calls are made. You almost certainly want to call this function when working with
Drawables as opposed to Component::setTransform().
The reason for this is that the origin of a Drawable is not the same as the point returned
by Component::getPosition() but has an additional offset internal to the Drawable class.
Using setDrawableTransform() will take this internal offset into account when applying the
transform to the Component base.
You can only use Drawable::setDrawableTransform() or Component::setTransform() for a given
object. Using both will lead to unpredictable behaviour.
*/
void setDrawableTransform (const AffineTransform& transform);
protected:
//==============================================================================
friend class DrawableComposite;
friend class DrawableShape;
/** @internal */
void transformContextToCorrectOrigin (Graphics&);
/** @internal */
void parentHierarchyChanged() override;
/** @internal */
void setBoundsToEnclose (Rectangle<float>);
/** @internal */
void applyDrawableClipPath (Graphics&);
Point<int> originRelativeToComponent;
std::unique_ptr<Drawable> drawableClipPath;
AffineTransform drawableTransform;
void nonConstDraw (Graphics&, float opacity, const AffineTransform&);
Drawable (const Drawable&);
Drawable& operator= (const Drawable&);
JUCE_LEAK_DETECTOR (Drawable)
private:
void updateTransform();
detail::BoundsChangeListener boundsChangeListener { *this, [this] { updateTransform(); } };
};
} // namespace juce
@@ -0,0 +1,164 @@
/*
==============================================================================
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
{
DrawableComposite::DrawableComposite()
: bounds ({ 0.0f, 0.0f, 100.0f, 100.0f })
{
setContentArea ({ 0.0f, 0.0f, 100.0f, 100.0f });
}
DrawableComposite::DrawableComposite (const DrawableComposite& other)
: Drawable (other),
bounds (other.bounds),
contentArea (other.contentArea)
{
for (auto* c : other.getChildren())
if (auto* d = dynamic_cast<const Drawable*> (c))
addAndMakeVisible (d->createCopy().release());
}
DrawableComposite::~DrawableComposite()
{
deleteAllChildren();
}
std::unique_ptr<Drawable> DrawableComposite::createCopy() const
{
return std::make_unique<DrawableComposite> (*this);
}
//==============================================================================
Rectangle<float> DrawableComposite::getDrawableBounds() const
{
Rectangle<float> r;
for (auto* c : getChildren())
if (auto* d = dynamic_cast<const Drawable*> (c))
r = r.getUnion (d->isTransformed() ? d->getDrawableBounds().transformedBy (d->getTransform())
: d->getDrawableBounds());
return r;
}
void DrawableComposite::setContentArea (Rectangle<float> newArea)
{
contentArea = newArea;
}
void DrawableComposite::setBoundingBox (Rectangle<float> newBounds)
{
setBoundingBox (Parallelogram<float> (newBounds));
}
void DrawableComposite::setBoundingBox (Parallelogram<float> newBounds)
{
if (bounds != newBounds)
{
bounds = newBounds;
auto t = AffineTransform::fromTargetPoints (contentArea.getTopLeft(), bounds.topLeft,
contentArea.getTopRight(), bounds.topRight,
contentArea.getBottomLeft(), bounds.bottomLeft);
if (t.isSingularity())
t = {};
setTransform (t);
}
}
void DrawableComposite::resetBoundingBoxToContentArea()
{
setBoundingBox (contentArea);
}
void DrawableComposite::resetContentAreaAndBoundingBoxToFitChildren()
{
setContentArea (getDrawableBounds());
resetBoundingBoxToContentArea();
}
void DrawableComposite::parentHierarchyChanged()
{
if (auto* parent = getParent())
originRelativeToComponent = parent->originRelativeToComponent - getPosition();
}
void DrawableComposite::childBoundsChanged (Component*)
{
updateBoundsToFitChildren();
}
void DrawableComposite::childrenChanged()
{
updateBoundsToFitChildren();
}
void DrawableComposite::updateBoundsToFitChildren()
{
if (! updateBoundsReentrant)
{
const ScopedValueSetter<bool> setter (updateBoundsReentrant, true, false);
Rectangle<int> childArea;
for (auto* c : getChildren())
childArea = childArea.getUnion (c->getBoundsInParent());
auto delta = childArea.getPosition();
childArea += getPosition();
if (childArea != getBounds())
{
if (! delta.isOrigin())
{
originRelativeToComponent -= delta;
for (auto* c : getChildren())
c->setBounds (c->getBounds() - delta);
}
setBounds (childArea);
}
}
}
//==============================================================================
Path DrawableComposite::getOutlineAsPath() const
{
Path p;
for (auto* c : getChildren())
if (auto* d = dynamic_cast<Drawable*> (c))
p.addPath (d->getOutlineAsPath());
p.applyTransform (getTransform());
return p;
}
} // namespace juce
@@ -0,0 +1,117 @@
/*
==============================================================================
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 drawable object which acts as a container for a set of other Drawables.
Note that although this is a Component, it takes ownership of its child components
and will delete them, so that you can use it as a self-contained graphic object.
The intention is that you should not add your own components to it, only add other
Drawable objects.
@see Drawable
@tags{GUI}
*/
class JUCE_API DrawableComposite : public Drawable
{
public:
//==============================================================================
/** Creates a composite Drawable. */
DrawableComposite();
/** Creates a copy of a DrawableComposite. */
DrawableComposite (const DrawableComposite&);
/** Destructor. */
~DrawableComposite() override;
//==============================================================================
/** Sets the parallelogram that defines the target position of the content rectangle when the drawable is rendered.
@see setContentArea
*/
void setBoundingBox (Parallelogram<float> newBoundingBox);
/** Sets the rectangle that defines the target position of the content rectangle when the drawable is rendered.
@see setContentArea
*/
void setBoundingBox (Rectangle<float> newBoundingBox);
/** Returns the parallelogram that defines the target position of the content rectangle when the drawable is rendered.
@see setBoundingBox
*/
Parallelogram<float> getBoundingBox() const noexcept { return bounds; }
/** Changes the bounding box transform to match the content area, so that any sub-items will
be drawn at their untransformed positions.
*/
void resetBoundingBoxToContentArea();
/** Returns the main content rectangle.
@see contentLeftMarkerName, contentRightMarkerName, contentTopMarkerName, contentBottomMarkerName
*/
Rectangle<float> getContentArea() const noexcept { return contentArea; }
/** Changes the main content area.
@see setBoundingBox, contentLeftMarkerName, contentRightMarkerName, contentTopMarkerName, contentBottomMarkerName
*/
void setContentArea (Rectangle<float> newArea);
/** Resets the content area and the bounding transform to fit around the area occupied
by the child components.
*/
void resetContentAreaAndBoundingBoxToFitChildren();
//==============================================================================
/** @internal */
std::unique_ptr<Drawable> createCopy() const override;
/** @internal */
Rectangle<float> getDrawableBounds() const override;
/** @internal */
void childBoundsChanged (Component*) override;
/** @internal */
void childrenChanged() override;
/** @internal */
void parentHierarchyChanged() override;
/** @internal */
Path getOutlineAsPath() const override;
private:
//==============================================================================
Parallelogram<float> bounds;
Rectangle<float> contentArea;
bool updateBoundsReentrant = false;
void updateBoundsToFitChildren();
DrawableComposite& operator= (const DrawableComposite&);
JUCE_LEAK_DETECTOR (DrawableComposite)
};
} // namespace juce
@@ -0,0 +1,156 @@
/*
==============================================================================
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
{
DrawableImage::DrawableImage() : bounds ({ 0.0f, 0.0f, 1.0f, 1.0f })
{
}
DrawableImage::DrawableImage (const DrawableImage& other)
: Drawable (other),
image (other.image),
opacity (other.opacity),
overlayColour (other.overlayColour),
bounds (other.bounds)
{
setBounds (other.getBounds());
}
DrawableImage::DrawableImage (const Image& imageToUse)
{
setImageInternal (imageToUse);
}
DrawableImage::~DrawableImage()
{
}
std::unique_ptr<Drawable> DrawableImage::createCopy() const
{
return std::make_unique<DrawableImage> (*this);
}
//==============================================================================
void DrawableImage::setImage (const Image& imageToUse)
{
if (setImageInternal (imageToUse))
repaint();
}
void DrawableImage::setOpacity (const float newOpacity)
{
opacity = newOpacity;
}
void DrawableImage::setOverlayColour (Colour newOverlayColour)
{
overlayColour = newOverlayColour;
}
void DrawableImage::setBoundingBox (Rectangle<float> newBounds)
{
setBoundingBox (Parallelogram<float> (newBounds));
}
void DrawableImage::setBoundingBox (Parallelogram<float> newBounds)
{
if (bounds != newBounds)
{
bounds = newBounds;
if (image.isValid())
{
auto tr = bounds.topLeft + (bounds.topRight - bounds.topLeft) / (float) image.getWidth();
auto bl = bounds.topLeft + (bounds.bottomLeft - bounds.topLeft) / (float) image.getHeight();
auto t = AffineTransform::fromTargetPoints (bounds.topLeft.x, bounds.topLeft.y,
tr.x, tr.y,
bl.x, bl.y);
if (t.isSingularity())
t = {};
setTransform (t);
}
}
}
//==============================================================================
void DrawableImage::paint (Graphics& g)
{
if (image.isValid())
{
if (opacity > 0.0f && ! overlayColour.isOpaque())
{
g.setOpacity (opacity);
g.drawImageAt (image, 0, 0, false);
}
if (! overlayColour.isTransparent())
{
g.setColour (overlayColour.withMultipliedAlpha (opacity));
g.drawImageAt (image, 0, 0, true);
}
}
}
Rectangle<float> DrawableImage::getDrawableBounds() const
{
return image.getBounds().toFloat();
}
bool DrawableImage::hitTest (int x, int y)
{
return Drawable::hitTest (x, y) && image.isValid() && image.getPixelAt (x, y).getAlpha() >= 127;
}
Path DrawableImage::getOutlineAsPath() const
{
return {}; // not applicable for images
}
//==============================================================================
bool DrawableImage::setImageInternal (const Image& imageToUse)
{
if (image != imageToUse)
{
image = imageToUse;
setBounds (image.getBounds());
setBoundingBox (image.getBounds().toFloat());
return true;
}
return false;
}
//==============================================================================
std::unique_ptr<AccessibilityHandler> DrawableImage::createAccessibilityHandler()
{
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::image);
}
} // namespace juce
@@ -0,0 +1,117 @@
/*
==============================================================================
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 drawable object which is a bitmap image.
@see Drawable
@tags{GUI}
*/
class JUCE_API DrawableImage : public Drawable
{
public:
//==============================================================================
DrawableImage();
DrawableImage (const DrawableImage&);
/** Sets the image that this drawable will render. */
explicit DrawableImage (const Image& imageToUse);
/** Destructor. */
~DrawableImage() override;
//==============================================================================
/** Sets the image that this drawable will render. */
void setImage (const Image& imageToUse);
/** Returns the current image. */
const Image& getImage() const noexcept { return image; }
/** Sets the opacity to use when drawing the image. */
void setOpacity (float newOpacity);
/** Returns the image's opacity. */
float getOpacity() const noexcept { return opacity; }
/** Sets a colour to draw over the image's alpha channel.
By default this is transparent so isn't drawn, but if you set a non-transparent
colour here, then it will be overlaid on the image, using the image's alpha
channel as a mask.
This is handy for doing things like darkening or lightening an image by overlaying
it with semi-transparent black or white.
*/
void setOverlayColour (Colour newOverlayColour);
/** Returns the overlay colour. */
Colour getOverlayColour() const noexcept { return overlayColour; }
/** Sets the bounding box within which the image should be displayed. */
void setBoundingBox (Parallelogram<float> newBounds);
/** Sets the bounding box within which the image should be displayed. */
void setBoundingBox (Rectangle<float> newBounds);
/** Returns the position to which the image's top-left corner should be remapped in the target
coordinate space when rendering this object.
@see setTransform
*/
Parallelogram<float> getBoundingBox() const noexcept { return bounds; }
//==============================================================================
/** @internal */
void paint (Graphics&) override;
/** @internal */
bool hitTest (int x, int y) override;
/** @internal */
std::unique_ptr<Drawable> createCopy() const override;
/** @internal */
Rectangle<float> getDrawableBounds() const override;
/** @internal */
Path getOutlineAsPath() const override;
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
private:
//==============================================================================
bool setImageInternal (const Image&);
//==============================================================================
Image image;
float opacity = 1.0f;
Colour overlayColour { 0 };
Parallelogram<float> bounds;
DrawableImage& operator= (const DrawableImage&);
JUCE_LEAK_DETECTOR (DrawableImage)
};
} // namespace juce
@@ -0,0 +1,57 @@
/*
==============================================================================
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
{
DrawablePath::DrawablePath() {}
DrawablePath::~DrawablePath() {}
DrawablePath::DrawablePath (const DrawablePath& other) : DrawableShape (other)
{
setPath (other.path);
}
std::unique_ptr<Drawable> DrawablePath::createCopy() const
{
return std::make_unique<DrawablePath> (*this);
}
void DrawablePath::setPath (const Path& newPath)
{
path = newPath;
pathChanged();
}
void DrawablePath::setPath (Path&& newPath)
{
path = std::move (newPath);
pathChanged();
}
const Path& DrawablePath::getPath() const { return path; }
const Path& DrawablePath::getStrokePath() const { return strokePath; }
} // namespace juce
@@ -0,0 +1,77 @@
/*
==============================================================================
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 drawable object which renders a filled or outlined shape.
For details on how to change the fill and stroke, see the DrawableShape class.
@see Drawable, DrawableShape
@tags{GUI}
*/
class JUCE_API DrawablePath : public DrawableShape
{
public:
//==============================================================================
/** Creates a DrawablePath. */
DrawablePath();
DrawablePath (const DrawablePath&);
/** Destructor. */
~DrawablePath() override;
//==============================================================================
/** Changes the path that will be drawn.
@see setFill, setStrokeType
*/
void setPath (const Path& newPath);
/** Changes the path that will be drawn.
@see setFill, setStrokeType
*/
void setPath (Path&& newPath);
/** Returns the current path. */
const Path& getPath() const;
/** Returns the current path for the outline. */
const Path& getStrokePath() const;
//==============================================================================
/** @internal */
std::unique_ptr<Drawable> createCopy() const override;
private:
//==============================================================================
DrawablePath& operator= (const DrawablePath&);
JUCE_LEAK_DETECTOR (DrawablePath)
};
} // namespace juce
@@ -0,0 +1,87 @@
/*
==============================================================================
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
{
DrawableRectangle::DrawableRectangle() {}
DrawableRectangle::~DrawableRectangle() {}
DrawableRectangle::DrawableRectangle (const DrawableRectangle& other)
: DrawableShape (other),
bounds (other.bounds),
cornerSize (other.cornerSize)
{
rebuildPath();
}
std::unique_ptr<Drawable> DrawableRectangle::createCopy() const
{
return std::make_unique<DrawableRectangle> (*this);
}
//==============================================================================
void DrawableRectangle::setRectangle (Parallelogram<float> newBounds)
{
if (bounds != newBounds)
{
bounds = newBounds;
rebuildPath();
}
}
void DrawableRectangle::setCornerSize (Point<float> newSize)
{
if (cornerSize != newSize)
{
cornerSize = newSize;
rebuildPath();
}
}
void DrawableRectangle::rebuildPath()
{
auto w = bounds.getWidth();
auto h = bounds.getHeight();
Path newPath;
if (cornerSize.x > 0 && cornerSize.y > 0)
newPath.addRoundedRectangle (0, 0, w, h, cornerSize.x, cornerSize.y);
else
newPath.addRectangle (0, 0, w, h);
newPath.applyTransform (AffineTransform::fromTargetPoints (Point<float>(), bounds.topLeft,
Point<float> (w, 0), bounds.topRight,
Point<float> (0, h), bounds.bottomLeft));
if (path != newPath)
{
path.swapWithPath (newPath);
pathChanged();
}
}
} // namespace juce
@@ -0,0 +1,76 @@
/*
==============================================================================
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 Drawable object which draws a rectangle.
For details on how to change the fill and stroke, see the DrawableShape class.
@see Drawable, DrawableShape
@tags{GUI}
*/
class JUCE_API DrawableRectangle : public DrawableShape
{
public:
//==============================================================================
DrawableRectangle();
DrawableRectangle (const DrawableRectangle&);
/** Destructor. */
~DrawableRectangle() override;
//==============================================================================
/** Sets the rectangle's bounds. */
void setRectangle (Parallelogram<float> newBounds);
/** Returns the rectangle's bounds. */
Parallelogram<float> getRectangle() const noexcept { return bounds; }
/** Returns the corner size to be used. */
Point<float> getCornerSize() const noexcept { return cornerSize; }
/** Sets a new corner size for the rectangle */
void setCornerSize (Point<float> newSize);
//==============================================================================
/** @internal */
std::unique_ptr<Drawable> createCopy() const override;
private:
Parallelogram<float> bounds;
Point<float> cornerSize;
void rebuildPath();
DrawableRectangle& operator= (const DrawableRectangle&);
JUCE_LEAK_DETECTOR (DrawableRectangle)
};
} // namespace juce
@@ -0,0 +1,181 @@
/*
==============================================================================
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
{
DrawableShape::DrawableShape()
: strokeType (0.0f),
mainFill (Colours::black),
strokeFill (Colours::black)
{
}
DrawableShape::DrawableShape (const DrawableShape& other)
: Drawable (other),
strokeType (other.strokeType),
dashLengths (other.dashLengths),
mainFill (other.mainFill),
strokeFill (other.strokeFill)
{
}
DrawableShape::~DrawableShape()
{
}
//==============================================================================
void DrawableShape::setFill (const FillType& newFill)
{
if (mainFill != newFill)
{
mainFill = newFill;
repaint();
}
}
void DrawableShape::setStrokeFill (const FillType& newFill)
{
if (strokeFill != newFill)
{
strokeFill = newFill;
repaint();
}
}
void DrawableShape::setStrokeType (const PathStrokeType& newStrokeType)
{
if (strokeType != newStrokeType)
{
strokeType = newStrokeType;
strokeChanged();
}
}
void DrawableShape::setDashLengths (const Array<float>& newDashLengths)
{
if (dashLengths != newDashLengths)
{
dashLengths = newDashLengths;
strokeChanged();
}
}
void DrawableShape::setStrokeThickness (const float newThickness)
{
setStrokeType (PathStrokeType (newThickness, strokeType.getJointStyle(), strokeType.getEndStyle()));
}
bool DrawableShape::isStrokeVisible() const noexcept
{
return strokeType.getStrokeThickness() > 0.0f && ! strokeFill.isInvisible();
}
//==============================================================================
void DrawableShape::paint (Graphics& g)
{
transformContextToCorrectOrigin (g);
applyDrawableClipPath (g);
g.setFillType (mainFill);
g.fillPath (path);
if (isStrokeVisible())
{
g.setFillType (strokeFill);
g.fillPath (strokePath);
}
}
void DrawableShape::pathChanged()
{
strokeChanged();
}
void DrawableShape::strokeChanged()
{
strokePath.clear();
const float extraAccuracy = 4.0f;
if (dashLengths.isEmpty())
strokeType.createStrokedPath (strokePath, path, AffineTransform(), extraAccuracy);
else
strokeType.createDashedStroke (strokePath, path, dashLengths.getRawDataPointer(),
dashLengths.size(), AffineTransform(), extraAccuracy);
setBoundsToEnclose (getDrawableBounds());
repaint();
}
Rectangle<float> DrawableShape::getDrawableBounds() const
{
if (isStrokeVisible())
return strokePath.getBounds();
return path.getBounds();
}
bool DrawableShape::hitTest (int x, int y)
{
bool allowsClicksOnThisComponent, allowsClicksOnChildComponents;
getInterceptsMouseClicks (allowsClicksOnThisComponent, allowsClicksOnChildComponents);
if (! allowsClicksOnThisComponent)
return false;
auto globalX = (float) (x - originRelativeToComponent.x);
auto globalY = (float) (y - originRelativeToComponent.y);
return path.contains (globalX, globalY)
|| (isStrokeVisible() && strokePath.contains (globalX, globalY));
}
//==============================================================================
static bool replaceColourInFill (FillType& fill, Colour original, Colour replacement)
{
if (fill.colour == original && fill.isColour())
{
fill = FillType (replacement);
return true;
}
return false;
}
bool DrawableShape::replaceColour (Colour original, Colour replacement)
{
bool changed1 = replaceColourInFill (mainFill, original, replacement);
bool changed2 = replaceColourInFill (strokeFill, original, replacement);
return changed1 || changed2;
}
Path DrawableShape::getOutlineAsPath() const
{
auto outline = isStrokeVisible() ? strokePath : path;
outline.applyTransform (getTransform());
return outline;
}
} // namespace juce
@@ -0,0 +1,126 @@
/*
==============================================================================
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 base class implementing common functionality for Drawable classes which
consist of some kind of filled and stroked outline.
@see DrawablePath, DrawableRectangle
@tags{GUI}
*/
class JUCE_API DrawableShape : public Drawable
{
protected:
//==============================================================================
DrawableShape();
DrawableShape (const DrawableShape&);
public:
/** Destructor. */
~DrawableShape() override;
//==============================================================================
/** Sets a fill type for the path.
This colour is used to fill the path - if you don't want the path to be
filled (e.g. if you're just drawing an outline), set this to a transparent
colour.
@see setPath, setStrokeFill
*/
void setFill (const FillType& newFill);
/** Returns the current fill type.
@see setFill
*/
const FillType& getFill() const noexcept { return mainFill; }
/** Sets the fill type with which the outline will be drawn.
@see setFill
*/
void setStrokeFill (const FillType& newStrokeFill);
/** Returns the current stroke fill.
@see setStrokeFill
*/
const FillType& getStrokeFill() const noexcept { return strokeFill; }
/** Changes the properties of the outline that will be drawn around the path.
If the stroke has 0 thickness, no stroke will be drawn.
@see setStrokeThickness, setStrokeColour
*/
void setStrokeType (const PathStrokeType& newStrokeType);
/** Changes the stroke thickness.
This is a shortcut for calling setStrokeType.
*/
void setStrokeThickness (float newThickness);
/** Returns the current outline style. */
const PathStrokeType& getStrokeType() const noexcept { return strokeType; }
/** Provides a set of dash lengths to use for stroking the path. */
void setDashLengths (const Array<float>& newDashLengths);
/** Returns the set of dash lengths that the path is using. */
const Array<float>& getDashLengths() const noexcept { return dashLengths; }
//==============================================================================
/** @internal */
Rectangle<float> getDrawableBounds() const override;
/** @internal */
void paint (Graphics&) override;
/** @internal */
bool hitTest (int x, int y) override;
/** @internal */
bool replaceColour (Colour originalColour, Colour replacementColour) override;
/** @internal */
Path getOutlineAsPath() const override;
protected:
//==============================================================================
/** Called when the cached path should be updated. */
void pathChanged();
/** Called when the cached stroke should be updated. */
void strokeChanged();
/** True if there's a stroke with a non-zero thickness and non-transparent colour. */
bool isStrokeVisible() const noexcept;
//==============================================================================
PathStrokeType strokeType;
Array<float> dashLengths;
Path path, strokePath;
private:
FillType mainFill, strokeFill;
DrawableShape& operator= (const DrawableShape&);
};
} // namespace juce
@@ -0,0 +1,232 @@
/*
==============================================================================
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
{
DrawableText::DrawableText()
: colour (Colours::black),
justification (Justification::centredLeft)
{
setBoundingBox (Parallelogram<float> ({ 0.0f, 0.0f, 50.0f, 20.0f }));
setFont (Font (15.0f), true);
}
DrawableText::DrawableText (const DrawableText& other)
: Drawable (other),
bounds (other.bounds),
fontHeight (other.fontHeight),
fontHScale (other.fontHScale),
font (other.font),
text (other.text),
colour (other.colour),
justification (other.justification)
{
refreshBounds();
}
DrawableText::~DrawableText()
{
}
std::unique_ptr<Drawable> DrawableText::createCopy() const
{
return std::make_unique<DrawableText> (*this);
}
//==============================================================================
void DrawableText::setText (const String& newText)
{
if (text != newText)
{
text = newText;
refreshBounds();
}
}
void DrawableText::setColour (Colour newColour)
{
if (colour != newColour)
{
colour = newColour;
repaint();
}
}
void DrawableText::setFont (const Font& newFont, bool applySizeAndScale)
{
if (font != newFont)
{
font = newFont;
if (applySizeAndScale)
{
fontHeight = font.getHeight();
fontHScale = font.getHorizontalScale();
}
refreshBounds();
}
}
void DrawableText::setJustification (Justification newJustification)
{
justification = newJustification;
repaint();
}
void DrawableText::setBoundingBox (Parallelogram<float> newBounds)
{
if (bounds != newBounds)
{
bounds = newBounds;
refreshBounds();
}
}
void DrawableText::setFontHeight (float newHeight)
{
if (! approximatelyEqual (fontHeight, newHeight))
{
fontHeight = newHeight;
refreshBounds();
}
}
void DrawableText::setFontHorizontalScale (float newScale)
{
if (! approximatelyEqual (fontHScale, newScale))
{
fontHScale = newScale;
refreshBounds();
}
}
void DrawableText::refreshBounds()
{
auto w = bounds.getWidth();
auto h = bounds.getHeight();
auto height = jlimit (0.01f, jmax (0.01f, h), fontHeight);
auto hscale = jlimit (0.01f, jmax (0.01f, w), fontHScale);
scaledFont = font;
scaledFont.setHeight (height);
scaledFont.setHorizontalScale (hscale);
setBoundsToEnclose (getDrawableBounds());
repaint();
}
//==============================================================================
Rectangle<int> DrawableText::getTextArea (float w, float h) const
{
return Rectangle<float> (w, h).getSmallestIntegerContainer();
}
AffineTransform DrawableText::getTextTransform (float w, float h) const
{
return AffineTransform::fromTargetPoints (Point<float>(), bounds.topLeft,
Point<float> (w, 0), bounds.topRight,
Point<float> (0, h), bounds.bottomLeft);
}
void DrawableText::paint (Graphics& g)
{
transformContextToCorrectOrigin (g);
auto w = bounds.getWidth();
auto h = bounds.getHeight();
g.addTransform (getTextTransform (w, h));
g.setFont (scaledFont);
g.setColour (colour);
g.drawFittedText (text, getTextArea (w, h), justification, 0x100000);
}
Rectangle<float> DrawableText::getDrawableBounds() const
{
return bounds.getBoundingBox();
}
Path DrawableText::getOutlineAsPath() const
{
auto w = bounds.getWidth();
auto h = bounds.getHeight();
auto area = getTextArea (w, h).toFloat();
GlyphArrangement arr;
arr.addFittedText (scaledFont, text,
area.getX(), area.getY(),
area.getWidth(), area.getHeight(),
justification,
0x100000);
Path pathOfAllGlyphs;
for (auto& glyph : arr)
{
Path gylphPath;
glyph.createPath (gylphPath);
pathOfAllGlyphs.addPath (gylphPath);
}
pathOfAllGlyphs.applyTransform (getTextTransform (w, h).followedBy (drawableTransform));
return pathOfAllGlyphs;
}
bool DrawableText::replaceColour (Colour originalColour, Colour replacementColour)
{
if (colour != originalColour)
return false;
setColour (replacementColour);
return true;
}
//==============================================================================
std::unique_ptr<AccessibilityHandler> DrawableText::createAccessibilityHandler()
{
class DrawableTextAccessibilityHandler final : public AccessibilityHandler
{
public:
DrawableTextAccessibilityHandler (DrawableText& drawableTextToWrap)
: AccessibilityHandler (drawableTextToWrap, AccessibilityRole::staticText),
drawableText (drawableTextToWrap)
{
}
String getTitle() const override { return drawableText.getText(); }
private:
DrawableText& drawableText;
};
return std::make_unique<DrawableTextAccessibilityHandler> (*this);
}
} // namespace juce
@@ -0,0 +1,121 @@
/*
==============================================================================
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 drawable object which renders a line of text.
@see Drawable
@tags{GUI}
*/
class JUCE_API DrawableText : public Drawable
{
public:
//==============================================================================
/** Creates a DrawableText object. */
DrawableText();
DrawableText (const DrawableText&);
/** Destructor. */
~DrawableText() override;
//==============================================================================
/** Sets the text to display.*/
void setText (const String& newText);
/** Returns the currently displayed text */
const String& getText() const noexcept { return text;}
/** Sets the colour of the text. */
void setColour (Colour newColour);
/** Returns the current text colour. */
Colour getColour() const noexcept { return colour; }
/** Sets the font to use.
Note that the font height and horizontal scale are set using setFontHeight() and
setFontHorizontalScale(). If applySizeAndScale is true, then these height
and scale values will be changed to match the dimensions of the font supplied;
if it is false, then the new font object's height and scale are ignored.
*/
void setFont (const Font& newFont, bool applySizeAndScale);
/** Returns the current font. */
const Font& getFont() const noexcept { return font; }
/** Changes the justification of the text within the bounding box. */
void setJustification (Justification newJustification);
/** Returns the current justification. */
Justification getJustification() const noexcept { return justification; }
/** Returns the parallelogram that defines the text bounding box. */
Parallelogram<float> getBoundingBox() const noexcept { return bounds; }
/** Sets the bounding box that contains the text. */
void setBoundingBox (Parallelogram<float> newBounds);
float getFontHeight() const noexcept { return fontHeight; }
void setFontHeight (float newHeight);
float getFontHorizontalScale() const noexcept { return fontHScale; }
void setFontHorizontalScale (float newScale);
//==============================================================================
/** @internal */
void paint (Graphics&) override;
/** @internal */
std::unique_ptr<Drawable> createCopy() const override;
/** @internal */
Rectangle<float> getDrawableBounds() const override;
/** @internal */
Path getOutlineAsPath() const override;
/** @internal */
bool replaceColour (Colour originalColour, Colour replacementColour) override;
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
private:
//==============================================================================
Parallelogram<float> bounds;
float fontHeight, fontHScale;
Font font, scaledFont;
String text;
Colour colour;
Justification justification;
void refreshBounds();
Rectangle<int> getTextArea (float width, float height) const;
AffineTransform getTextTransform (float width, float height) const;
DrawableText& operator= (const DrawableText&);
JUCE_LEAK_DETECTOR (DrawableText)
};
} // namespace juce
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
/*
==============================================================================
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
{
ScopedMessageBox ContentSharer::shareFilesScoped (const Array<URL>& files,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareFiles (files, parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
ScopedMessageBox ContentSharer::shareTextScoped (const String& text,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareText (text, parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
ScopedMessageBox ContentSharer::shareImagesScoped (const Array<Image>& images,
std::unique_ptr<ImageFileFormat> format,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareImages (images, std::move (format), parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
ScopedMessageBox ContentSharer::shareDataScoped (const MemoryBlock& mb,
Callback callback,
Component* parent)
{
auto impl = detail::ScopedContentSharerInterface::shareData (mb, parent);
return detail::ConcreteScopedContentSharerImpl::show (std::move (impl), std::move (callback));
}
#if ! (JUCE_CONTENT_SHARING && (JUCE_IOS || JUCE_ANDROID))
auto detail::ScopedContentSharerInterface::shareFiles (const Array<URL>&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
return std::make_unique<detail::ScopedContentSharerInterface>();
}
auto detail::ScopedContentSharerInterface::shareText (const String&, Component*) -> std::unique_ptr<ScopedContentSharerInterface>
{
return std::make_unique<detail::ScopedContentSharerInterface>();
}
#endif
} // namespace juce
@@ -0,0 +1,145 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Functions that allow sharing content between apps and devices.
You can share text, images, files or an arbitrary data block.
@tags{GUI}
*/
class JUCE_API ContentSharer
{
public:
ContentSharer() = delete;
/** A callback of this type is passed when starting a content sharing
session.
When the session ends, the function will receive a flag indicating
whether the session was successful. In the case of failure, the
errorText argument may hold a string describing the problem.
*/
using Callback = std::function<void (bool success, const String& errorText)>;
/** Shares the given files. Each URL should be either a full file path
or it should point to a resource within the application bundle. For
resources on iOS it should be something like "content/image.png" if you
want to specify a file from application bundle located in "content"
directory. On Android you should specify only a filename, without an
extension.
Upon completion you will receive a callback with a sharing result. Note:
Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android.
@param files the files to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/
[[nodiscard]] static ScopedMessageBox shareFilesScoped (const Array<URL>& files,
Callback callback,
Component* parent = nullptr);
/** Shares the given text.
Upon completion you will receive a callback with a sharing result. Note:
Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android.
@param text the text to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/
[[nodiscard]] static ScopedMessageBox shareTextScoped (const String& text,
Callback callback,
Component* parent = nullptr);
/** A convenience function to share an image. This is useful when you have images
loaded in memory. The images will be written to temporary files first, so if
you have the images in question stored on disk already call shareFiles() instead.
By default, images will be saved to PNG files, but you can supply a custom
ImageFileFormat to override this. The custom file format will be owned and
deleted by the sharer. e.g.
@code
Graphics g (myImage);
g.setColour (Colours::green);
g.fillEllipse (20, 20, 300, 200);
Array<Image> images;
images.add (myImage);
ContentSharer::getInstance()->shareImages (images, myCallback);
@endcode
Upon completion you will receive a callback with a sharing result. Note:
Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android.
@param images the images to share
@param format the file format to use when saving the images.
If no format is provided, a sensible default will
be used.
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/
[[nodiscard]] static ScopedMessageBox shareImagesScoped (const Array<Image>& images,
std::unique_ptr<ImageFileFormat> format,
Callback callback,
Component* parent = nullptr);
/** A convenience function to share arbitrary data. The data will be written
to a temporary file and then that file will be shared. If you have
your data stored on disk already, call shareFiles() instead.
Upon completion you will receive a callback with a sharing result. Note:
Sadly on Android the returned success flag may be wrong as there is no
standard way the sharing targets report if the sharing operation
succeeded. Also, the optional error message is always empty on Android.
@param mb the data to share
@param callback a callback that will be called on the main thread
when the sharing session ends
@param parent the component that should be used to host the
sharing view
*/
[[nodiscard]] static ScopedMessageBox shareDataScoped (const MemoryBlock& mb,
Callback callback,
Component* parent = nullptr);
};
} // namespace juce
@@ -0,0 +1,70 @@
/*
==============================================================================
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
{
DirectoryContentsDisplayComponent::DirectoryContentsDisplayComponent (DirectoryContentsList& l)
: directoryContentsList (l)
{
}
DirectoryContentsDisplayComponent::~DirectoryContentsDisplayComponent()
{
}
//==============================================================================
FileBrowserListener::~FileBrowserListener()
{
}
void DirectoryContentsDisplayComponent::addListener (FileBrowserListener* l) { listeners.add (l); }
void DirectoryContentsDisplayComponent::removeListener (FileBrowserListener* l) { listeners.remove (l); }
void DirectoryContentsDisplayComponent::sendSelectionChangeMessage()
{
Component::BailOutChecker checker (dynamic_cast<Component*> (this));
listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
}
void DirectoryContentsDisplayComponent::sendMouseClickMessage (const File& file, const MouseEvent& e)
{
if (directoryContentsList.getDirectory().exists())
{
Component::BailOutChecker checker (dynamic_cast<Component*> (this));
listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (file, e); });
}
}
void DirectoryContentsDisplayComponent::sendDoubleClickMessage (const File& file)
{
if (directoryContentsList.getDirectory().exists())
{
Component::BailOutChecker checker (dynamic_cast<Component*> (this));
listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (file); });
}
}
} // namespace juce
@@ -0,0 +1,116 @@
/*
==============================================================================
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 base class for components that display a list of the files in a directory.
@see DirectoryContentsList
@tags{GUI}
*/
class JUCE_API DirectoryContentsDisplayComponent
{
public:
//==============================================================================
/** Creates a DirectoryContentsDisplayComponent for a given list of files. */
DirectoryContentsDisplayComponent (DirectoryContentsList& listToShow);
/** Destructor. */
virtual ~DirectoryContentsDisplayComponent();
//==============================================================================
/** The list that this component is displaying */
DirectoryContentsList& directoryContentsList;
//==============================================================================
/** Returns the number of files the user has got selected.
@see getSelectedFile
*/
virtual int getNumSelectedFiles() const = 0;
/** Returns one of the files that the user has currently selected.
The index should be in the range 0 to (getNumSelectedFiles() - 1).
@see getNumSelectedFiles
*/
virtual File getSelectedFile (int index) const = 0;
/** Deselects any selected files. */
virtual void deselectAllFiles() = 0;
/** Scrolls this view to the top. */
virtual void scrollToTop() = 0;
/** If the specified file is in the list, it will become the only selected item
(and if the file isn't in the list, all other items will be deselected). */
virtual void setSelectedFile (const File&) = 0;
//==============================================================================
/** Adds a listener to be told when files are selected or clicked.
@see removeListener
*/
void addListener (FileBrowserListener* listener);
/** Removes a listener.
@see addListener
*/
void removeListener (FileBrowserListener* listener);
//==============================================================================
/** A set of colour IDs to use to change the colour of various aspects of the list.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
highlightColourId = 0x1000540, /**< The colour to use to fill a highlighted row of the list. */
textColourId = 0x1000541, /**< The colour for the text. */
highlightedTextColourId = 0x1000542 /**< The colour with which to draw the text in highlighted sections. */
};
//==============================================================================
/** @internal */
void sendSelectionChangeMessage();
/** @internal */
void sendDoubleClickMessage (const File&);
/** @internal */
void sendMouseClickMessage (const File&, const MouseEvent&);
protected:
//==============================================================================
ListenerList<FileBrowserListener> listeners;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectoryContentsDisplayComponent)
};
} // namespace juce
@@ -0,0 +1,270 @@
/*
==============================================================================
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
{
DirectoryContentsList::DirectoryContentsList (const FileFilter* f, TimeSliceThread& t)
: fileFilter (f), thread (t)
{
}
DirectoryContentsList::~DirectoryContentsList()
{
stopSearching();
}
void DirectoryContentsList::setIgnoresHiddenFiles (const bool shouldIgnoreHiddenFiles)
{
setTypeFlags (shouldIgnoreHiddenFiles ? (fileTypeFlags | File::ignoreHiddenFiles)
: (fileTypeFlags & ~File::ignoreHiddenFiles));
}
bool DirectoryContentsList::ignoresHiddenFiles() const
{
return (fileTypeFlags & File::ignoreHiddenFiles) != 0;
}
//==============================================================================
void DirectoryContentsList::setDirectory (const File& directory,
const bool includeDirectories,
const bool includeFiles)
{
jassert (includeDirectories || includeFiles); // you have to specify at least one of these!
if (directory != root)
{
clear();
root = directory;
changed();
// (this forces a refresh when setTypeFlags() is called, rather than triggering two refreshes)
fileTypeFlags &= ~(File::findDirectories | File::findFiles);
}
auto newFlags = fileTypeFlags;
if (includeDirectories) newFlags |= File::findDirectories;
else newFlags &= ~File::findDirectories;
if (includeFiles) newFlags |= File::findFiles;
else newFlags &= ~File::findFiles;
setTypeFlags (newFlags);
}
void DirectoryContentsList::setTypeFlags (const int newFlags)
{
if (fileTypeFlags != newFlags)
{
fileTypeFlags = newFlags;
refresh();
}
}
void DirectoryContentsList::stopSearching()
{
shouldStop = true;
thread.removeTimeSliceClient (this);
isSearching = false;
}
void DirectoryContentsList::clear()
{
stopSearching();
if (! files.isEmpty())
{
files.clear();
changed();
}
}
void DirectoryContentsList::refresh()
{
stopSearching();
wasEmpty = files.isEmpty();
files.clear();
if (root.isDirectory())
{
fileFindHandle = std::make_unique<RangedDirectoryIterator> (root, false, "*", fileTypeFlags);
shouldStop = false;
isSearching = true;
thread.addTimeSliceClient (this);
}
}
void DirectoryContentsList::setFileFilter (const FileFilter* newFileFilter)
{
const ScopedLock sl (fileListLock);
fileFilter = newFileFilter;
}
//==============================================================================
int DirectoryContentsList::getNumFiles() const noexcept
{
const ScopedLock sl (fileListLock);
return files.size();
}
bool DirectoryContentsList::getFileInfo (const int index, FileInfo& result) const
{
const ScopedLock sl (fileListLock);
if (auto* info = files [index])
{
result = *info;
return true;
}
return false;
}
File DirectoryContentsList::getFile (const int index) const
{
const ScopedLock sl (fileListLock);
if (auto* info = files [index])
return root.getChildFile (info->filename);
return {};
}
bool DirectoryContentsList::contains (const File& targetFile) const
{
const ScopedLock sl (fileListLock);
for (int i = files.size(); --i >= 0;)
if (root.getChildFile (files.getUnchecked (i)->filename) == targetFile)
return true;
return false;
}
bool DirectoryContentsList::isStillLoading() const
{
return isSearching;
}
void DirectoryContentsList::changed()
{
sendChangeMessage();
}
//==============================================================================
int DirectoryContentsList::useTimeSlice()
{
auto startTime = Time::getApproximateMillisecondCounter();
bool hasChanged = false;
for (int i = 100; --i >= 0;)
{
if (! checkNextFile (hasChanged))
{
if (hasChanged)
changed();
return 500;
}
if (shouldStop || (Time::getApproximateMillisecondCounter() > startTime + 150))
break;
}
if (hasChanged)
changed();
return 0;
}
bool DirectoryContentsList::checkNextFile (bool& hasChanged)
{
if (fileFindHandle == nullptr)
return false;
if (*fileFindHandle == RangedDirectoryIterator())
{
fileFindHandle = nullptr;
isSearching = false;
hasChanged = true;
return false;
}
const auto entry = *(*fileFindHandle)++;
hasChanged |= addFile (entry.getFile(),
entry.isDirectory(),
entry.getFileSize(),
entry.getModificationTime(),
entry.getCreationTime(),
entry.isReadOnly());
return true;
}
bool DirectoryContentsList::addFile (const File& file, const bool isDir,
const int64 fileSize,
Time modTime, Time creationTime,
const bool isReadOnly)
{
const ScopedLock sl (fileListLock);
if (fileFilter == nullptr
|| ((! isDir) && fileFilter->isFileSuitable (file))
|| (isDir && fileFilter->isDirectorySuitable (file)))
{
auto info = std::make_unique<FileInfo>();
info->filename = file.getFileName();
info->fileSize = fileSize;
info->modificationTime = modTime;
info->creationTime = creationTime;
info->isDirectory = isDir;
info->isReadOnly = isReadOnly;
for (int i = files.size(); --i >= 0;)
if (files.getUnchecked (i)->filename == info->filename)
return false;
files.add (std::move (info));
std::sort (files.begin(), files.end(), [] (const FileInfo* a, const FileInfo* b)
{
#if JUCE_WINDOWS
if (a->isDirectory != b->isDirectory)
return a->isDirectory;
#endif
return a->filename.compareNatural (b->filename) < 0;
});
return true;
}
return false;
}
} // namespace juce
@@ -0,0 +1,226 @@
/*
==============================================================================
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 class to asynchronously scan for details about the files in a directory.
This keeps a list of files and some information about them, using a background
thread to scan for more files. As files are found, it broadcasts change messages
to tell any listeners.
@see FileListComponent, FileBrowserComponent
@tags{GUI}
*/
class JUCE_API DirectoryContentsList : public ChangeBroadcaster,
private TimeSliceClient
{
public:
//==============================================================================
/** Creates a directory list.
To set the directory it should point to, use setDirectory(), which will
also start it scanning for files on the background thread.
When the background thread finds and adds new files to this list, the
ChangeBroadcaster class will send a change message, so you can register
listeners and update them when the list changes.
@param fileFilter an optional filter to select which files are
included in the list. If this is nullptr, then all files
and directories are included. Make sure that the filter
doesn't get deleted during the lifetime of this object
@param threadToUse a thread object that this list can use
to scan for files as a background task. Make sure
that the thread you give it has been started, or you
won't get any files!
*/
DirectoryContentsList (const FileFilter* fileFilter,
TimeSliceThread& threadToUse);
/** Destructor. */
~DirectoryContentsList() override;
//==============================================================================
/** Returns the directory that's currently being used. */
const File& getDirectory() const noexcept { return root; }
/** Sets the directory to look in for files.
If the directory that's passed in is different to the current one, this will
also start the background thread scanning it for files.
*/
void setDirectory (const File& directory,
bool includeDirectories,
bool includeFiles);
/** Returns true if this list contains directories.
@see setDirectory
*/
bool isFindingDirectories() const noexcept { return (fileTypeFlags & File::findDirectories) != 0; }
/** Returns true if this list contains files.
@see setDirectory
*/
bool isFindingFiles() const noexcept { return (fileTypeFlags & File::findFiles) != 0; }
/** Clears the list, and stops the thread scanning for files. */
void clear();
/** Clears the list and restarts scanning the directory for files. */
void refresh();
/** True if the background thread hasn't yet finished scanning for files. */
bool isStillLoading() const;
/** Tells the list whether or not to ignore hidden files.
By default these are ignored.
*/
void setIgnoresHiddenFiles (bool shouldIgnoreHiddenFiles);
/** Returns true if hidden files are ignored.
@see setIgnoresHiddenFiles
*/
bool ignoresHiddenFiles() const;
/** Replaces the current FileFilter.
This can be nullptr to have no filter. The DirectoryContentList does not take
ownership of this object - it just keeps a pointer to it, so you must manage its
lifetime.
Note that this only replaces the filter, it doesn't refresh the list - you'll
probably want to call refresh() after calling this.
*/
void setFileFilter (const FileFilter* newFileFilter);
//==============================================================================
/** Contains cached information about one of the files in a DirectoryContentsList.
*/
struct FileInfo
{
//==============================================================================
/** The filename.
This isn't a full pathname, it's just the last part of the path, same as you'd
get from File::getFileName().
To get the full pathname, use DirectoryContentsList::getDirectory().getChildFile (filename).
*/
String filename;
/** File size in bytes. */
int64 fileSize;
/** File modification time.
As supplied by File::getLastModificationTime().
*/
Time modificationTime;
/** File creation time.
As supplied by File::getCreationTime().
*/
Time creationTime;
/** True if the file is a directory. */
bool isDirectory;
/** True if the file is read-only. */
bool isReadOnly;
};
//==============================================================================
/** Returns the number of files currently available in the list.
The info about one of these files can be retrieved with getFileInfo() or getFile().
Obviously as the background thread runs and scans the directory for files, this
number will change.
@see getFileInfo, getFile
*/
int getNumFiles() const noexcept;
/** Returns the cached information about one of the files in the list.
If the index is in-range, this will return true and will copy the file's details
to the structure that is passed-in.
If it returns false, then the index wasn't in range, and the structure won't
be affected.
@see getNumFiles, getFile
*/
bool getFileInfo (int index, FileInfo& resultInfo) const;
/** Returns one of the files in the list.
@param index should be less than getNumFiles(). If this is out-of-range, the
return value will be a default File() object
@see getNumFiles, getFileInfo
*/
File getFile (int index) const;
/** Returns the file filter being used.
The filter is specified in the constructor.
*/
const FileFilter* getFilter() const noexcept { return fileFilter; }
/** Returns true if the list contains the specified file. */
bool contains (const File&) const;
//==============================================================================
/** @internal */
TimeSliceThread& getTimeSliceThread() const noexcept { return thread; }
private:
File root;
const FileFilter* fileFilter = nullptr;
TimeSliceThread& thread;
int fileTypeFlags = File::ignoreHiddenFiles | File::findFiles;
CriticalSection fileListLock;
OwnedArray<FileInfo> files;
std::unique_ptr<RangedDirectoryIterator> fileFindHandle;
std::atomic<bool> shouldStop { true }, isSearching { false };
bool wasEmpty = true;
int useTimeSlice() override;
void stopSearching();
void changed();
bool checkNextFile (bool& hasChanged);
bool addFile (const File&, bool isDir, int64 fileSize, Time modTime,
Time creationTime, bool isReadOnly);
void setTypeFlags (int);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectoryContentsList)
};
} // namespace juce
@@ -0,0 +1,624 @@
/*
==============================================================================
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
{
FileBrowserComponent::FileBrowserComponent (int flags_,
const File& initialFileOrDirectory,
const FileFilter* fileFilter_,
FilePreviewComponent* previewComp_)
: FileFilter ({}),
fileFilter (fileFilter_),
flags (flags_),
previewComp (previewComp_),
currentPathBox ("path"),
fileLabel ("f", TRANS ("file:")),
thread ("JUCE FileBrowser"),
wasProcessActive (true)
{
// You need to specify one or other of the open/save flags..
jassert ((flags & (saveMode | openMode)) != 0);
jassert ((flags & (saveMode | openMode)) != (saveMode | openMode));
// You need to specify at least one of these flags..
jassert ((flags & (canSelectFiles | canSelectDirectories)) != 0);
String filename;
if (initialFileOrDirectory == File())
{
currentRoot = File::getCurrentWorkingDirectory();
}
else if (initialFileOrDirectory.isDirectory())
{
currentRoot = initialFileOrDirectory;
}
else
{
chosenFiles.add (initialFileOrDirectory);
currentRoot = initialFileOrDirectory.getParentDirectory();
filename = initialFileOrDirectory.getFileName();
}
// The thread must be started before the DirectoryContentsList attempts to scan any file
thread.startThread (Thread::Priority::low);
fileList.reset (new DirectoryContentsList (this, thread));
fileList->setDirectory (currentRoot, true, true);
if ((flags & useTreeView) != 0)
{
auto tree = new FileTreeComponent (*fileList);
fileListComponent.reset (tree);
if ((flags & canSelectMultipleItems) != 0)
tree->setMultiSelectEnabled (true);
addAndMakeVisible (tree);
}
else
{
auto list = new FileListComponent (*fileList);
fileListComponent.reset (list);
list->setOutlineThickness (1);
if ((flags & canSelectMultipleItems) != 0)
list->setMultipleSelectionEnabled (true);
addAndMakeVisible (list);
}
fileListComponent->addListener (this);
addAndMakeVisible (currentPathBox);
currentPathBox.setEditableText (true);
resetRecentPaths();
currentPathBox.onChange = [this] { updateSelectedPath(); };
addAndMakeVisible (filenameBox);
filenameBox.setMultiLine (false);
filenameBox.setSelectAllWhenFocused (true);
filenameBox.setText (filename, false);
filenameBox.onTextChange = [this] { sendListenerChangeMessage(); };
filenameBox.onReturnKey = [this] { changeFilename(); };
filenameBox.onFocusLost = [this]
{
if (! isSaveMode())
selectionChanged();
};
filenameBox.setReadOnly ((flags & (filenameBoxIsReadOnly | canSelectMultipleItems)) != 0);
addAndMakeVisible (fileLabel);
fileLabel.attachToComponent (&filenameBox, true);
if (previewComp != nullptr)
addAndMakeVisible (previewComp);
lookAndFeelChanged();
setRoot (currentRoot);
if (filename.isNotEmpty())
setFileName (filename);
startTimer (2000);
}
FileBrowserComponent::~FileBrowserComponent()
{
fileListComponent.reset();
fileList.reset();
thread.stopThread (10000);
}
//==============================================================================
void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
{
listeners.add (newListener);
}
void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
{
listeners.remove (listener);
}
//==============================================================================
bool FileBrowserComponent::isSaveMode() const noexcept
{
return (flags & saveMode) != 0;
}
int FileBrowserComponent::getNumSelectedFiles() const noexcept
{
if (chosenFiles.isEmpty() && currentFileIsValid())
return 1;
return chosenFiles.size();
}
File FileBrowserComponent::getSelectedFile (int index) const noexcept
{
if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
return currentRoot;
if (! filenameBox.isReadOnly())
return currentRoot.getChildFile (filenameBox.getText());
return chosenFiles[index];
}
bool FileBrowserComponent::currentFileIsValid() const
{
auto f = getSelectedFile (0);
if ((flags & canSelectDirectories) == 0 && f.isDirectory())
return false;
return isSaveMode() || f.exists();
}
File FileBrowserComponent::getHighlightedFile() const noexcept
{
return fileListComponent->getSelectedFile (0);
}
void FileBrowserComponent::deselectAllFiles()
{
fileListComponent->deselectAllFiles();
}
//==============================================================================
bool FileBrowserComponent::isFileSuitable (const File& file) const
{
return (flags & canSelectFiles) != 0
&& (fileFilter == nullptr || fileFilter->isFileSuitable (file));
}
bool FileBrowserComponent::isDirectorySuitable (const File&) const
{
return true;
}
bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
{
if (f.isDirectory())
return (flags & canSelectDirectories) != 0
&& (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
return (flags & canSelectFiles) != 0 && f.exists()
&& (fileFilter == nullptr || fileFilter->isFileSuitable (f));
}
//==============================================================================
const File& FileBrowserComponent::getRoot() const
{
return currentRoot;
}
void FileBrowserComponent::setRoot (const File& newRootDirectory)
{
bool callListeners = false;
if (currentRoot != newRootDirectory)
{
callListeners = true;
fileListComponent->scrollToTop();
String path (newRootDirectory.getFullPathName());
if (path.isEmpty())
path = File::getSeparatorString();
StringArray rootNames, rootPaths;
getRoots (rootNames, rootPaths);
if (! rootPaths.contains (path, true))
{
bool alreadyListed = false;
for (int i = currentPathBox.getNumItems(); --i >= 0;)
{
if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
{
alreadyListed = true;
break;
}
}
if (! alreadyListed)
currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
}
}
currentRoot = newRootDirectory;
fileList->setDirectory (currentRoot, true, true);
if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
tree->refresh();
auto currentRootName = currentRoot.getFullPathName();
if (currentRootName.isEmpty())
currentRootName = File::getSeparatorString();
currentPathBox.setText (currentRootName, dontSendNotification);
goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
&& currentRoot.getParentDirectory() != currentRoot);
if (callListeners)
{
Component::BailOutChecker checker (this);
listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
}
}
void FileBrowserComponent::setFileName (const String& newName)
{
filenameBox.setText (newName, true);
fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
}
void FileBrowserComponent::resetRecentPaths()
{
currentPathBox.clear();
StringArray rootNames, rootPaths;
getRoots (rootNames, rootPaths);
for (int i = 0; i < rootNames.size(); ++i)
{
if (rootNames[i].isEmpty())
currentPathBox.addSeparator();
else
currentPathBox.addItem (rootNames[i], i + 1);
}
currentPathBox.addSeparator();
}
void FileBrowserComponent::goUp()
{
setRoot (getRoot().getParentDirectory());
}
void FileBrowserComponent::refresh()
{
fileList->refresh();
}
void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
{
if (fileFilter != newFileFilter)
{
fileFilter = newFileFilter;
refresh();
}
}
String FileBrowserComponent::getActionVerb() const
{
return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS ("Choose")
: TRANS ("Save"))
: TRANS ("Open");
}
void FileBrowserComponent::setFilenameBoxLabel (const String& name)
{
fileLabel.setText (name, dontSendNotification);
}
FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
{
return previewComp;
}
DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
{
return fileListComponent.get();
}
//==============================================================================
void FileBrowserComponent::resized()
{
getLookAndFeel()
.layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
&currentPathBox, &filenameBox, goUpButton.get());
}
//==============================================================================
void FileBrowserComponent::lookAndFeelChanged()
{
goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
if (auto* buttonPtr = goUpButton.get())
{
addAndMakeVisible (*buttonPtr);
buttonPtr->onClick = [this] { goUp(); };
buttonPtr->setTooltip (TRANS ("Go up to parent directory"));
}
currentPathBox.setColour (ComboBox::backgroundColourId, findColour (currentPathBoxBackgroundColourId));
currentPathBox.setColour (ComboBox::textColourId, findColour (currentPathBoxTextColourId));
currentPathBox.setColour (ComboBox::arrowColourId, findColour (currentPathBoxArrowColourId));
filenameBox.setColour (TextEditor::backgroundColourId, findColour (filenameBoxBackgroundColourId));
filenameBox.applyColourToAllText (findColour (filenameBoxTextColourId));
resized();
}
//==============================================================================
void FileBrowserComponent::sendListenerChangeMessage()
{
Component::BailOutChecker checker (this);
if (previewComp != nullptr)
previewComp->selectedFileChanged (getSelectedFile (0));
// You shouldn't delete the browser when the file gets changed!
jassert (! checker.shouldBailOut());
listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
}
void FileBrowserComponent::selectionChanged()
{
StringArray newFilenames;
bool resetChosenFiles = true;
for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
{
const File f (fileListComponent->getSelectedFile (i));
if (isFileOrDirSuitable (f))
{
if (resetChosenFiles)
{
chosenFiles.clear();
resetChosenFiles = false;
}
chosenFiles.add (f);
newFilenames.add (f.getRelativePathFrom (getRoot()));
}
}
if (newFilenames.size() > 0)
filenameBox.setText (newFilenames.joinIntoString (", "), false);
sendListenerChangeMessage();
}
void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
{
Component::BailOutChecker checker (this);
listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
}
void FileBrowserComponent::fileDoubleClicked (const File& f)
{
if (f.isDirectory())
{
setRoot (f);
if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
filenameBox.setText ({});
}
else
{
Component::BailOutChecker checker (this);
listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
}
}
void FileBrowserComponent::browserRootChanged (const File&) {}
bool FileBrowserComponent::keyPressed ([[maybe_unused]] const KeyPress& key)
{
#if JUCE_LINUX || JUCE_BSD || JUCE_WINDOWS
if (key.getModifiers().isCommandDown()
&& (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
{
fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
fileList->refresh();
return true;
}
#endif
return false;
}
//==============================================================================
void FileBrowserComponent::changeFilename()
{
if (filenameBox.getText().containsChar (File::getSeparatorChar()))
{
auto f = currentRoot.getChildFile (filenameBox.getText());
if (f.isDirectory())
{
setRoot (f);
chosenFiles.clear();
if ((flags & doNotClearFileNameOnRootChange) == 0)
filenameBox.setText ({});
}
else
{
setRoot (f.getParentDirectory());
chosenFiles.clear();
chosenFiles.add (f);
filenameBox.setText (f.getFileName());
}
}
else
{
fileDoubleClicked (getSelectedFile (0));
}
}
//==============================================================================
void FileBrowserComponent::updateSelectedPath()
{
auto newText = currentPathBox.getText().trim().unquoted();
if (newText.isNotEmpty())
{
auto index = currentPathBox.getSelectedId() - 1;
StringArray rootNames, rootPaths;
getRoots (rootNames, rootPaths);
if (rootPaths[index].isNotEmpty())
{
setRoot (File (rootPaths[index]));
}
else
{
File f (newText);
for (;;)
{
if (f.isDirectory())
{
setRoot (f);
break;
}
if (f.getParentDirectory() == f)
break;
f = f.getParentDirectory();
}
}
}
}
void FileBrowserComponent::getDefaultRoots (StringArray& rootNames, StringArray& rootPaths)
{
#if JUCE_WINDOWS
Array<File> roots;
File::findFileSystemRoots (roots);
rootPaths.clear();
for (int i = 0; i < roots.size(); ++i)
{
const File& drive = roots.getReference (i);
String name (drive.getFullPathName());
rootPaths.add (name);
if (drive.isOnHardDisk())
{
String volume (drive.getVolumeLabel());
if (volume.isEmpty())
volume = TRANS ("Hard Drive");
name << " [" << volume << ']';
}
else if (drive.isOnCDRomDrive())
{
name << " [" << TRANS ("CD/DVD drive") << ']';
}
rootNames.add (name);
}
rootPaths.add ({});
rootNames.add ({});
rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
rootNames.add (TRANS ("Documents"));
rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
rootNames.add (TRANS ("Music"));
rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
rootNames.add (TRANS ("Pictures"));
rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
rootNames.add (TRANS ("Desktop"));
#elif JUCE_MAC
rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
rootNames.add (TRANS ("Home folder"));
rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
rootNames.add (TRANS ("Documents"));
rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
rootNames.add (TRANS ("Music"));
rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
rootNames.add (TRANS ("Pictures"));
rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
rootNames.add (TRANS ("Desktop"));
rootPaths.add ({});
rootNames.add ({});
for (auto& volume : File ("/Volumes").findChildFiles (File::findDirectories, false))
{
if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
{
rootPaths.add (volume.getFullPathName());
rootNames.add (volume.getFileName());
}
}
#else
rootPaths.add ("/");
rootNames.add ("/");
rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
rootNames.add (TRANS ("Home folder"));
rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
rootNames.add (TRANS ("Desktop"));
#endif
}
void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
{
getDefaultRoots (rootNames, rootPaths);
}
void FileBrowserComponent::timerCallback()
{
const auto isProcessActive = detail::WindowingHelpers::isForegroundOrEmbeddedProcess (this);
if (wasProcessActive != isProcessActive)
{
wasProcessActive = isProcessActive;
if (isProcessActive && fileList != nullptr)
refresh();
}
}
//==============================================================================
std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler()
{
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
}
} // namespace juce
@@ -0,0 +1,297 @@
/*
==============================================================================
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 component for browsing and selecting a file or directory to open or save.
This contains a FileListComponent and adds various boxes and controls for
navigating and selecting a file. It can work in different modes so that it can
be used for loading or saving a file, or for choosing a directory.
@see FileChooserDialogBox, FileChooser, FileListComponent
@tags{GUI}
*/
class JUCE_API FileBrowserComponent : public Component,
private FileBrowserListener,
private FileFilter,
private Timer
{
public:
//==============================================================================
/** Various options for the browser.
A combination of these is passed into the FileBrowserComponent constructor.
*/
enum FileChooserFlags
{
openMode = 1, /**< specifies that the component should allow the user to
choose an existing file with the intention of opening it. */
saveMode = 2, /**< specifies that the component should allow the user to specify
the name of a file that will be used to save something. */
canSelectFiles = 4, /**< specifies that the user can select files (can be used in
conjunction with canSelectDirectories). */
canSelectDirectories = 8, /**< specifies that the user can select directories (can be used in
conjunction with canSelectFiles). */
canSelectMultipleItems = 16, /**< specifies that the user can select multiple items. */
useTreeView = 32, /**< specifies that a tree-view should be shown instead of a file list. */
filenameBoxIsReadOnly = 64, /**< specifies that the user can't type directly into the filename box. */
warnAboutOverwriting = 128, /**< specifies that the dialog should warn about overwriting existing files (if possible). */
doNotClearFileNameOnRootChange = 256 /**< specifies that the file name should not be cleared upon root change. */
};
//==============================================================================
/** Creates a FileBrowserComponent.
@param flags A combination of flags from the FileChooserFlags enumeration, used to
specify the component's behaviour. The flags must contain either openMode
or saveMode, and canSelectFiles and/or canSelectDirectories.
@param initialFileOrDirectory The file or directory that should be selected when the component begins.
If this is File(), a default directory will be chosen.
@param fileFilter an optional filter to use to determine which files are shown.
If this is nullptr then all files are displayed. Note that a pointer
is kept internally to this object, so make sure that it is not deleted
before the FileBrowserComponent object is deleted.
@param previewComp an optional preview component that will be used to show previews of
files that the user selects
*/
FileBrowserComponent (int flags,
const File& initialFileOrDirectory,
const FileFilter* fileFilter,
FilePreviewComponent* previewComp);
/** Destructor. */
~FileBrowserComponent() override;
//==============================================================================
/** Returns the number of files that the user has got selected.
If multiple select isn't active, this will only be 0 or 1. To get the complete
list of files they've chosen, pass an index to getCurrentFile().
*/
int getNumSelectedFiles() const noexcept;
/** Returns one of the files that the user has chosen.
If the box has multi-select enabled, the index lets you specify which of the files
to get - see getNumSelectedFiles() to find out how many files were chosen.
@see getHighlightedFile
*/
File getSelectedFile (int index) const noexcept;
/** Deselects any files that are currently selected. */
void deselectAllFiles();
/** Returns true if the currently selected file(s) are usable.
This can be used to decide whether the user can press "ok" for the
current file. What it does depends on the mode, so for example in an "open"
mode, this only returns true if a file has been selected and if it exists.
In a "save" mode, a non-existent file would also be valid.
*/
bool currentFileIsValid() const;
/** This returns the last item in the view that the user has highlighted.
This may be different from getCurrentFile(), which returns the value
that is shown in the filename box, and if there are multiple selections,
this will only return one of them.
@see getSelectedFile
*/
File getHighlightedFile() const noexcept;
//==============================================================================
/** Returns the directory whose contents are currently being shown in the listbox. */
const File& getRoot() const;
/** Changes the directory that's being shown in the listbox. */
void setRoot (const File& newRootDirectory);
/** Changes the name that is currently shown in the filename box. */
void setFileName (const String& newName);
/** Equivalent to pressing the "up" button to browse the parent directory. */
void goUp();
/** Refreshes the directory that's currently being listed. */
void refresh();
/** Changes the filter that's being used to sift the files. */
void setFileFilter (const FileFilter* newFileFilter);
/** Returns a verb to describe what should happen when the file is accepted.
E.g. if browsing in "load file" mode, this will be "Open", if in "save file"
mode, it'll be "Save", etc.
*/
virtual String getActionVerb() const;
/** Returns true if the saveMode flag was set when this component was created. */
bool isSaveMode() const noexcept;
/** Sets the label that will be displayed next to the filename entry box.
By default this is just "file", but you might want to change it to something more
appropriate for your app.
*/
void setFilenameBoxLabel (const String& name);
//==============================================================================
/** Adds a listener to be told when the user selects and clicks on files.
@see removeListener
*/
void addListener (FileBrowserListener* listener);
/** Removes a listener.
@see addListener
*/
void removeListener (FileBrowserListener* listener);
/** Returns a platform-specific list of names and paths for some suggested places the user
might want to use as root folders.
The list returned contains empty strings to indicate section breaks.
@see getRoots()
*/
static void getDefaultRoots (StringArray& rootNames, StringArray& rootPaths);
//==============================================================================
/** This abstract base class is implemented by LookAndFeel classes to provide
various file-browser layout and drawing methods.
*/
struct JUCE_API LookAndFeelMethods
{
virtual ~LookAndFeelMethods() = default;
// These return a pointer to an internally cached drawable - make sure you don't keep
// a copy of this pointer anywhere, as it may become invalid in the future.
virtual const Drawable* getDefaultFolderImage() = 0;
virtual const Drawable* getDefaultDocumentFileImage() = 0;
virtual AttributedString createFileChooserHeaderText (const String& title,
const String& instructions) = 0;
virtual void drawFileBrowserRow (Graphics&, int width, int height,
const File& file,
const String& filename,
Image* optionalIcon,
const String& fileSizeDescription,
const String& fileTimeDescription,
bool isDirectory,
bool isItemSelected,
int itemIndex,
DirectoryContentsDisplayComponent&) = 0;
virtual Button* createFileBrowserGoUpButton() = 0;
virtual void layoutFileBrowserComponent (FileBrowserComponent& browserComp,
DirectoryContentsDisplayComponent* fileListComponent,
FilePreviewComponent* previewComp,
ComboBox* currentPathBox,
TextEditor* filenameBox,
Button* goUpButton) = 0;
};
/** A set of colour IDs to use to change the colour of various aspects of the FileBrowserComponent.
These constants can be used either via the Component::setColour(), or LookAndFeel::setColour()
methods.
@see Component::setColour, Component::findColour, LookAndFeel::setColour, LookAndFeel::findColour
*/
enum ColourIds
{
currentPathBoxBackgroundColourId = 0x1000640, /**< The colour to use to fill the background of the current path ComboBox. */
currentPathBoxTextColourId = 0x1000641, /**< The colour to use for the text of the current path ComboBox. */
currentPathBoxArrowColourId = 0x1000642, /**< The colour to use to draw the arrow of the current path ComboBox. */
filenameBoxBackgroundColourId = 0x1000643, /**< The colour to use to fill the background of the filename TextEditor. */
filenameBoxTextColourId = 0x1000644 /**< The colour to use for the text of the filename TextEditor. */
};
//==============================================================================
/** @internal */
void resized() override;
/** @internal */
void lookAndFeelChanged() override;
/** @internal */
bool keyPressed (const KeyPress&) override;
/** @internal */
void selectionChanged() override;
/** @internal */
void fileClicked (const File&, const MouseEvent&) override;
/** @internal */
void fileDoubleClicked (const File&) override;
/** @internal */
void browserRootChanged (const File&) override;
/** @internal */
bool isFileSuitable (const File&) const override;
/** @internal */
bool isDirectorySuitable (const File&) const override;
/** @internal */
FilePreviewComponent* getPreviewComponent() const noexcept;
/** @internal */
DirectoryContentsDisplayComponent* getDisplayComponent() const noexcept;
/** @internal */
std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override;
protected:
/** Returns a list of names and paths for the default places the user might want to look.
By default this just calls getDefaultRoots(), but you may want to override it to
return a custom list.
*/
virtual void getRoots (StringArray& rootNames, StringArray& rootPaths);
/** Updates the items in the dropdown list of recent paths with the values from getRoots(). */
void resetRecentPaths();
private:
//==============================================================================
std::unique_ptr<DirectoryContentsList> fileList;
const FileFilter* fileFilter;
int flags;
File currentRoot;
Array<File> chosenFiles;
ListenerList<FileBrowserListener> listeners;
std::unique_ptr<DirectoryContentsDisplayComponent> fileListComponent;
FilePreviewComponent* previewComp;
ComboBox currentPathBox;
TextEditor filenameBox;
Label fileLabel;
std::unique_ptr<Button> goUpButton;
TimeSliceThread thread;
bool wasProcessActive;
void timerCallback() override;
void sendListenerChangeMessage();
bool isFileOrDirSuitable (const File&) const;
void updateSelectedPath();
void changeFilename();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileBrowserComponent)
};
} // 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
{
//==============================================================================
/**
A listener for user selection events in a file browser.
This is used by a FileBrowserComponent or FileListComponent.
@tags{GUI}
*/
class JUCE_API FileBrowserListener
{
public:
//==============================================================================
/** Destructor. */
virtual ~FileBrowserListener();
//==============================================================================
/** Callback when the user selects a different file in the browser. */
virtual void selectionChanged() = 0;
/** Callback when the user clicks on a file in the browser. */
virtual void fileClicked (const File& file, const MouseEvent& e) = 0;
/** Callback when the user double-clicks on a file in the browser. */
virtual void fileDoubleClicked (const File& file) = 0;
/** Callback when the browser's root folder changes. */
virtual void browserRootChanged (const File& newRoot) = 0;
};
} // namespace juce
@@ -0,0 +1,283 @@
/*
==============================================================================
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 FileChooser::NonNative final : public std::enable_shared_from_this<NonNative>,
public FileChooser::Pimpl
{
public:
NonNative (FileChooser& fileChooser, int flags, FilePreviewComponent* preview)
: owner (fileChooser),
selectsDirectories ((flags & FileBrowserComponent::canSelectDirectories) != 0),
selectsFiles ((flags & FileBrowserComponent::canSelectFiles) != 0),
warnAboutOverwrite ((flags & FileBrowserComponent::warnAboutOverwriting) != 0),
filter (selectsFiles ? owner.filters : String(), selectsDirectories ? "*" : String(), {}),
browserComponent (flags, owner.startingFile, &filter, preview),
dialogBox (owner.title, {}, browserComponent, warnAboutOverwrite,
browserComponent.findColour (AlertWindow::backgroundColourId), owner.parent)
{}
~NonNative() override
{
dialogBox.exitModalState (0);
}
void launch() override
{
dialogBox.centreWithDefaultSize (nullptr);
const std::weak_ptr<NonNative> ref (shared_from_this());
auto* callback = ModalCallbackFunction::create ([ref] (int r)
{
if (auto locked = ref.lock())
locked->modalStateFinished (r);
});
dialogBox.enterModalState (true, callback, true);
}
void runModally() override
{
#if JUCE_MODAL_LOOPS_PERMITTED
modalStateFinished (dialogBox.show() ? 1 : 0);
#else
jassertfalse;
#endif
}
private:
void modalStateFinished (int returnValue)
{
Array<URL> result;
if (returnValue != 0)
{
for (int i = 0; i < browserComponent.getNumSelectedFiles(); ++i)
result.add (URL (browserComponent.getSelectedFile (i)));
}
owner.finished (result);
}
//==============================================================================
FileChooser& owner;
bool selectsDirectories, selectsFiles, warnAboutOverwrite;
WildcardFileFilter filter;
FileBrowserComponent browserComponent;
FileChooserDialogBox dialogBox;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NonNative)
};
//==============================================================================
FileChooser::FileChooser (const String& chooserBoxTitle,
const File& currentFileOrDirectory,
const String& fileFilters,
const bool useNativeBox,
const bool treatFilePackagesAsDirectories,
Component* parentComponentToUse)
: title (chooserBoxTitle),
filters (fileFilters),
startingFile (currentFileOrDirectory),
parent (parentComponentToUse),
useNativeDialogBox (useNativeBox && isPlatformDialogAvailable()),
treatFilePackagesAsDirs (treatFilePackagesAsDirectories)
{
#ifndef JUCE_MAC
ignoreUnused (treatFilePackagesAsDirs);
#endif
if (! fileFilters.containsNonWhitespaceChars())
filters = "*";
}
FileChooser::~FileChooser()
{
asyncCallback = nullptr;
}
#if JUCE_MODAL_LOOPS_PERMITTED
bool FileChooser::browseForFileToOpen (FilePreviewComponent* previewComp)
{
return showDialog (FileBrowserComponent::openMode
| FileBrowserComponent::canSelectFiles,
previewComp);
}
bool FileChooser::browseForMultipleFilesToOpen (FilePreviewComponent* previewComp)
{
return showDialog (FileBrowserComponent::openMode
| FileBrowserComponent::canSelectFiles
| FileBrowserComponent::canSelectMultipleItems,
previewComp);
}
bool FileChooser::browseForMultipleFilesOrDirectories (FilePreviewComponent* previewComp)
{
return showDialog (FileBrowserComponent::openMode
| FileBrowserComponent::canSelectFiles
| FileBrowserComponent::canSelectDirectories
| FileBrowserComponent::canSelectMultipleItems,
previewComp);
}
bool FileChooser::browseForFileToSave (const bool warnAboutOverwrite)
{
return showDialog (FileBrowserComponent::saveMode
| FileBrowserComponent::canSelectFiles
| (warnAboutOverwrite ? FileBrowserComponent::warnAboutOverwriting : 0),
nullptr);
}
bool FileChooser::browseForDirectory()
{
return showDialog (FileBrowserComponent::openMode
| FileBrowserComponent::canSelectDirectories,
nullptr);
}
bool FileChooser::showDialog (const int flags, FilePreviewComponent* const previewComp)
{
detail::FocusRestorer focusRestorer;
pimpl = createPimpl (flags, previewComp);
pimpl->runModally();
// ensure that the finished function was invoked
jassert (pimpl == nullptr);
return (results.size() > 0);
}
#endif
void FileChooser::launchAsync (int flags, std::function<void (const FileChooser&)> callback,
FilePreviewComponent* previewComp)
{
// You must specify a callback when using launchAsync
jassert (callback);
// you cannot run two file chooser dialog boxes at the same time
jassert (asyncCallback == nullptr);
asyncCallback = std::move (callback);
pimpl = createPimpl (flags, previewComp);
pimpl->launch();
}
std::shared_ptr<FileChooser::Pimpl> FileChooser::createPimpl (int flags, FilePreviewComponent* previewComp)
{
results.clear();
// the preview component needs to be the right size before you pass it in here..
jassert (previewComp == nullptr || (previewComp->getWidth() > 10
&& previewComp->getHeight() > 10));
if (pimpl != nullptr)
{
// you cannot run two file chooser dialog boxes at the same time
jassertfalse;
pimpl.reset();
}
// You've set the flags for both saveMode and openMode!
jassert (! (((flags & FileBrowserComponent::saveMode) != 0)
&& ((flags & FileBrowserComponent::openMode) != 0)));
#if JUCE_WINDOWS
const bool selectsFiles = (flags & FileBrowserComponent::canSelectFiles) != 0;
const bool selectsDirectories = (flags & FileBrowserComponent::canSelectDirectories) != 0;
if (useNativeDialogBox && ! (selectsFiles && selectsDirectories))
#else
if (useNativeDialogBox)
#endif
{
return showPlatformDialog (*this, flags, previewComp);
}
return std::make_unique<NonNative> (*this, flags, previewComp);
}
Array<File> FileChooser::getResults() const noexcept
{
Array<File> files;
for (auto url : getURLResults())
if (url.isLocalFile())
files.add (url.getLocalFile());
return files;
}
File FileChooser::getResult() const
{
auto fileResults = getResults();
// if you've used a multiple-file select, you should use the getResults() method
// to retrieve all the files that were chosen.
jassert (fileResults.size() <= 1);
return fileResults.getFirst();
}
URL FileChooser::getURLResult() const
{
// if you've used a multiple-file select, you should use the getResults() method
// to retrieve all the files that were chosen.
jassert (results.size() <= 1);
return results.getFirst();
}
void FileChooser::finished (const Array<URL>& asyncResults)
{
const auto callback = std::exchange (asyncCallback, nullptr);
results = asyncResults;
pimpl.reset();
if (callback)
callback (*this);
}
#if ! JUCE_ANDROID
void FileChooser::registerCustomMimeTypeForFileExtension ([[maybe_unused]] const String& mimeType,
[[maybe_unused]] const String& fileExtension)
{
}
#endif
//==============================================================================
FilePreviewComponent::FilePreviewComponent() {}
FilePreviewComponent::~FilePreviewComponent() {}
} // namespace juce
@@ -0,0 +1,349 @@
/*
==============================================================================
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
{
//==============================================================================
/**
Creates a dialog box to choose a file or directory to load or save.
@code
std::unique_ptr<FileChooser> myChooser;
void loadMooseFile()
{
myChooser = std::make_unique<FileChooser> ("Please select the moose you want to load...",
File::getSpecialLocation (File::userHomeDirectory),
"*.moose");
auto folderChooserFlags = FileBrowserComponent::openMode | FileBrowserComponent::canSelectDirectories;
myChooser->launchAsync (folderChooserFlags, [this] (const FileChooser& chooser)
{
File mooseFile (chooser.getResult());
loadMoose (mooseFile);
});
}
@endcode
@tags{GUI}
*/
class JUCE_API FileChooser
{
public:
//==============================================================================
/** Creates a FileChooser.
After creating one of these, use one of the browseFor... methods to display it.
@param dialogBoxTitle a text string to display in the dialog box to
tell the user what's going on
@param initialFileOrDirectory the file or directory that should be selected
when the dialog box opens. If this parameter is
set to File(), a sensible default directory will
be used instead. When using native dialogs, not
all platforms will actually select the file. For
example, on macOS, when initialFileOrDirectory is
a file, only the parent directory of
initialFileOrDirectory will be used as the initial
directory of the native file chooser.
Note: On iOS when saving a file, a user will not
be able to change a file name, so it may be a good
idea to include at least a valid file name in
initialFileOrDirectory. When no filename is found,
"Untitled" will be used.
Also, if you pass an already existing file on iOS,
that file will be automatically copied to the
destination chosen by user and if it can be previewed,
its preview will be presented in the dialog too. You
will still be able to write into this file copy, since
its URL will be returned by getURLResult(). This can be
useful when you want to save e.g. an image, so that
you can pass a (temporary) file with low quality
preview and after the user picks the destination,
you can write a high quality image into the copied
file. If you create such a temporary file, you need
to delete it yourself, once it is not needed anymore.
@param filePatternsAllowed a set of file patterns to specify which files can be
selected - each pattern should be separated by a comma or
semi-colon, e.g. "*" or "*.jpg;*.gif". The native MacOS
file browser only supports wildcard that specify
extensions, so "*.jpg" is OK but "myfilename*" will not
work. An empty string means that all files are allowed
@param useOSNativeDialogBox if true, then a native dialog box will be used
if possible; if false, then a Juce-based
browser dialog box will always be used
@param treatFilePackagesAsDirectories if true, then the file chooser will allow the
selection of files inside packages when
invoked on OS X and when using native dialog
boxes.
@param parentComponent An optional component which should be the parent
for the file chooser. If this is a nullptr then the
FileChooser will be a top-level window. AUv3s on iOS
must specify this parameter as opening a top-level window
in an AUv3 is forbidden due to sandbox restrictions.
@see browseForFileToOpen, browseForFileToSave, browseForDirectory
*/
FileChooser (const String& dialogBoxTitle,
const File& initialFileOrDirectory = File(),
const String& filePatternsAllowed = String(),
bool useOSNativeDialogBox = true,
bool treatFilePackagesAsDirectories = false,
Component* parentComponent = nullptr);
/** Destructor. */
~FileChooser();
//==============================================================================
#if JUCE_MODAL_LOOPS_PERMITTED
/** Shows a dialog box to choose a file to open.
This will display the dialog box modally, using an "open file" mode, so that
it won't allow non-existent files or directories to be chosen.
@param previewComponent an optional component to display inside the dialog
box to show special info about the files that the user
is browsing. The component will not be deleted by this
object, so the caller must take care of it.
@returns true if the user selected a file, in which case, use the getResult()
method to find out what it was. Returns false if they cancelled instead.
@see browseForFileToSave, browseForDirectory
*/
bool browseForFileToOpen (FilePreviewComponent* previewComponent = nullptr);
/** Same as browseForFileToOpen, but allows the user to select multiple files.
The files that are returned can be obtained by calling getResults(). See
browseForFileToOpen() for more info about the behaviour of this method.
*/
bool browseForMultipleFilesToOpen (FilePreviewComponent* previewComponent = nullptr);
/** Shows a dialog box to choose a file to save.
This will display the dialog box modally, using an "save file" mode, so it
will allow non-existent files to be chosen, but not directories.
@param warnAboutOverwritingExistingFiles if true, the dialog box will ask
the user if they're sure they want to overwrite a file that already
exists
@returns true if the user chose a file and pressed 'ok', in which case, use
the getResult() method to find out what the file was. Returns false
if they cancelled instead.
@see browseForFileToOpen, browseForDirectory
*/
bool browseForFileToSave (bool warnAboutOverwritingExistingFiles);
/** Shows a dialog box to choose a directory.
This will display the dialog box modally, using an "open directory" mode, so it
will only allow directories to be returned, not files.
@returns true if the user chose a directory and pressed 'ok', in which case, use
the getResult() method to find out what they chose. Returns false
if they cancelled instead.
@see browseForFileToOpen, browseForFileToSave
*/
bool browseForDirectory();
/** Same as browseForFileToOpen, but allows the user to select multiple files and directories.
The files that are returned can be obtained by calling getResults(). See
browseForFileToOpen() for more info about the behaviour of this method.
*/
bool browseForMultipleFilesOrDirectories (FilePreviewComponent* previewComponent = nullptr);
//==============================================================================
/** Runs a dialog box for the given set of option flags.
The flag values used are those in FileBrowserComponent::FileChooserFlags.
@returns true if the user chose a directory and pressed 'ok', in which case, use
the getResult() method to find out what they chose. Returns false
if they cancelled instead.
@see FileBrowserComponent::FileChooserFlags
*/
bool showDialog (int flags, FilePreviewComponent* previewComponent);
#endif
/** Use this method to launch the file browser window asynchronously.
This will create a file browser dialog based on the settings in this
structure and will launch it modally, returning immediately.
You must specify a callback which is called when the file browser is
cancelled or a file is selected. To abort the file selection, simply
delete the FileChooser object.
You must ensure that the lifetime of the callback object is longer than
the lifetime of the file-chooser.
*/
void launchAsync (int flags,
std::function<void (const FileChooser&)>,
FilePreviewComponent* previewComponent = nullptr);
//==============================================================================
/** Returns the last file that was chosen by one of the browseFor methods.
After calling the appropriate browseFor... method, this method lets you
find out what file or directory they chose.
Note that the file returned is only valid if the browse method returned true (i.e.
if the user pressed 'ok' rather than cancelling).
On mobile platforms, the file browser may return a URL instead of a local file.
Therefore, on mobile platforms, you should call getURLResult() instead.
If you're using a multiple-file select, then use the getResults() method instead,
to obtain the list of all files chosen.
@see getURLResult, getResults
*/
File getResult() const;
/** Returns a list of all the files that were chosen during the last call to a
browse method.
On mobile platforms, the file browser may return a URL instead of a local file.
Therefore, on mobile platforms, you should call getURLResults() instead.
This array may be empty if no files were chosen, or can contain multiple entries
if multiple files were chosen.
@see getURLResults, getResult
*/
Array<File> getResults() const noexcept;
//==============================================================================
/** Returns the last document that was chosen by one of the browseFor methods.
Use this method if you are using the FileChooser on a mobile platform which
may return a URL to a remote document. If a local file is chosen then you can
convert this file to a JUCE File class via the URL::getLocalFile method.
Note: On iOS you must use the returned URL object directly (you are also
allowed to copy- or move-construct another URL from the returned URL), rather
than just storing the path as a String and then creating a new URL from that
String. This is because the returned URL contains internally a security
bookmark that is required to access the files pointed by it. Then, once you stop
dealing with the file pointed by the URL, you should dispose that URL object,
so that the security bookmark can be released by the system (only a limited
number of such URLs is allowed).
@see getResult, URL::getLocalFile
*/
URL getURLResult() const;
/** Returns a list of all the files that were chosen during the last call to a
browse method.
Use this method if you are using the FileChooser on a mobile platform which
may return a URL to a remote document. If a local file is chosen then you can
convert this file to a JUCE File class via the URL::getLocalFile method.
This array may be empty if no files were chosen, or can contain multiple entries
if multiple files were chosen.
Note: On iOS you must use the returned URL object directly (you are also
allowed to copy- or move-construct another URL from the returned URL), rather
than just storing the path as a String and then creating a new URL from that
String. This is because the returned URL contains internally a security
bookmark that is required to access the files pointed by it. Then, once you stop
dealing with the file pointed by the URL, you should dispose that URL object,
so that the security bookmark can be released by the system (only a limited
number of such URLs is allowed).
@see getResults, URL::getLocalFile
*/
const Array<URL>& getURLResults() const noexcept { return results; }
//==============================================================================
/** Returns if a native filechooser is currently available on this platform.
Note: On iOS this will only return true if you have iCloud permissions
and code-signing enabled in the Projucer and have added iCloud containers
to your app in Apple's online developer portal. Additionally, the user must
have installed the iCloud app on their device and used the app at least once.
*/
static bool isPlatformDialogAvailable();
/** Associate a particular file-extension to a mime-type
On Android, JUCE needs to convert common file extensions to mime-types when using
wildcard filters in native file chooser dialog boxes. JUCE has an extensive conversion
table to convert between the most common file-types and mime-types transparently, but
some more obscure file-types may be missing. Use this method to register your own
mime-type to file extension conversions. Please contact the JUCE team if you think
that a common mime-type/file-extension entry is missing in JUCE's internal tables.
Does nothing on other platforms.
*/
static void registerCustomMimeTypeForFileExtension (const String& mimeType,
const String& fileExtension);
//==============================================================================
#ifndef DOXYGEN
class Native;
#endif
private:
//==============================================================================
String title, filters;
File startingFile;
Component* parent;
Array<URL> results;
const bool useNativeDialogBox;
const bool treatFilePackagesAsDirs;
std::function<void (const FileChooser&)> asyncCallback;
//==============================================================================
void finished (const Array<URL>&);
//==============================================================================
struct Pimpl
{
virtual ~Pimpl() = default;
virtual void launch() = 0;
virtual void runModally() = 0;
};
std::shared_ptr<Pimpl> pimpl;
//==============================================================================
std::shared_ptr<Pimpl> createPimpl (int, FilePreviewComponent*);
static std::shared_ptr<Pimpl> showPlatformDialog (FileChooser&, int, FilePreviewComponent*);
class NonNative;
friend class NonNative;
friend class Native;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileChooser)
};
} // namespace juce

Some files were not shown because too many files have changed in this diff Show More