diff --git a/modules/juce_gui_basics/components/juce_Component.cpp b/modules/juce_gui_basics/components/juce_Component.cpp index e2a42bab0b..e9441e5e86 100644 --- a/modules/juce_gui_basics/components/juce_Component.cpp +++ b/modules/juce_gui_basics/components/juce_Component.cpp @@ -2943,6 +2943,11 @@ void Component::takeKeyboardFocus (FocusChangeType cause) return; WeakReference 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) diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index b84370565c..fc32cc7a23 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -942,6 +942,8 @@ public: void dismissPendingTextInput() override { + closeInputMethodContext(); + view.callVoidMethod (ComponentPeerView.showKeyboard, javaString ("").get()); if (! isTimerRunning()) diff --git a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm index 3f293b5add..3a62264678 100644 --- a/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm +++ b/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm @@ -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, TextInputTarget&) override; + void dismissPendingTextInput() override; BOOL textViewReplaceCharacters (Range, 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, TextInputTarget&) +void UIViewComponentPeer::textInputRequired (Point 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 (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 (0, target.getHighlightedRegion().getStart()))); + view->hiddenTextView.selectedRange = NSMakeRange ((NSUInteger) target.getHighlightedRegion().getStart(), 0); } BOOL UIViewComponentPeer::textViewReplaceCharacters (Range range, const String& text) @@ -1175,31 +1181,12 @@ BOOL UIViewComponentPeer::textViewReplaceCharacters (Range 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 (target)) - { - auto pos = component.getLocalPoint (comp, Point()); - view->hiddenTextView.frame = CGRectMake (pos.x, pos.y, 0, 0); - - updateHiddenTextContent (target); - [view->hiddenTextView becomeFirstResponder]; - } - } - else - { - [view->hiddenTextView resignFirstResponder]; - } -} - //============================================================================== void UIViewComponentPeer::displayLinkCallback() { diff --git a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp index e0912ea76c..d65bf48e1e 100644 --- a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp @@ -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; } } diff --git a/modules/juce_gui_basics/widgets/juce_Label.cpp b/modules/juce_gui_basics/widgets/juce_Label.cpp index 29726544fb..9f24a20b20 100644 --- a/modules/juce_gui_basics/widgets/juce_Label.cpp +++ b/modules/juce_gui_basics/widgets/juce_Label.cpp @@ -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); }); diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index 9e57b899a1..7068e24828 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -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 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); diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index 8b1fbc25ad..7f30f7fceb 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -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 caret; diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp index 9a4dc009ad..3ab01734a5 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp @@ -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 diff --git a/modules/juce_gui_basics/windows/juce_ComponentPeer.h b/modules/juce_gui_basics/windows/juce_ComponentPeer.h index 3c0640c3ae..64084b9d82 100644 --- a/modules/juce_gui_basics/windows/juce_ComponentPeer.h +++ b/modules/juce_gui_basics/windows/juce_ComponentPeer.h @@ -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 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, 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 lastFocusedComponent, dragAndDropTargetComponent; Component* lastDragAndDropCompUnderMouse = nullptr; + TextInputTarget* textInputTarget = nullptr; const uint32 uniqueID; bool isWindowMinimised = false;