mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-13 00:04:19 +00:00
Previously, individual components had to ask the peer to hide and show the keyboard, by calling textInputRequired() and dismissPendingTextInput() respectively. When an onscreen keyboard (OSK) was required, most Peer implementation would directly hide/show the OSK inside these function. However, the iOS ComponentPeer implementation instead listened to the application's global keyboard focus, and only opened the OSK when the focused component was also a TextInputTarget with active input. The iOS scheme seems like a better design, as it enforces that the OSK hiding and showing is synced with the keyboard focus of the application. In the other implementations, it was possible for a Component to call textInputRequired even when it didn't have the keyboard focus, putting the application into an inconsistent state. The iOS scheme also makes the TextInputTarget interface more useful, as it enforces that the OSK will only display for components that implement TextInputTarget, and return true from isTextInputActive(). This patch changes all Peer implementations to match the iOS implementation, improving consistency. Each time the global keyboard focus changes, refreshTextInputTarget is called automatically, and the OSK is shown if the focused component is a TextInputTarget that returns true from isTextInputActive, and hidden otherwise. Components can also call refreshTextInputTarget manually. This should be done whenever the component updates the return value of isTextInputActive(). Effectively, the Peer is now responsible for keeping track of the focused TextInputTarget, rather than allowing individual components to hide and show the OSK at will. Additionally, this patch adds an option to the TextEditor to automatically dismiss the OSK when the mouse is clicked outside of the editor. This should improve user experience on mobile platforms, where touches on sibling components may cause a TextEditor to gain keyboard focus and unnecessarily display the OSK.
586 lines
16 KiB
C++
586 lines
16 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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
|
|
{
|
|
|
|
Label::Label (const String& name, const String& labelText)
|
|
: Component (name),
|
|
textValue (labelText),
|
|
lastTextValue (labelText)
|
|
{
|
|
setColour (TextEditor::textColourId, Colours::black);
|
|
setColour (TextEditor::backgroundColourId, Colours::transparentBlack);
|
|
setColour (TextEditor::outlineColourId, Colours::transparentBlack);
|
|
|
|
textValue.addListener (this);
|
|
}
|
|
|
|
Label::~Label()
|
|
{
|
|
textValue.removeListener (this);
|
|
|
|
if (ownerComponent != nullptr)
|
|
ownerComponent->removeComponentListener (this);
|
|
|
|
editor.reset();
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::setText (const String& newText, NotificationType notification)
|
|
{
|
|
hideEditor (true);
|
|
|
|
if (lastTextValue != newText)
|
|
{
|
|
lastTextValue = newText;
|
|
textValue = newText;
|
|
repaint();
|
|
|
|
textWasChanged();
|
|
|
|
if (ownerComponent != nullptr)
|
|
componentMovedOrResized (*ownerComponent, true, true);
|
|
|
|
if (notification != dontSendNotification)
|
|
callChangeListeners();
|
|
}
|
|
}
|
|
|
|
String Label::getText (bool returnActiveEditorContents) const
|
|
{
|
|
return (returnActiveEditorContents && isBeingEdited())
|
|
? editor->getText()
|
|
: textValue.toString();
|
|
}
|
|
|
|
void Label::valueChanged (Value&)
|
|
{
|
|
if (lastTextValue != textValue.toString())
|
|
setText (textValue.toString(), sendNotification);
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::setFont (const Font& newFont)
|
|
{
|
|
if (font != newFont)
|
|
{
|
|
font = newFont;
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
Font Label::getFont() const noexcept
|
|
{
|
|
return font;
|
|
}
|
|
|
|
void Label::setEditable (bool editOnSingleClick,
|
|
bool editOnDoubleClick,
|
|
bool lossOfFocusDiscards)
|
|
{
|
|
editSingleClick = editOnSingleClick;
|
|
editDoubleClick = editOnDoubleClick;
|
|
lossOfFocusDiscardsChanges = lossOfFocusDiscards;
|
|
|
|
const auto isKeybordFocusable = (editOnSingleClick || editOnDoubleClick);
|
|
|
|
setWantsKeyboardFocus (isKeybordFocusable);
|
|
setFocusContainerType (isKeybordFocusable ? FocusContainerType::keyboardFocusContainer
|
|
: FocusContainerType::none);
|
|
|
|
invalidateAccessibilityHandler();
|
|
}
|
|
|
|
void Label::setJustificationType (Justification newJustification)
|
|
{
|
|
if (justification != newJustification)
|
|
{
|
|
justification = newJustification;
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
void Label::setBorderSize (BorderSize<int> newBorder)
|
|
{
|
|
if (border != newBorder)
|
|
{
|
|
border = newBorder;
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
Component* Label::getAttachedComponent() const
|
|
{
|
|
return ownerComponent.get();
|
|
}
|
|
|
|
void Label::attachToComponent (Component* owner, bool onLeft)
|
|
{
|
|
jassert (owner != this); // Not a great idea to try to attach it to itself!
|
|
|
|
if (ownerComponent != nullptr)
|
|
ownerComponent->removeComponentListener (this);
|
|
|
|
ownerComponent = owner;
|
|
leftOfOwnerComp = onLeft;
|
|
|
|
if (ownerComponent != nullptr)
|
|
{
|
|
setVisible (ownerComponent->isVisible());
|
|
ownerComponent->addComponentListener (this);
|
|
componentParentHierarchyChanged (*ownerComponent);
|
|
componentMovedOrResized (*ownerComponent, true, true);
|
|
}
|
|
}
|
|
|
|
void Label::componentMovedOrResized (Component& component, bool /*wasMoved*/, bool /*wasResized*/)
|
|
{
|
|
auto& lf = getLookAndFeel();
|
|
auto f = lf.getLabelFont (*this);
|
|
auto borderSize = lf.getLabelBorderSize (*this);
|
|
|
|
if (leftOfOwnerComp)
|
|
{
|
|
auto width = jmin (roundToInt (f.getStringWidthFloat (textValue.toString()) + 0.5f)
|
|
+ borderSize.getLeftAndRight(),
|
|
component.getX());
|
|
|
|
setBounds (component.getX() - width, component.getY(), width, component.getHeight());
|
|
}
|
|
else
|
|
{
|
|
auto height = borderSize.getTopAndBottom() + 6 + roundToInt (f.getHeight() + 0.5f);
|
|
|
|
setBounds (component.getX(), component.getY() - height, component.getWidth(), height);
|
|
}
|
|
}
|
|
|
|
void Label::componentParentHierarchyChanged (Component& component)
|
|
{
|
|
if (auto* parent = component.getParentComponent())
|
|
parent->addChildComponent (this);
|
|
}
|
|
|
|
void Label::componentVisibilityChanged (Component& component)
|
|
{
|
|
setVisible (component.isVisible());
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::textWasEdited() {}
|
|
void Label::textWasChanged() {}
|
|
|
|
void Label::editorShown (TextEditor* textEditor)
|
|
{
|
|
Component::BailOutChecker checker (this);
|
|
listeners.callChecked (checker, [this, textEditor] (Label::Listener& l) { l.editorShown (this, *textEditor); });
|
|
|
|
if (checker.shouldBailOut())
|
|
return;
|
|
|
|
if (onEditorShow != nullptr)
|
|
onEditorShow();
|
|
}
|
|
|
|
void Label::editorAboutToBeHidden (TextEditor* textEditor)
|
|
{
|
|
Component::BailOutChecker checker (this);
|
|
listeners.callChecked (checker, [this, textEditor] (Label::Listener& l) { l.editorHidden (this, *textEditor); });
|
|
|
|
if (checker.shouldBailOut())
|
|
return;
|
|
|
|
if (onEditorHide != nullptr)
|
|
onEditorHide();
|
|
}
|
|
|
|
void Label::showEditor()
|
|
{
|
|
if (editor == nullptr)
|
|
{
|
|
editor.reset (createEditorComponent());
|
|
editor->setSize (10, 10);
|
|
addAndMakeVisible (editor.get());
|
|
editor->setText (getText(), false);
|
|
editor->setKeyboardType (keyboardType);
|
|
editor->addListener (this);
|
|
editor->grabKeyboardFocus();
|
|
|
|
if (editor == nullptr) // may be deleted by a callback
|
|
return;
|
|
|
|
editor->setHighlightedRegion (Range<int> (0, textValue.toString().length()));
|
|
|
|
resized();
|
|
repaint();
|
|
|
|
editorShown (editor.get());
|
|
|
|
enterModalState (false);
|
|
editor->grabKeyboardFocus();
|
|
}
|
|
}
|
|
|
|
bool Label::updateFromTextEditorContents (TextEditor& ed)
|
|
{
|
|
auto newText = ed.getText();
|
|
|
|
if (textValue.toString() != newText)
|
|
{
|
|
lastTextValue = newText;
|
|
textValue = newText;
|
|
repaint();
|
|
|
|
textWasChanged();
|
|
|
|
if (ownerComponent != nullptr)
|
|
componentMovedOrResized (*ownerComponent, true, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Label::hideEditor (bool discardCurrentEditorContents)
|
|
{
|
|
if (editor != nullptr)
|
|
{
|
|
WeakReference<Component> deletionChecker (this);
|
|
std::unique_ptr<TextEditor> outgoingEditor;
|
|
std::swap (outgoingEditor, editor);
|
|
|
|
editorAboutToBeHidden (outgoingEditor.get());
|
|
|
|
const bool changed = (! discardCurrentEditorContents)
|
|
&& updateFromTextEditorContents (*outgoingEditor);
|
|
outgoingEditor.reset();
|
|
|
|
if (deletionChecker != nullptr)
|
|
repaint();
|
|
|
|
if (changed)
|
|
textWasEdited();
|
|
|
|
if (deletionChecker != nullptr)
|
|
exitModalState (0);
|
|
|
|
if (changed && deletionChecker != nullptr)
|
|
callChangeListeners();
|
|
}
|
|
}
|
|
|
|
void Label::inputAttemptWhenModal()
|
|
{
|
|
if (editor != nullptr)
|
|
{
|
|
if (lossOfFocusDiscardsChanges)
|
|
textEditorEscapeKeyPressed (*editor);
|
|
else
|
|
textEditorReturnKeyPressed (*editor);
|
|
}
|
|
}
|
|
|
|
bool Label::isBeingEdited() const noexcept
|
|
{
|
|
return editor != nullptr;
|
|
}
|
|
|
|
static void copyColourIfSpecified (Label& l, TextEditor& ed, int colourID, int targetColourID)
|
|
{
|
|
if (l.isColourSpecified (colourID) || l.getLookAndFeel().isColourSpecified (colourID))
|
|
ed.setColour (targetColourID, l.findColour (colourID));
|
|
}
|
|
|
|
TextEditor* Label::createEditorComponent()
|
|
{
|
|
auto* ed = new TextEditor (getName());
|
|
ed->applyFontToAllText (getLookAndFeel().getLabelFont (*this));
|
|
copyAllExplicitColoursTo (*ed);
|
|
|
|
copyColourIfSpecified (*this, *ed, textWhenEditingColourId, TextEditor::textColourId);
|
|
copyColourIfSpecified (*this, *ed, backgroundWhenEditingColourId, TextEditor::backgroundColourId);
|
|
copyColourIfSpecified (*this, *ed, outlineWhenEditingColourId, TextEditor::focusedOutlineColourId);
|
|
|
|
return ed;
|
|
}
|
|
|
|
TextEditor* Label::getCurrentTextEditor() const noexcept
|
|
{
|
|
return editor.get();
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::paint (Graphics& g)
|
|
{
|
|
getLookAndFeel().drawLabel (g, *this);
|
|
}
|
|
|
|
void Label::mouseUp (const MouseEvent& e)
|
|
{
|
|
if (editSingleClick
|
|
&& isEnabled()
|
|
&& contains (e.getPosition())
|
|
&& ! (e.mouseWasDraggedSinceMouseDown() || e.mods.isPopupMenu()))
|
|
{
|
|
showEditor();
|
|
}
|
|
}
|
|
|
|
void Label::mouseDoubleClick (const MouseEvent& e)
|
|
{
|
|
if (editDoubleClick
|
|
&& isEnabled()
|
|
&& ! e.mods.isPopupMenu())
|
|
{
|
|
showEditor();
|
|
}
|
|
}
|
|
|
|
void Label::resized()
|
|
{
|
|
if (editor != nullptr)
|
|
editor->setBounds (getLocalBounds());
|
|
}
|
|
|
|
void Label::focusGained (FocusChangeType cause)
|
|
{
|
|
if (editSingleClick
|
|
&& isEnabled()
|
|
&& cause == focusChangedByTabKey)
|
|
{
|
|
showEditor();
|
|
}
|
|
}
|
|
|
|
void Label::enablementChanged()
|
|
{
|
|
repaint();
|
|
}
|
|
|
|
void Label::colourChanged()
|
|
{
|
|
repaint();
|
|
}
|
|
|
|
void Label::setMinimumHorizontalScale (const float newScale)
|
|
{
|
|
if (minimumHorizontalScale != newScale)
|
|
{
|
|
minimumHorizontalScale = newScale;
|
|
repaint();
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
// We'll use a custom focus traverser here to make sure focus goes from the
|
|
// text editor to another component rather than back to the label itself.
|
|
class LabelKeyboardFocusTraverser : public KeyboardFocusTraverser
|
|
{
|
|
public:
|
|
explicit LabelKeyboardFocusTraverser (Label& l) : owner (l) {}
|
|
|
|
Component* getDefaultComponent (Component* parent) override
|
|
{
|
|
if (auto* container = getKeyboardFocusContainer (parent))
|
|
return KeyboardFocusTraverser::getDefaultComponent (container);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Component* getNextComponent (Component* c) override { return KeyboardFocusTraverser::getNextComponent (getComp (c)); }
|
|
Component* getPreviousComponent (Component* c) override { return KeyboardFocusTraverser::getPreviousComponent (getComp (c)); }
|
|
|
|
std::vector<Component*> getAllComponents (Component* parent) override
|
|
{
|
|
if (auto* container = getKeyboardFocusContainer (parent))
|
|
return KeyboardFocusTraverser::getAllComponents (container);
|
|
|
|
return {};
|
|
}
|
|
|
|
private:
|
|
Component* getComp (Component* current) const
|
|
{
|
|
if (auto* ed = owner.getCurrentTextEditor())
|
|
if (current == ed)
|
|
return current->getParentComponent();
|
|
|
|
return current;
|
|
}
|
|
|
|
Component* getKeyboardFocusContainer (Component* parent) const
|
|
{
|
|
if (owner.getCurrentTextEditor() != nullptr && parent == &owner)
|
|
return owner.findKeyboardFocusContainer();
|
|
|
|
return parent;
|
|
}
|
|
|
|
Label& owner;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelKeyboardFocusTraverser)
|
|
};
|
|
|
|
std::unique_ptr<ComponentTraverser> Label::createKeyboardFocusTraverser()
|
|
{
|
|
return std::make_unique<LabelKeyboardFocusTraverser> (*this);
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::addListener (Label::Listener* l) { listeners.add (l); }
|
|
void Label::removeListener (Label::Listener* l) { listeners.remove (l); }
|
|
|
|
void Label::callChangeListeners()
|
|
{
|
|
Component::BailOutChecker checker (this);
|
|
listeners.callChecked (checker, [this] (Listener& l) { l.labelTextChanged (this); });
|
|
|
|
if (checker.shouldBailOut())
|
|
return;
|
|
|
|
if (onTextChange != nullptr)
|
|
onTextChange();
|
|
}
|
|
|
|
//==============================================================================
|
|
void Label::textEditorTextChanged (TextEditor& ed)
|
|
{
|
|
if (editor != nullptr)
|
|
{
|
|
jassert (&ed == editor.get());
|
|
|
|
if (! (hasKeyboardFocus (true) || isCurrentlyBlockedByAnotherModalComponent()))
|
|
{
|
|
if (lossOfFocusDiscardsChanges)
|
|
textEditorEscapeKeyPressed (ed);
|
|
else
|
|
textEditorReturnKeyPressed (ed);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Label::textEditorReturnKeyPressed (TextEditor& ed)
|
|
{
|
|
if (editor != nullptr)
|
|
{
|
|
jassert (&ed == editor.get());
|
|
|
|
WeakReference<Component> deletionChecker (this);
|
|
bool changed = updateFromTextEditorContents (ed);
|
|
hideEditor (true);
|
|
|
|
if (changed && deletionChecker != nullptr)
|
|
{
|
|
textWasEdited();
|
|
|
|
if (deletionChecker != nullptr)
|
|
callChangeListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Label::textEditorEscapeKeyPressed (TextEditor& ed)
|
|
{
|
|
if (editor != nullptr)
|
|
{
|
|
jassertquiet (&ed == editor.get());
|
|
|
|
editor->setText (textValue.toString(), false);
|
|
hideEditor (true);
|
|
}
|
|
}
|
|
|
|
void Label::textEditorFocusLost (TextEditor& ed)
|
|
{
|
|
textEditorTextChanged (ed);
|
|
}
|
|
|
|
//==============================================================================
|
|
class LabelAccessibilityHandler : public AccessibilityHandler
|
|
{
|
|
public:
|
|
explicit LabelAccessibilityHandler (Label& labelToWrap)
|
|
: AccessibilityHandler (labelToWrap,
|
|
labelToWrap.isEditable() ? AccessibilityRole::editableText : AccessibilityRole::label,
|
|
getAccessibilityActions (labelToWrap),
|
|
{ std::make_unique<LabelValueInterface> (labelToWrap) }),
|
|
label (labelToWrap)
|
|
{
|
|
}
|
|
|
|
String getTitle() const override { return label.getText(); }
|
|
String getHelp() const override { return label.getTooltip(); }
|
|
|
|
AccessibleState getCurrentState() const override
|
|
{
|
|
if (label.isBeingEdited())
|
|
return {}; // allow focus to pass through to the TextEditor
|
|
|
|
return AccessibilityHandler::getCurrentState();
|
|
}
|
|
|
|
private:
|
|
class LabelValueInterface : public AccessibilityTextValueInterface
|
|
{
|
|
public:
|
|
explicit LabelValueInterface (Label& labelToWrap)
|
|
: label (labelToWrap)
|
|
{
|
|
}
|
|
|
|
bool isReadOnly() const override { return true; }
|
|
String getCurrentValueAsString() const override { return label.getText(); }
|
|
void setValueAsString (const String&) override {}
|
|
|
|
private:
|
|
Label& label;
|
|
|
|
//==============================================================================
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelValueInterface)
|
|
};
|
|
|
|
static AccessibilityActions getAccessibilityActions (Label& label)
|
|
{
|
|
if (label.isEditable())
|
|
return AccessibilityActions().addAction (AccessibilityActionType::press, [&label] { label.showEditor(); });
|
|
|
|
return {};
|
|
}
|
|
|
|
Label& label;
|
|
|
|
//==============================================================================
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LabelAccessibilityHandler)
|
|
};
|
|
|
|
std::unique_ptr<AccessibilityHandler> Label::createAccessibilityHandler()
|
|
{
|
|
return std::make_unique<LabelAccessibilityHandler> (*this);
|
|
}
|
|
|
|
} // namespace juce
|