Browse Source

TextEditor: Add option to dismiss the virtual keyboard on touches outside

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.
pull/22/head
reuk 3 years ago
parent
commit
140f8fedb1
No known key found for this signature in database GPG Key ID: 9ADCD339CFC98A11
9 changed files with 131 additions and 63 deletions
  1. +8
    -0
      modules/juce_gui_basics/components/juce_Component.cpp
  2. +2
    -0
      modules/juce_gui_basics/native/juce_android_Windowing.cpp
  3. +18
    -31
      modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm
  4. +7
    -2
      modules/juce_gui_basics/native/juce_win32_Windowing.cpp
  5. +0
    -3
      modules/juce_gui_basics/widgets/juce_Label.cpp
  6. +29
    -13
      modules/juce_gui_basics/widgets/juce_TextEditor.cpp
  7. +18
    -0
      modules/juce_gui_basics/widgets/juce_TextEditor.h
  8. +22
    -1
      modules/juce_gui_basics/windows/juce_ComponentPeer.cpp
  9. +27
    -13
      modules/juce_gui_basics/windows/juce_ComponentPeer.h

+ 8
- 0
modules/juce_gui_basics/components/juce_Component.cpp View File

@@ -2943,6 +2943,11 @@ void Component::takeKeyboardFocus (FocusChangeType cause)
return;
WeakReference<Component> componentLosingFocus (currentlyFocusedComponent);
if (auto* losingFocus = componentLosingFocus.get())
if (auto* otherPeer = losingFocus->getPeer())
otherPeer->closeInputMethodContext();
currentlyFocusedComponent = this;
Desktop::getInstance().triggerFocusCallback();
@@ -3008,6 +3013,9 @@ void Component::giveAwayKeyboardFocusInternal (bool sendFocusLossEvent)
{
if (auto* componentLosingFocus = currentlyFocusedComponent)
{
if (auto* otherPeer = componentLosingFocus->getPeer())
otherPeer->closeInputMethodContext();
currentlyFocusedComponent = nullptr;
if (sendFocusLossEvent && componentLosingFocus != nullptr)


+ 2
- 0
modules/juce_gui_basics/native/juce_android_Windowing.cpp View File

@@ -942,6 +942,8 @@ public:
void dismissPendingTextInput() override
{
closeInputMethodContext();
view.callVoidMethod (ComponentPeerView.showKeyboard, javaString ("").get());
if (! isTimerRunning())


+ 18
- 31
modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm View File

@@ -213,7 +213,6 @@ struct UIViewPeerControllerReceiver
};
class UIViewComponentPeer : public ComponentPeer,
private FocusChangeListener,
private UIViewPeerControllerReceiver
{
public:
@@ -264,10 +263,10 @@ public:
bool isFocused() const override;
void grabFocus() override;
void textInputRequired (Point<int>, TextInputTarget&) override;
void dismissPendingTextInput() override;
BOOL textViewReplaceCharacters (Range<int>, const String&);
void updateHiddenTextContent (TextInputTarget*);
void globalFocusChanged (Component*) override;
void updateHiddenTextContent (TextInputTarget&);
void updateScreenBounds();
@@ -754,8 +753,6 @@ UIViewComponentPeer::UIViewComponentPeer (Component& comp, int windowStyleFlags,
setTitle (component.getName());
setVisible (component.isVisible());
Desktop::getInstance().addFocusChangeListener (this);
}
static UIViewComponentPeer* currentlyFocusedPeer = nullptr;
@@ -766,7 +763,6 @@ UIViewComponentPeer::~UIViewComponentPeer()
currentlyFocusedPeer = nullptr;
currentTouches.deleteAllTouchesForPeer (this);
Desktop::getInstance().removeFocusChangeListener (this);
view->owner = nullptr;
[view removeFromSuperview];
@@ -1130,8 +1126,18 @@ void UIViewComponentPeer::grabFocus()
}
}
void UIViewComponentPeer::textInputRequired (Point<int>, TextInputTarget&)
void UIViewComponentPeer::textInputRequired (Point<int> pos, TextInputTarget& target)
{
view->hiddenTextView.frame = CGRectMake (pos.x, pos.y, 0, 0);
updateHiddenTextContent (target);
[view->hiddenTextView becomeFirstResponder];
}
void UIViewComponentPeer::dismissPendingTextInput()
{
closeInputMethodContext();
[view->hiddenTextView resignFirstResponder];
}
static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType type) noexcept
@@ -1150,11 +1156,11 @@ static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType ty
return UIKeyboardTypeDefault;
}
void UIViewComponentPeer::updateHiddenTextContent (TextInputTarget* target)
void UIViewComponentPeer::updateHiddenTextContent (TextInputTarget& target)
{
view->hiddenTextView.keyboardType = getUIKeyboardType (target->getKeyboardType());
view->hiddenTextView.text = juceStringToNS (target->getTextInRange (Range<int> (0, target->getHighlightedRegion().getStart())));
view->hiddenTextView.selectedRange = NSMakeRange ((NSUInteger) target->getHighlightedRegion().getStart(), 0);
view->hiddenTextView.keyboardType = getUIKeyboardType (target.getKeyboardType());
view->hiddenTextView.text = juceStringToNS (target.getTextInRange (Range<int> (0, target.getHighlightedRegion().getStart())));
view->hiddenTextView.selectedRange = NSMakeRange ((NSUInteger) target.getHighlightedRegion().getStart(), 0);
}
BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const String& text)
@@ -1175,31 +1181,12 @@ BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const Str
target->insertTextAtCaret (text);
if (deletionChecker != nullptr)
updateHiddenTextContent (target);
updateHiddenTextContent (*target);
}
return NO;
}
void UIViewComponentPeer::globalFocusChanged (Component*)
{
if (auto* target = findCurrentTextInputTarget())
{
if (auto* comp = dynamic_cast<Component*> (target))
{
auto pos = component.getLocalPoint (comp, Point<int>());
view->hiddenTextView.frame = CGRectMake (pos.x, pos.y, 0, 0);
updateHiddenTextContent (target);
[view->hiddenTextView becomeFirstResponder];
}
}
else
{
[view->hiddenTextView resignFirstResponder];
}
}
//==============================================================================
void UIViewComponentPeer::displayLinkCallback()
{


+ 7
- 2
modules/juce_gui_basics/native/juce_win32_Windowing.cpp View File

@@ -4401,13 +4401,18 @@ private:
{
if (compositionInProgress && ! windowIsActive)
{
compositionInProgress = false;
if (HIMC hImc = ImmGetContext (hWnd))
{
ImmNotifyIME (hImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
ImmReleaseContext (hWnd, hImc);
}
// If the composition is still in progress, calling ImmNotifyIME may call back
// into handleComposition to let us know that the composition has finished.
// We need to set compositionInProgress *after* calling handleComposition, so that
// the text replaces the current selection, rather than being inserted after the
// caret.
compositionInProgress = false;
}
}


+ 0
- 3
modules/juce_gui_basics/widgets/juce_Label.cpp View File

@@ -208,9 +208,6 @@ void Label::editorShown (TextEditor* textEditor)
void Label::editorAboutToBeHidden (TextEditor* textEditor)
{
if (auto* peer = getPeer())
peer->dismissPendingTextInput();
Component::BailOutChecker checker (this);
listeners.callChecked (checker, [this, textEditor] (Label::Listener& l) { l.editorHidden (this, *textEditor); });


+ 29
- 13
modules/juce_gui_basics/widgets/juce_TextEditor.cpp View File

@@ -933,13 +933,13 @@ TextEditor::TextEditor (const String& name, juce_wchar passwordChar)
setWantsKeyboardFocus (true);
recreateCaret();
juce::Desktop::getInstance().addGlobalMouseListener (this);
}
TextEditor::~TextEditor()
{
if (wasFocused)
if (auto* peer = getPeer())
peer->dismissPendingTextInput();
juce::Desktop::getInstance().removeGlobalMouseListener (this);
textValue.removeListener (textHolder);
textValue.referTo (Value());
@@ -1017,9 +1017,17 @@ void TextEditor::setReadOnly (bool shouldBeReadOnly)
readOnly = shouldBeReadOnly;
enablementChanged();
invalidateAccessibilityHandler();
if (auto* peer = getPeer())
peer->refreshTextInputTarget();
}
}
void TextEditor::setClicksOutsideDismissVirtualKeyboard (bool newValue)
{
clicksOutsideDismissVirtualKeyboard = newValue;
}
bool TextEditor::isReadOnly() const noexcept
{
return readOnly || ! isEnabled();
@@ -1027,7 +1035,7 @@ bool TextEditor::isReadOnly() const noexcept
bool TextEditor::isTextInputActive() const
{
return ! isReadOnly();
return ! isReadOnly() && (! clicksOutsideDismissVirtualKeyboard || mouseDownInEditor);
}
void TextEditor::setReturnKeyStartsNewLine (bool shouldStartNewLine)
@@ -1322,13 +1330,7 @@ void TextEditor::timerCallbackInt()
void TextEditor::checkFocus()
{
if (! wasFocused && hasKeyboardFocus (false) && ! isCurrentlyBlockedByAnotherModalComponent())
{
wasFocused = true;
if (auto* peer = getPeer())
if (! isReadOnly())
peer->textInputRequired (peer->globalToLocal (getScreenPosition()), *this);
}
}
void TextEditor::repaintText (Range<int> range)
@@ -1827,6 +1829,11 @@ void TextEditor::performPopupMenuAction (const int menuItemID)
//==============================================================================
void TextEditor::mouseDown (const MouseEvent& e)
{
mouseDownInEditor = e.originalComponent == this;
if (! mouseDownInEditor)
return;
beginDragAutoRepeat (100);
newTransaction();
@@ -1865,6 +1872,9 @@ void TextEditor::mouseDown (const MouseEvent& e)
void TextEditor::mouseDrag (const MouseEvent& e)
{
if (! mouseDownInEditor)
return;
if (wasFocused || ! selectAllTextWhenFocused)
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
moveCaretTo (getTextIndexAt (e.x, e.y), true);
@@ -1872,6 +1882,9 @@ void TextEditor::mouseDrag (const MouseEvent& e)
void TextEditor::mouseUp (const MouseEvent& e)
{
if (! mouseDownInEditor)
return;
newTransaction();
textHolder->restartTimer();
@@ -1884,6 +1897,9 @@ void TextEditor::mouseUp (const MouseEvent& e)
void TextEditor::mouseDoubleClick (const MouseEvent& e)
{
if (! mouseDownInEditor)
return;
int tokenEnd = getTextIndexAt (e.x, e.y);
int tokenStart = 0;
@@ -1950,6 +1966,9 @@ void TextEditor::mouseDoubleClick (const MouseEvent& e)
void TextEditor::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
{
if (! mouseDownInEditor)
return;
if (! viewport->useMouseWheelMoveIfNeeded (e, wheel))
Component::mouseWheelMove (e, wheel);
}
@@ -2214,9 +2233,6 @@ void TextEditor::focusLost (FocusChangeType)
underlinedSections.clear();
if (auto* peer = getPeer())
peer->dismissPendingTextInput();
updateCaretPosition();
postCommandMessage (TextEditorDefs::focusLossMessageId);


+ 18
- 0
modules/juce_gui_basics/widgets/juce_TextEditor.h View File

@@ -665,8 +665,24 @@ public:
void setInputRestrictions (int maxTextLength,
const String& allowedCharacters = String());
/** Sets the type of virtual keyboard that should be displayed when this editor has
focus.
*/
void setKeyboardType (VirtualKeyboardType type) noexcept { keyboardType = type; }
/** Sets the behaviour of mouse/touch interactions outside this component.
If true, then presses outside of the TextEditor will dismiss the virtual keyboard.
If false, then the virtual keyboard will remain onscreen for as long as the TextEditor has
keyboard focus.
*/
void setClicksOutsideDismissVirtualKeyboard (bool);
/** Returns true if the editor is configured to hide the virtual keyboard when the mouse is
pressed on another component.
*/
bool getClicksOutsideDismissVirtualKeyboard() const { return clicksOutsideDismissVirtualKeyboard; }
//==============================================================================
/** This abstract base class is implemented by LookAndFeel classes to provide
TextEditor drawing functionality.
@@ -765,6 +781,8 @@ private:
bool valueTextNeedsUpdating = false;
bool consumeEscAndReturnKeys = true;
bool underlineWhitespace = true;
bool mouseDownInEditor = false;
bool clicksOutsideDismissVirtualKeyboard = false;
UndoManager undoManager;
std::unique_ptr<CaretComponent> caret;


+ 22
- 1
modules/juce_gui_basics/windows/juce_ComponentPeer.cpp View File

@@ -34,12 +34,15 @@ ComponentPeer::ComponentPeer (Component& comp, int flags)
styleFlags (flags),
uniqueID (lastUniquePeerID += 2) // increment by 2 so that this can never hit 0
{
Desktop::getInstance().peers.add (this);
auto& desktop = Desktop::getInstance();
desktop.peers.add (this);
desktop.addFocusChangeListener (this);
}
ComponentPeer::~ComponentPeer()
{
auto& desktop = Desktop::getInstance();
desktop.removeFocusChangeListener (this);
desktop.peers.removeFirstMatchingValue (this);
desktop.triggerFocusCallback();
}
@@ -262,6 +265,19 @@ void ComponentPeer::handleModifierKeysChange()
target->internalModifierKeysChanged();
}
void ComponentPeer::refreshTextInputTarget()
{
const auto* lastTarget = std::exchange (textInputTarget, findCurrentTextInputTarget());
if (lastTarget == textInputTarget)
return;
if (textInputTarget == nullptr)
dismissPendingTextInput();
else if (auto* c = Component::getCurrentlyFocusedComponent())
textInputRequired (globalToLocal (c->getScreenPosition()), *textInputTarget);
}
TextInputTarget* ComponentPeer::findCurrentTextInputTarget()
{
auto* c = Component::getCurrentlyFocusedComponent();
@@ -591,4 +607,9 @@ void ComponentPeer::forceDisplayUpdate()
Desktop::getInstance().displays->refresh();
}
void ComponentPeer::globalFocusChanged (Component*)
{
refreshTextInputTarget();
}
} // namespace juce

+ 27
- 13
modules/juce_gui_basics/windows/juce_ComponentPeer.h View File

@@ -40,7 +40,7 @@ namespace juce
@tags{GUI}
*/
class JUCE_API ComponentPeer
class JUCE_API ComponentPeer : private FocusChangeListener
{
public:
//==============================================================================
@@ -135,7 +135,7 @@ public:
ComponentPeer (Component& component, int styleFlags);
/** Destructor. */
virtual ~ComponentPeer();
~ComponentPeer() override;
//==============================================================================
/** Returns the component being represented by this peer. */
@@ -356,25 +356,19 @@ public:
/** Called whenever a modifier key is pressed or released. */
void handleModifierKeysChange();
//==============================================================================
/** Tells the window that text input may be required at the given position.
This may cause things like a virtual on-screen keyboard to appear, depending
on the OS.
*/
virtual void textInputRequired (Point<int> position, TextInputTarget&) = 0;
/** If there's a currently active input-method context - i.e. characters are being
composed using multiple keystrokes - this should commit the current state of the
context to the text and clear the context.
context to the text and clear the context. This should not hide the virtual keyboard.
*/
virtual void closeInputMethodContext();
/** If there's some kind of OS input-method in progress, this should dismiss it.
/** Alerts the peer that the current text input target has changed somehow.
Overrides of this function should call closeInputMethodContext().
The peer may hide or show the virtual keyboard as a result of this call.
*/
virtual void dismissPendingTextInput();
void refreshTextInputTarget();
//==============================================================================
/** Returns the currently focused TextInputTarget, or null if none is found. */
TextInputTarget* findCurrentTextInputTarget();
@@ -536,10 +530,30 @@ private:
//==============================================================================
virtual void appStyleChanged() {}
/** Tells the window that text input may be required at the given position.
This may cause things like a virtual on-screen keyboard to appear, depending
on the OS.
This function should not be called directly by Components - use refreshTextInputTarget
instead.
*/
virtual void textInputRequired (Point<int>, TextInputTarget&) = 0;
/** If there's some kind of OS input-method in progress, this should dismiss it.
Overrides of this function should call closeInputMethodContext().
This function should not be called directly by Components - use refreshTextInputTarget
instead.
*/
virtual void dismissPendingTextInput();
void globalFocusChanged (Component*) override;
Component* getTargetForKeyPress();
WeakReference<Component> lastFocusedComponent, dragAndDropTargetComponent;
Component* lastDragAndDropCompUnderMouse = nullptr;
TextInputTarget* textInputTarget = nullptr;
const uint32 uniqueID;
bool isWindowMinimised = false;


Loading…
Cancel
Save