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
@@ -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) | |||
@@ -942,6 +942,8 @@ public: | |||
void dismissPendingTextInput() override | |||
{ | |||
closeInputMethodContext(); | |||
view.callVoidMethod (ComponentPeerView.showKeyboard, javaString ("").get()); | |||
if (! isTimerRunning()) | |||
@@ -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() | |||
{ | |||
@@ -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; | |||
} | |||
} | |||
@@ -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); }); | |||
@@ -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); | |||
@@ -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; | |||
@@ -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 |
@@ -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; | |||