1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

iOS: Add proper UITextInput implementation

This provides an improved user experience, allowing cursor movement
directly from the keyboard.
This commit is contained in:
reuk 2022-07-05 20:13:07 +01:00
parent 09c107698b
commit 5cf1a964fc
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
8 changed files with 1022 additions and 255 deletions

View file

@ -271,7 +271,7 @@ namespace juce
#include "native/juce_MultiTouchMapper.h"
#endif
#if JUCE_ANDROID || JUCE_WINDOWS || JUCE_UNIT_TESTS
#if JUCE_ANDROID || JUCE_WINDOWS || JUCE_IOS || JUCE_UNIT_TESTS
#include "native/accessibility/juce_AccessibilityTextHelpers.h"
#endif
@ -279,8 +279,8 @@ namespace juce
#include "native/accessibility/juce_mac_AccessibilitySharedCode.mm"
#if JUCE_IOS
#include "native/accessibility/juce_ios_Accessibility.mm"
#include "native/juce_ios_UIViewComponentPeer.mm"
#include "native/accessibility/juce_ios_Accessibility.mm"
#include "native/juce_ios_Windowing.mm"
#include "native/juce_ios_FileChooser.mm"

View file

@ -71,8 +71,32 @@ public:
/** Inserts some text, overwriting the selected text region, if there is one. */
virtual void insertTextAtCaret (const String& textToInsert) = 0;
/** Returns the current index of the caret. */
virtual int getCaretPosition() const = 0;
/** Returns the position of the caret, relative to the component's origin. */
virtual Rectangle<int> getCaretRectangle() = 0;
Rectangle<int> getCaretRectangle() const { return getCaretRectangleForCharIndex (getCaretPosition()); }
/** Returns the bounding box of the character at the given index. */
virtual Rectangle<int> getCaretRectangleForCharIndex (int characterIndex) const = 0;
/** Returns the total number of codepoints in the string. */
virtual int getTotalNumChars() const = 0;
/** Returns the index closest to the given point.
This is the location where the cursor might be placed after clicking at the given
point in a text field.
*/
virtual int getCharIndexForPoint (Point<int> point) const = 0;
/** Returns the bounding box for a range of text in the editor. As the range may span
multiple lines, this method returns a RectangleList.
The bounds are relative to the component's top-left and may extend beyond the bounds
of the component if the text is long and word wrapping is disabled.
*/
virtual RectangleList<int> getTextBounds (Range<int> textRange) const = 0;
/** A set of possible on-screen keyboard types, for use in the
getKeyboardType() method.

View file

@ -43,33 +43,45 @@ namespace juce
#define JUCE_NATIVE_ACCESSIBILITY_INCLUDED 1
//==============================================================================
static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler)
{
const auto children = handler.getChildren();
template <typename> struct Signature;
NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()];
template <typename Result, typename... Args>
struct Signature<Result (Args...)> {};
for (auto* childHandler : children)
{
id accessibleElement = [&childHandler]
{
id native = static_cast<id> (childHandler->getNativeImplementation());
if (! childHandler->getChildren().empty())
return [native accessibilityContainer];
return native;
}();
if (accessibleElement != nil)
[accessibleChildren addObject: accessibleElement];
}
[accessibleChildren addObject: static_cast<id> (handler.getNativeImplementation())];
return accessibleChildren;
}
// @selector isn't constexpr, so the 'sel' members are functions rather than static constexpr data members
struct SignatureHasText : Signature<BOOL()> { static auto sel() { return @selector (hasText); } };
struct SignatureSetSelectedTextRange : Signature<void (UITextRange*)> { static auto sel() { return @selector (setSelectedTextRange:); } };
struct SignatureSelectedTextRange : Signature<UITextRange*()> { static auto sel() { return @selector (selectedTextRange); } };
struct SignatureMarkedTextRange : Signature<UITextRange*()> { static auto sel() { return @selector (markedTextRange); } };
struct SignatureSetMarkedTextSelectedRange : Signature<void (NSString*, NSRange)> { static auto sel() { return @selector (setMarkedText:selectedRange:); } };
struct SignatureUnmarkText : Signature<void()> { static auto sel() { return @selector (unmarkText); } };
struct SignatureMarkedTextStyle : Signature<NSDictionary<NSAttributedStringKey, id>*()> { static auto sel() { return @selector (markedTextStyle); } };
struct SignatureSetMarkedTextStyle : Signature<void (NSDictionary<NSAttributedStringKey, id>*)> { static auto sel() { return @selector (setMarkedTextStyle:); } };
struct SignatureBeginningOfDocument : Signature<UITextPosition*()> { static auto sel() { return @selector (beginningOfDocument); } };
struct SignatureEndOfDocument : Signature<UITextPosition*()> { static auto sel() { return @selector (endOfDocument); } };
struct SignatureTokenizer : Signature<id<UITextInputTokenizer>()> { static auto sel() { return @selector (tokenizer); } };
struct SignatureBaseWritingDirection : Signature<NSWritingDirection (UITextPosition*, UITextStorageDirection)> { static auto sel() { return @selector (baseWritingDirectionForPosition:inDirection:); } };
struct SignatureCaretRectForPosition : Signature<CGRect (UITextPosition*)> { static auto sel() { return @selector (caretRectForPosition:); } };
struct SignatureCharacterRangeByExtending : Signature<UITextRange* (UITextPosition*, UITextLayoutDirection)> { static auto sel() { return @selector (characterRangeByExtendingPosition:inDirection:); } };
struct SignatureCharacterRangeAtPoint : Signature<UITextRange* (CGPoint)> { static auto sel() { return @selector (characterRangeAtPoint:); } };
struct SignatureClosestPositionToPoint : Signature<UITextPosition* (CGPoint)> { static auto sel() { return @selector (closestPositionToPoint:); } };
struct SignatureClosestPositionToPointInRange : Signature<UITextPosition* (CGPoint, UITextRange*)> { static auto sel() { return @selector (closestPositionToPoint:withinRange:); } };
struct SignatureComparePositionToPosition : Signature<NSComparisonResult (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (comparePosition:toPosition:); } };
struct SignatureOffsetFromPositionToPosition : Signature<NSInteger (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (offsetFromPosition:toPosition:); } };
struct SignaturePositionFromPositionInDirection : Signature<UITextPosition* (UITextPosition*, UITextLayoutDirection, NSInteger)> { static auto sel() { return @selector (positionFromPosition:inDirection:offset:); } };
struct SignaturePositionFromPositionOffset : Signature<UITextPosition* (UITextPosition*, NSInteger)> { static auto sel() { return @selector (positionFromPosition:offset:); } };
struct SignatureFirstRectForRange : Signature<CGRect (UITextRange*)> { static auto sel() { return @selector (firstRectForRange:); } };
struct SignatureSelectionRectsForRange : Signature<NSArray<UITextSelectionRect*>* (UITextRange*)> { static auto sel() { return @selector (selectionRectsForRange:); } };
struct SignaturePositionWithinRange : Signature<UITextPosition* (UITextRange*, UITextLayoutDirection)> { static auto sel() { return @selector (positionWithinRange:farthestInDirection:); } };
struct SignatureReplaceRangeWithText : Signature<void (UITextRange*, NSString*)> { static auto sel() { return @selector (replaceRange:withText:); } };
struct SignatureSetBaseWritingDirection : Signature<void (NSWritingDirection, UITextRange*)> { static auto sel() { return @selector (setBaseWritingDirection:forRange:); } };
struct SignatureTextInRange : Signature<NSString* (UITextRange*)> { static auto sel() { return @selector (textInRange:); } };
struct SignatureTextRangeFromPosition : Signature<UITextRange* (UITextPosition*, UITextPosition*)> { static auto sel() { return @selector (textRangeFromPosition:toPosition:); } };
struct SignatureSetInputDelegate : Signature<void (id)> { static auto sel() { return @selector (setInputDelegate:); } };
struct SignatureInputDelegate : Signature<id()> { static auto sel() { return @selector (inputDelegate); } };
struct SignatureKeyboardType : Signature<UIKeyboardType()> { static auto sel() { return @selector (keyboardType); } };
struct SignatureAutocapitalizationType : Signature<UITextAutocapitalizationType()> { static auto sel() { return @selector (autocapitalizationType); } };
struct SignatureAutocorrectionType : Signature<UITextAutocorrectionType()> { static auto sel() { return @selector (autocorrectionType); } };
//==============================================================================
class AccessibilityHandler::AccessibilityNativeImpl
@ -208,6 +220,12 @@ private:
//==============================================================================
class AccessibilityElement : public AccessibleObjCClass<UIAccessibilityElement>
{
template <typename Func, typename... Items>
static constexpr void forEach (Func&& func, Items&&... items)
{
(void) std::initializer_list<int> { ((void) func (std::forward<Items> (items)), 0)... };
}
public:
enum class Type { defaultElement, textElement };
@ -318,7 +336,8 @@ private:
case AccessibilityRole::hyperlink: return UIAccessibilityTraitLink;
case AccessibilityRole::ignored: return UIAccessibilityTraitNotEnabled;
case AccessibilityRole::editableText:
case AccessibilityRole::editableText: return UIAccessibilityTraitKeyboardKey;
case AccessibilityRole::slider:
case AccessibilityRole::menuItem:
case AccessibilityRole::menuBar:
@ -394,7 +413,7 @@ private:
handler->giveAwayFocus();
});
addMethod (@selector (accessibilityElementIsFocused), [] (id self, SEL)
addMethod (@selector (accessibilityElementIsFocused), [] (id self, SEL) -> BOOL
{
if (auto* handler = getHandler (self))
return handler->hasFocus (false);
@ -447,63 +466,45 @@ private:
if (elementType == Type::textElement)
{
addMethod (@selector (accessibilityLineNumberForPoint:), [] (id self, SEL, CGPoint point)
{
if (auto* handler = getHandler (self))
{
if (auto* textInterface = handler->getTextInterface())
{
auto pointInt = roundToIntPoint (point);
addMethod (@selector (deleteBackward), [] (id, SEL) {});
addMethod (@selector (insertText:), [] (id, SEL, NSString*) {});
if (handler->getComponent().getScreenBounds().contains (pointInt))
{
auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() });
forEach ([this] (auto signature) { addPassthroughMethodWithSignature (signature); },
SignatureHasText{},
SignatureSetSelectedTextRange{},
SignatureSelectedTextRange{},
SignatureMarkedTextRange{},
SignatureSetMarkedTextSelectedRange{},
SignatureUnmarkText{},
SignatureMarkedTextStyle{},
SignatureSetMarkedTextStyle{},
SignatureBeginningOfDocument{},
SignatureEndOfDocument{},
SignatureTokenizer{},
SignatureBaseWritingDirection{},
SignatureCaretRectForPosition{},
SignatureCharacterRangeByExtending{},
SignatureCharacterRangeAtPoint{},
SignatureClosestPositionToPoint{},
SignatureClosestPositionToPointInRange{},
SignatureComparePositionToPosition{},
SignatureOffsetFromPositionToPosition{},
SignaturePositionFromPositionInDirection{},
SignaturePositionFromPositionOffset{},
SignatureFirstRectForRange{},
SignatureSelectionRectsForRange{},
SignaturePositionWithinRange{},
SignatureReplaceRangeWithText{},
SignatureSetBaseWritingDirection{},
SignatureTextInRange{},
SignatureTextRangeFromPosition{},
SignatureSetInputDelegate{},
SignatureInputDelegate{},
SignatureKeyboardType{},
SignatureAutocapitalizationType{},
SignatureAutocorrectionType{});
for (int i = 0; i < textBounds.getNumRectangles(); ++i)
if (textBounds.getRectangle (i).contains (pointInt))
return (NSInteger) i;
}
}
}
return NSNotFound;
});
addMethod (@selector (accessibilityContentForLineNumber:), [] (id self, SEL, NSInteger lineNumber) -> NSString*
{
if (auto* textInterface = getTextInterface (self))
{
auto lines = StringArray::fromLines (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }));
if ((int) lineNumber < lines.size())
return juceStringToNS (lines[(int) lineNumber]);
}
return nil;
});
addMethod (@selector (accessibilityFrameForLineNumber:), [] (id self, SEL, NSInteger lineNumber)
{
if (auto* textInterface = getTextInterface (self))
{
auto textBounds = textInterface->getTextBounds ({ 0, textInterface->getTotalNumCharacters() });
if (lineNumber < textBounds.getNumRectangles())
return convertToCGRect (textBounds.getRectangle ((int) lineNumber));
}
return CGRectZero;
});
addMethod (@selector (accessibilityPageContent), [] (id self, SEL) -> NSString*
{
if (auto* textInterface = getTextInterface (self))
return juceStringToNS (textInterface->getText ({ 0, textInterface->getTotalNumCharacters() }));
return nil;
});
addProtocol (@protocol (UIAccessibilityReadingContent));
addProtocol (@protocol (UITextInput));
}
#if JUCE_IOS_CONTAINER_API_AVAILABLE
@ -521,15 +522,69 @@ private:
}
private:
template <typename Return, typename Method>
void addMethodWithReturn (SEL selector, Method method) { addMethod (selector, static_cast<Return (*) (id, SEL)> (method)); }
template <typename Result>
static auto getResult (NSInvocation* invocation, detail::Tag<Result>)
{
Result result{};
[invocation getReturnValue: &result];
return result;
}
static void getResult (NSInvocation*, detail::Tag<void>) {}
template <typename HasSelector, typename Result, typename... Args>
auto makePassthroughCallback (HasSelector, Signature<Result (Args...)>)
{
return [] (id self, SEL, Args... args) -> Result
{
if (auto* input = getPeerTextInput (self))
{
const auto s = detail::makeCompileTimeStr (@encode (Result), @encode (id), @encode (SEL), @encode (Args)...);
const auto signature = [NSMethodSignature signatureWithObjCTypes: s.data()];
const auto invocation = [NSInvocation invocationWithMethodSignature: signature];
invocation.selector = HasSelector::sel();
// Indices 0 and 1 are 'id self' and 'SEL _cmd' respectively
auto counter = 2;
forEach ([&] (auto& arg) { [invocation setArgument: &arg atIndex: counter++]; }, args...);
[invocation invokeWithTarget: input];
return getResult (invocation, detail::Tag<Result>{});
}
jassertfalse;
return {};
};
}
template <typename Signature>
void addPassthroughMethodWithSignature (Signature signature)
{
addMethod (Signature::sel(), makePassthroughCallback (signature, signature));
}
//==============================================================================
static UIAccessibilityElement* getContainer (id self)
{
return getIvar<UIAccessibilityElement*> (self, "container");
}
static UIViewComponentPeer* getPeer (id self)
{
if (auto* handler = getHandler (self))
return static_cast<UIViewComponentPeer*> (handler->getComponent().getPeer());
return nil;
}
static JuceTextView* getPeerTextInput (id self)
{
if (auto* peer = getPeer (self))
return peer->hiddenTextInput.get();
return nil;
}
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AccessibilityElement)
};

View file

@ -39,6 +39,34 @@
namespace juce
{
//==============================================================================
static NSArray* getContainerAccessibilityElements (AccessibilityHandler& handler)
{
const auto children = handler.getChildren();
NSMutableArray* accessibleChildren = [NSMutableArray arrayWithCapacity: (NSUInteger) children.size()];
for (auto* childHandler : children)
{
id accessibleElement = [&childHandler]
{
id native = static_cast<id> (childHandler->getNativeImplementation());
if (! childHandler->getChildren().empty())
return [native accessibilityContainer];
return native;
}();
if (accessibleElement != nil)
[accessibleChildren addObject: accessibleElement];
}
[accessibleChildren addObject: static_cast<id> (handler.getNativeImplementation())];
return accessibleChildren;
}
class UIViewComponentPeer;
static UIInterfaceOrientation getWindowOrientation()
@ -119,6 +147,107 @@ enum class MouseEventFlags
using namespace juce;
@interface JuceUITextPosition : UITextPosition
{
@public
int index;
}
@end
@implementation JuceUITextPosition
+ (instancetype) withIndex: (int) indexIn
{
auto* result = [[JuceUITextPosition alloc] init];
result->index = indexIn;
return [result autorelease];
}
@end
//==============================================================================
@interface JuceUITextRange : UITextRange
{
@public
int from, to;
}
@end
@implementation JuceUITextRange
+ (instancetype) withRange: (juce::Range<int>) range
{
return [JuceUITextRange from: range.getStart() to: range.getEnd()];
}
+ (instancetype) from: (int) from to: (int) to
{
auto* result = [[JuceUITextRange alloc] init];
result->from = from;
result->to = to;
return [result autorelease];
}
- (UITextPosition*) start
{
return [JuceUITextPosition withIndex: from];
}
- (UITextPosition*) end
{
return [JuceUITextPosition withIndex: to];
}
- (Range<int>) range
{
return Range<int>::between (from, to);
}
- (BOOL) isEmpty
{
return from == to;
}
@end
//==============================================================================
// UITextInputStringTokenizer doesn't handle 'line' granularities correctly by default, hence
// this subclass.
@interface JuceTextInputTokenizer : UITextInputStringTokenizer
{
UIViewComponentPeer* peer;
}
- (instancetype) initWithPeer: (UIViewComponentPeer*) peer;
@end
//==============================================================================
@interface JuceUITextSelectionRect : UITextSelectionRect
{
CGRect _rect;
}
@end
@implementation JuceUITextSelectionRect
+ (instancetype) withRect: (CGRect) rect
{
auto* result = [[JuceUITextSelectionRect alloc] init];
result->_rect = rect;
return [result autorelease];
}
- (CGRect) rect { return _rect; }
- (NSWritingDirection) writingDirection { return NSWritingDirectionNatural; }
- (BOOL) containsStart { return NO; }
- (BOOL) containsEnd { return NO; }
- (BOOL) isVertical { return NO; }
@end
//==============================================================================
struct CADisplayLinkDeleter
{
void operator() (CADisplayLink* displayLink) const noexcept
@ -128,11 +257,21 @@ struct CADisplayLinkDeleter
}
};
@interface JuceUIView : UIView <UITextViewDelegate>
@interface JuceTextView : UIView <UITextInput>
{
@public
UIViewComponentPeer* owner;
id<UITextInputDelegate> delegate;
}
- (instancetype) initWithOwner: (UIViewComponentPeer*) owner;
@end
@interface JuceUIView : UIView
{
@public
UIViewComponentPeer* owner;
UITextView* hiddenTextView;
std::unique_ptr<CADisplayLink, CADisplayLinkDeleter> displayLink;
}
@ -159,8 +298,6 @@ struct CADisplayLinkDeleter
- (BOOL) resignFirstResponder;
- (BOOL) canBecomeFirstResponder;
- (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text;
- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection;
- (BOOL) isAccessibilityElement;
@ -212,6 +349,7 @@ struct UIViewPeerControllerReceiver
virtual void setViewController (UIViewController*) = 0;
};
//==============================================================================
class UIViewComponentPeer : public ComponentPeer,
private UIViewPeerControllerReceiver
{
@ -264,9 +402,9 @@ public:
void grabFocus() override;
void textInputRequired (Point<int>, TextInputTarget&) override;
void dismissPendingTextInput() override;
void closeInputMethodContext() override;
BOOL textViewReplaceCharacters (Range<int>, const String&);
void updateHiddenTextContent (TextInputTarget&);
void updateScreenBounds();
@ -286,7 +424,10 @@ public:
JuceUIView* view = nil;
UIViewController* controller = nil;
const bool isSharedWindow, isAppex;
String stringBeingComposed;
bool fullScreen = false, insideDrawRect = false;
NSUniquePtr<JuceTextView> hiddenTextInput { [[JuceTextView alloc] initWithOwner: this] };
NSUniquePtr<JuceTextInputTokenizer> tokenizer { [[JuceTextInputTokenizer alloc] initWithPeer: this] };
static int64 getMouseTime (NSTimeInterval timestamp) noexcept
{
@ -306,6 +447,22 @@ public:
static MultiTouchMapper<UITouch*> currentTouches;
static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType type) noexcept
{
switch (type)
{
case TextInputTarget::textKeyboard: return UIKeyboardTypeDefault;
case TextInputTarget::numericKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
case TextInputTarget::decimalKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
case TextInputTarget::urlKeyboard: return UIKeyboardTypeURL;
case TextInputTarget::emailAddressKeyboard: return UIKeyboardTypeEmailAddress;
case TextInputTarget::phoneNumberKeyboard: return UIKeyboardTypePhonePad;
default: jassertfalse; break;
}
return UIKeyboardTypeDefault;
}
private:
void appStyleChanged() override
{
@ -491,14 +648,7 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
[displayLink.get() addToRunLoop: [NSRunLoop mainRunLoop]
forMode: NSDefaultRunLoopMode];
hiddenTextView = [[UITextView alloc] initWithFrame: CGRectZero];
[self addSubview: hiddenTextView];
hiddenTextView.delegate = self;
hiddenTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
hiddenTextView.autocorrectionType = UITextAutocorrectionTypeNo;
hiddenTextView.inputAssistantItem.leadingBarButtonGroups = @[];
hiddenTextView.inputAssistantItem.trailingBarButtonGroups = @[];
[self addSubview: owner->hiddenTextInput.get()];
#if JUCE_HAS_IOS_POINTER_SUPPORT
if (@available (iOS 13.4, *))
@ -522,9 +672,7 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
- (void) dealloc
{
[hiddenTextView removeFromSuperview];
[hiddenTextView release];
[owner->hiddenTextInput.get() removeFromSuperview];
displayLink = nullptr;
[super dealloc];
@ -623,13 +771,6 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
return owner != nullptr && owner->canBecomeKeyWindow();
}
- (BOOL) textView: (UITextView*) textView shouldChangeTextInRange: (NSRange) range replacementText: (NSString*) text
{
ignoreUnused (textView);
return owner->textViewReplaceCharacters (Range<int> ((int) range.location, (int) (range.location + range.length)),
nsStringToJuce (text));
}
- (void) traitCollectionDidChange: (UITraitCollection*) previousTraitCollection
{
[super traitCollectionDidChange: previousTraitCollection];
@ -689,6 +830,542 @@ MultiTouchMapper<UITouch*> UIViewComponentPeer::currentTouches;
@end
/** see https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/LowerLevelText-HandlingTechnologies/LowerLevelText-HandlingTechnologies.html */
@implementation JuceTextView
- (TextInputTarget*) getTextInputTarget
{
if (owner != nullptr)
return owner->findCurrentTextInputTarget();
return nullptr;
}
- (instancetype) initWithOwner: (UIViewComponentPeer*) ownerIn
{
[super init];
owner = ownerIn;
delegate = nil;
// The frame must have a finite size, otherwise some accessibility events will be ignored
self.frame = CGRectMake (0.0, 0.0, 1.0, 1.0);
return self;
}
- (BOOL) canPerformAction: (SEL) action withSender: (id) sender
{
if (auto* target = [self getTextInputTarget])
{
if (action == @selector (paste:))
{
if (@available (iOS 10, *))
return [[UIPasteboard generalPasteboard] hasStrings];
return [[UIPasteboard generalPasteboard] string] != nil;
}
}
return [super canPerformAction: action withSender: sender];
}
- (void) cut: (id) sender
{
[self copy: sender];
if (auto* target = [self getTextInputTarget])
{
if (delegate != nil)
[delegate textWillChange: self];
target->insertTextAtCaret ("");
if (delegate != nil)
[delegate textDidChange: self];
}
}
- (void) copy: (id) sender
{
if (auto* target = [self getTextInputTarget])
[[UIPasteboard generalPasteboard] setString: juceStringToNS (target->getTextInRange (target->getHighlightedRegion()))];
}
- (void) paste: (id) sender
{
if (auto* target = [self getTextInputTarget])
{
if (auto* string = [[UIPasteboard generalPasteboard] string])
{
if (delegate != nil)
[delegate textWillChange: self];
target->insertTextAtCaret (nsStringToJuce (string));
if (delegate != nil)
[delegate textDidChange: self];
}
}
}
- (void) selectAll: (id) sender
{
if (auto* target = [self getTextInputTarget])
target->setHighlightedRegion ({ 0, target->getTotalNumChars() });
}
- (void) deleteBackward
{
auto* target = [self getTextInputTarget];
if (target == nullptr)
return;
const auto range = target->getHighlightedRegion();
const auto rangeToDelete = range.isEmpty() ? range.withStartAndLength (jmax (range.getStart() - 1, 0),
range.getStart() != 0 ? 1 : 0)
: range;
target->setHighlightedRegion (rangeToDelete);
target->insertTextAtCaret ("");
}
- (void) insertText: (NSString*) text
{
if (owner == nullptr)
return;
owner->stringBeingComposed.clear();
if (auto* target = owner->findCurrentTextInputTarget())
target->insertTextAtCaret (nsStringToJuce (text));
}
- (BOOL) hasText
{
if (auto* target = [self getTextInputTarget])
return target->getTextInRange ({ 0, 1 }).isNotEmpty();
return NO;
}
- (BOOL) accessibilityElementsHidden
{
return NO;
}
- (UITextRange*) selectedTextRangeForTarget: (TextInputTarget*) target
{
if (target != nullptr)
return [JuceUITextRange withRange: target->getHighlightedRegion()];
return nil;
}
- (UITextRange*) selectedTextRange
{
return [self selectedTextRangeForTarget: [self getTextInputTarget]];
}
- (void) setSelectedTextRange: (JuceUITextRange*) range
{
if (auto* target = [self getTextInputTarget])
target->setHighlightedRegion (range != nil ? [range range] : Range<int>());
}
- (UITextRange*) markedTextRange
{
if (owner != nullptr && owner->stringBeingComposed.isNotEmpty())
if (auto* target = owner->findCurrentTextInputTarget())
return [JuceUITextRange withRange: target->getHighlightedRegion()];
return nil;
}
- (void) setMarkedText: (NSString*) markedText
selectedRange: (NSRange) selectedRange
{
ignoreUnused (selectedRange);
if (owner == nullptr)
return;
owner->stringBeingComposed = nsStringToJuce (markedText);
auto* target = owner->findCurrentTextInputTarget();
if (target == nullptr)
return;
const auto currentHighlight = target->getHighlightedRegion();
target->insertTextAtCaret (owner->stringBeingComposed);
target->setHighlightedRegion (currentHighlight.withLength (0));
target->setHighlightedRegion (currentHighlight.withLength (owner->stringBeingComposed.length()));
}
- (void) unmarkText
{
if (owner == nullptr)
return;
auto* target = owner->findCurrentTextInputTarget();
if (target == nullptr)
return;
target->insertTextAtCaret (owner->stringBeingComposed);
owner->stringBeingComposed.clear();
}
- (NSDictionary<NSAttributedStringKey, id>*) markedTextStyle
{
return nil;
}
- (void) setMarkedTextStyle: (NSDictionary<NSAttributedStringKey, id>*) dict
{
}
- (UITextPosition*) beginningOfDocument
{
return [JuceUITextPosition withIndex: 0];
}
- (UITextPosition*) endOfDocument
{
if (auto* target = [self getTextInputTarget])
return [JuceUITextPosition withIndex: target->getTotalNumChars()];
return [JuceUITextPosition withIndex: 0];
}
- (id<UITextInputTokenizer>) tokenizer
{
return owner->tokenizer.get();
}
- (NSWritingDirection) baseWritingDirectionForPosition: (UITextPosition*) position
inDirection: (UITextStorageDirection) direction
{
return NSWritingDirectionNatural;
}
- (CGRect) caretRectForPosition: (JuceUITextPosition*) position
{
if (position == nil)
return CGRectZero;
// Currently we ignore the requested position and just return the text editor's caret position
if (auto* target = [self getTextInputTarget])
{
if (auto* comp = dynamic_cast<Component*> (target))
{
const auto areaOnDesktop = comp->localAreaToGlobal (target->getCaretRectangle());
return convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
}
}
return CGRectZero;
}
- (UITextRange*) characterRangeByExtendingPosition: (JuceUITextPosition*) position
inDirection: (UITextLayoutDirection) direction
{
const auto newPosition = [self indexFromPosition: position inDirection: direction offset: 1];
return [JuceUITextRange from: position->index to: newPosition];
}
- (int) closestIndexToPoint: (CGPoint) point
{
if (auto* target = [self getTextInputTarget])
{
if (auto* comp = dynamic_cast<Component*> (target))
{
const auto pointOnDesktop = ScalingHelpers::unscaledScreenPosToScaled (convertToPointFloat (point));
return target->getCharIndexForPoint (comp->getLocalPoint (nullptr, pointOnDesktop).roundToInt());
}
}
return -1;
}
- (UITextRange*) characterRangeAtPoint: (CGPoint) point
{
const auto index = [self closestIndexToPoint: point];
const auto result = index != -1 ? [JuceUITextRange from: index to: index] : nil;
jassert (result != nullptr);
return result;
}
- (UITextPosition*) closestPositionToPoint: (CGPoint) point
{
const auto index = [self closestIndexToPoint: point];
const auto result = index != -1 ? [JuceUITextPosition withIndex: index] : nil;
jassert (result != nullptr);
return result;
}
- (UITextPosition*) closestPositionToPoint: (CGPoint) point
withinRange: (JuceUITextRange*) range
{
const auto index = [self closestIndexToPoint: point];
const auto result = index != -1 ? [JuceUITextPosition withIndex: [range range].clipValue (index)] : nil;
jassert (result != nullptr);
return result;
}
- (NSComparisonResult) comparePosition: (JuceUITextPosition*) position
toPosition: (JuceUITextPosition*) other
{
const auto a = position != nil ? makeOptional (position->index) : nullopt;
const auto b = other != nil ? makeOptional (other ->index) : nullopt;
if (a < b)
return NSOrderedAscending;
if (b < a)
return NSOrderedDescending;
return NSOrderedSame;
}
- (NSInteger) offsetFromPosition: (JuceUITextPosition*) from
toPosition: (JuceUITextPosition*) toPosition
{
if (from != nil && toPosition != nil)
return toPosition->index - from->index;
return 0;
}
- (int) indexFromPosition: (JuceUITextPosition*) position
inDirection: (UITextLayoutDirection) direction
offset: (NSInteger) offset
{
switch (direction)
{
case UITextLayoutDirectionLeft:
case UITextLayoutDirectionRight:
return position->index + (int) (offset * (direction == UITextLayoutDirectionLeft ? -1 : 1));
case UITextLayoutDirectionUp:
case UITextLayoutDirectionDown:
{
if (auto* target = [self getTextInputTarget])
{
const auto originalRectangle = target->getCaretRectangleForCharIndex (position->index);
auto testIndex = position->index;
for (auto lineOffset = 0; lineOffset < offset; ++lineOffset)
{
const auto caretRect = target->getCaretRectangleForCharIndex (testIndex);
const auto newY = direction == UITextLayoutDirectionUp ? caretRect.getY() - 1
: caretRect.getBottom() + 1;
testIndex = target->getCharIndexForPoint ({ originalRectangle.getX(), newY });
}
return testIndex;
}
}
}
return position->index;
}
- (UITextPosition*) positionFromPosition: (JuceUITextPosition*) position
inDirection: (UITextLayoutDirection) direction
offset: (NSInteger) offset
{
return [JuceUITextPosition withIndex: [self indexFromPosition: position inDirection: direction offset: offset]];
}
- (UITextPosition*) positionFromPosition: (JuceUITextPosition*) position
offset: (NSInteger) offset
{
if (position != nil)
{
if (auto* target = [self getTextInputTarget])
{
const auto newIndex = position->index + (int) offset;
if (isPositiveAndBelow (newIndex, target->getTotalNumChars() + 1))
return [JuceUITextPosition withIndex: newIndex];
}
}
return nil;
}
- (CGRect) firstRectForRange: (JuceUITextRange*) range
{
if (auto* target = [self getTextInputTarget])
{
if (auto* comp = dynamic_cast<Component*> (target))
{
const auto list = target->getTextBounds ([range range]);
if (! list.isEmpty())
{
const auto areaOnDesktop = comp->localAreaToGlobal (list.getRectangle (0));
return convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
}
}
}
return {};
}
- (NSArray<UITextSelectionRect*>*) selectionRectsForRange: (JuceUITextRange*) range
{
if (auto* target = [self getTextInputTarget])
{
if (auto* comp = dynamic_cast<Component*> (target))
{
const auto list = target->getTextBounds ([range range]);
auto* result = [NSMutableArray arrayWithCapacity: (NSUInteger) list.getNumRectangles()];
for (const auto& rect : list)
{
const auto areaOnDesktop = comp->localAreaToGlobal (rect);
const auto nativeArea = convertToCGRect (ScalingHelpers::scaledScreenPosToUnscaled (areaOnDesktop));
[result addObject: [JuceUITextSelectionRect withRect: nativeArea]];
}
return result;
}
}
return nil;
}
- (UITextPosition*) positionWithinRange: (JuceUITextRange*) range
farthestInDirection: (UITextLayoutDirection) direction
{
return direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionLeft
? [range start]
: [range end];
}
- (void) replaceRange: (JuceUITextRange*) range
withText: (NSString*) text
{
if (owner == nullptr)
return;
owner->stringBeingComposed.clear();
if (auto* target = owner->findCurrentTextInputTarget())
{
target->setHighlightedRegion ([range range]);
target->insertTextAtCaret (nsStringToJuce (text));
}
}
- (void) setBaseWritingDirection: (NSWritingDirection) writingDirection
forRange: (UITextRange*) range
{
}
- (NSString*) textInRange: (JuceUITextRange*) range
{
if (auto* target = [self getTextInputTarget])
return juceStringToNS (target->getTextInRange ([range range]));
return nil;
}
- (UITextRange*) textRangeFromPosition: (JuceUITextPosition*) fromPosition
toPosition: (JuceUITextPosition*) toPosition
{
const auto from = fromPosition != nil ? fromPosition->index : 0;
const auto to = toPosition != nil ? toPosition ->index : 0;
return [JuceUITextRange withRange: Range<int>::between (from, to)];
}
- (void) setInputDelegate: (id<UITextInputDelegate>) delegateIn
{
delegate = delegateIn;
}
- (id<UITextInputDelegate>) inputDelegate
{
return delegate;
}
- (UIKeyboardType) keyboardType
{
if (auto* target = [self getTextInputTarget])
return UIViewComponentPeer::getUIKeyboardType (target->getKeyboardType());
return UIKeyboardTypeDefault;
}
- (UITextAutocapitalizationType) autocapitalizationType
{
return UITextAutocapitalizationTypeNone;
}
- (UITextAutocorrectionType) autocorrectionType
{
return UITextAutocorrectionTypeNo;
}
- (BOOL) canBecomeFirstResponder
{
return YES;
}
@end
//==============================================================================
@implementation JuceTextInputTokenizer
- (instancetype) initWithPeer: (UIViewComponentPeer*) peerIn
{
[super initWithTextInput: peerIn->hiddenTextInput.get()];
peer = peerIn;
return self;
}
- (UITextRange*) rangeEnclosingPosition: (JuceUITextPosition*) position
withGranularity: (UITextGranularity) granularity
inDirection: (UITextDirection) direction
{
if (granularity != UITextGranularityLine)
return [super rangeEnclosingPosition: position withGranularity: granularity inDirection: direction];
auto* target = peer->findCurrentTextInputTarget();
if (target == nullptr)
return nullptr;
const auto numChars = target->getTotalNumChars();
if (! isPositiveAndBelow (position->index, numChars))
return nullptr;
const auto allText = target->getTextInRange ({ 0, numChars });
const auto begin = AccessibilityTextHelpers::makeCharPtrIteratorAdapter (allText.begin());
const auto end = AccessibilityTextHelpers::makeCharPtrIteratorAdapter (allText.end());
const auto positionIter = begin + position->index;
const auto nextNewlineIter = std::find (positionIter, end, '\n');
const auto lastNewlineIter = std::find (std::make_reverse_iterator (positionIter),
std::make_reverse_iterator (begin),
'\n').base();
const auto from = std::distance (begin, lastNewlineIter);
const auto to = std::distance (begin, nextNewlineIter);
return [JuceUITextRange from: from to: to];
}
@end
//==============================================================================
//==============================================================================
namespace juce
@ -975,6 +1652,9 @@ static float getTouchForce (UITouch* touch) noexcept
void UIViewComponentPeer::handleTouches (UIEvent* event, MouseEventFlags mouseEventFlags)
{
if (event == nullptr)
return;
NSArray* touches = [[event touchesForView: view] allObjects];
for (unsigned int i = 0; i < [touches count]; ++i)
@ -1126,41 +1806,27 @@ void UIViewComponentPeer::grabFocus()
}
}
void UIViewComponentPeer::textInputRequired (Point<int> pos, TextInputTarget& target)
void UIViewComponentPeer::textInputRequired (Point<int>, TextInputTarget&)
{
view->hiddenTextView.frame = CGRectMake (pos.x, pos.y, 0, 0);
[hiddenTextInput.get() becomeFirstResponder];
}
updateHiddenTextContent (target);
[view->hiddenTextView becomeFirstResponder];
void UIViewComponentPeer::closeInputMethodContext()
{
if (auto* input = hiddenTextInput.get())
{
if (auto* delegate = [input inputDelegate])
{
[delegate selectionWillChange: input];
[delegate selectionDidChange: input];
}
}
}
void UIViewComponentPeer::dismissPendingTextInput()
{
closeInputMethodContext();
[view->hiddenTextView resignFirstResponder];
}
static UIKeyboardType getUIKeyboardType (TextInputTarget::VirtualKeyboardType type) noexcept
{
switch (type)
{
case TextInputTarget::textKeyboard: return UIKeyboardTypeAlphabet;
case TextInputTarget::numericKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
case TextInputTarget::decimalKeyboard: return UIKeyboardTypeNumbersAndPunctuation;
case TextInputTarget::urlKeyboard: return UIKeyboardTypeURL;
case TextInputTarget::emailAddressKeyboard: return UIKeyboardTypeEmailAddress;
case TextInputTarget::phoneNumberKeyboard: return UIKeyboardTypePhonePad;
default: jassertfalse; break;
}
return UIKeyboardTypeDefault;
}
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);
[hiddenTextInput.get() resignFirstResponder];
}
BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const String& text)
@ -1179,9 +1845,6 @@ BOOL UIViewComponentPeer::textViewReplaceCharacters (Range<int> range, const Str
handleKeyPress (KeyPress::returnKey, text[0]);
else
target->insertTextAtCaret (text);
if (deletionChecker != nullptr)
updateHiddenTextContent (*target);
}
return NO;

View file

@ -1168,7 +1168,7 @@ void TextEditor::updateCaretPosition()
{
Iterator i (*this);
caret->setCaretPosition (getCaretRectangle().translated (leftIndent,
topIndent + roundToInt (i.getYOffset())));
topIndent + roundToInt (i.getYOffset())) - getTextOffset());
if (auto* handler = getAccessibilityHandler())
handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged);
@ -1451,18 +1451,13 @@ void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX,
viewport->setViewPosition (vx, vy);
}
Rectangle<int> TextEditor::getCaretRectangle()
{
return getCaretRectangleFloat().getSmallestIntegerContainer();
}
Rectangle<float> TextEditor::getCaretRectangleFloat() const
Rectangle<int> TextEditor::getCaretRectangleForCharIndex (int index) const
{
Point<float> anchor;
auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value)
getCharPosition (caretPosition, anchor, cursorHeight);
getCharPosition (index, anchor, cursorHeight);
return { anchor.x, anchor.y, 2.0f, cursorHeight };
return Rectangle<float> { anchor.x, anchor.y, 2.0f, cursorHeight }.getSmallestIntegerContainer() + getTextOffset();
}
Point<int> TextEditor::getTextOffset() const noexcept
@ -1474,7 +1469,7 @@ Point<int> TextEditor::getTextOffset() const noexcept
roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + yOffset) - viewport->getViewPositionY() };
}
RectangleList<int> TextEditor::getTextBounds (Range<int> textRange)
RectangleList<int> TextEditor::getTextBounds (Range<int> textRange) const
{
RectangleList<int> boundingBox;
Iterator i (*this);
@ -1564,7 +1559,7 @@ void TextEditor::scrollToMakeSureCursorIsVisible()
if (keepCaretOnScreen)
{
auto viewPos = viewport->getViewPosition();
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent);
auto caretRect = getCaretRectangle().translated (leftIndent, topIndent) - getTextOffset();
auto relativeCursor = caretRect.getPosition() - viewPos;
if (relativeCursor.x < jmax (1, proportionOfWidth (0.05f)))
@ -1647,6 +1642,16 @@ int TextEditor::getTextIndexAt (const int x, const int y) const
(float) (y - offset.y));
}
int TextEditor::getTextIndexAt (const Point<int> pt) const
{
return getTextIndexAt (pt.x, pt.y);
}
int TextEditor::getCharIndexForPoint (const Point<int> point) const
{
return getTextIndexAt (isMultiLine() ? point : getTextBounds ({ 0, getTotalNumChars() }).getBounds().getConstrainedPoint (point));
}
void TextEditor::insertTextAtCaret (const String& t)
{
String newText (inputFilter != nullptr ? inputFilter->filterNewText (*this, t) : t);
@ -1670,8 +1675,13 @@ void TextEditor::insertTextAtCaret (const String& t)
void TextEditor::setHighlightedRegion (const Range<int>& newSelection)
{
moveCaretTo (newSelection.getStart(), false);
moveCaretTo (newSelection.getEnd(), true);
if (newSelection == getHighlightedRegion())
return;
const auto cursorAtStart = newSelection.getEnd() == getHighlightedRegion().getStart()
|| newSelection.getEnd() == getHighlightedRegion().getEnd();
moveCaretTo (cursorAtStart ? newSelection.getEnd() : newSelection.getStart(), false);
moveCaretTo (cursorAtStart ? newSelection.getStart() : newSelection.getEnd(), true);
}
//==============================================================================
@ -1853,8 +1863,7 @@ void TextEditor::mouseDown (const MouseEvent& e)
{
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
{
moveCaretTo (getTextIndexAt (e.x, e.y),
e.mods.isShiftDown());
moveCaretTo (getTextIndexAt (e.getPosition()), e.mods.isShiftDown());
if (auto* peer = getPeer())
peer->closeInputMethodContext();
@ -1889,7 +1898,7 @@ void TextEditor::mouseDrag (const MouseEvent& e)
if (wasFocused || ! selectAllTextWhenFocused)
if (! (popupMenuEnabled && e.mods.isPopupMenu()))
moveCaretTo (getTextIndexAt (e.x, e.y), true);
moveCaretTo (getTextIndexAt (e.getPosition()), true);
}
void TextEditor::mouseUp (const MouseEvent& e)
@ -1902,7 +1911,7 @@ void TextEditor::mouseUp (const MouseEvent& e)
if (wasFocused || ! selectAllTextWhenFocused)
if (e.mouseWasClicked() && ! (popupMenuEnabled && e.mods.isPopupMenu()))
moveCaret (getTextIndexAt (e.x, e.y));
moveCaret (getTextIndexAt (e.getPosition()));
wasFocused = true;
}
@ -1912,7 +1921,7 @@ void TextEditor::mouseDoubleClick (const MouseEvent& e)
if (! mouseDownInEditor)
return;
int tokenEnd = getTextIndexAt (e.x, e.y);
int tokenEnd = getTextIndexAt (e.getPosition());
int tokenStart = 0;
if (e.getNumberOfClicks() > 3)
@ -2026,7 +2035,7 @@ bool TextEditor::moveCaretUp (bool selecting)
if (! isMultiLine())
return moveCaretToStartOfLine (selecting);
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - 1.0f), selecting);
}
@ -2035,7 +2044,7 @@ bool TextEditor::moveCaretDown (bool selecting)
if (! isMultiLine())
return moveCaretToEndOfLine (selecting);
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting);
}
@ -2044,7 +2053,7 @@ bool TextEditor::pageUp (bool selecting)
if (! isMultiLine())
return moveCaretToStartOfLine (selecting);
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getY() - (float) viewport->getViewHeight()), selecting);
}
@ -2053,7 +2062,7 @@ bool TextEditor::pageDown (bool selecting)
if (! isMultiLine())
return moveCaretToEndOfLine (selecting);
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + (float) viewport->getViewHeight()), selecting);
}
@ -2081,7 +2090,7 @@ bool TextEditor::moveCaretToTop (bool selecting)
bool TextEditor::moveCaretToStartOfLine (bool selecting)
{
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getY()), selecting);
}
@ -2092,7 +2101,7 @@ bool TextEditor::moveCaretToEnd (bool selecting)
bool TextEditor::moveCaretToEndOfLine (bool selecting)
{
auto caretPos = getCaretRectangleFloat();
const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat();
return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getY()), selecting);
}
@ -2727,20 +2736,7 @@ private:
void setSelection (Range<int> r) override
{
if (r == textEditor.getHighlightedRegion())
return;
if (r.isEmpty())
{
textEditor.setCaretPosition (r.getStart());
}
else
{
const auto cursorAtStart = r.getEnd() == textEditor.getHighlightedRegion().getStart()
|| r.getEnd() == textEditor.getHighlightedRegion().getEnd();
textEditor.moveCaretTo (cursorAtStart ? r.getEnd() : r.getStart(), false);
textEditor.moveCaretTo (cursorAtStart ? r.getStart() : r.getEnd(), true);
}
textEditor.setHighlightedRegion (r);
}
String getText (Range<int> r) const override
@ -2772,8 +2768,7 @@ private:
int getOffsetAtPoint (Point<int> point) const override
{
auto localPoint = textEditor.getLocalPoint (nullptr, point);
return textEditor.getTextIndexAt (localPoint.x, localPoint.y);
return textEditor.getTextIndexAt (textEditor.getLocalPoint (nullptr, point));
}
private:

View file

@ -423,7 +423,7 @@ public:
/** Returns the current index of the caret.
@see setCaretPosition
*/
int getCaretPosition() const;
int getCaretPosition() const override;
/** Moves the caret to be in front of a given character.
@see getCaretPosition, moveCaretToEnd
@ -443,12 +443,11 @@ public:
*/
void scrollEditorToPositionCaret (int desiredCaretX, int desiredCaretY);
/** Get the graphical position of the caret.
/** Get the graphical position of the caret for a particular index in the text.
The rectangle returned is relative to the component's top-left corner.
@see scrollEditorToPositionCaret
*/
Rectangle<int> getCaretRectangle() override;
Rectangle<int> getCaretRectangleForCharIndex (int index) const override;
/** Selects a section of the text. */
void setHighlightedRegion (const Range<int>& newSelection) override;
@ -467,12 +466,22 @@ public:
*/
int getTextIndexAt (int x, int y) const;
/** Finds the index of the character at a given position.
The coordinates are relative to the component's top-left.
*/
int getTextIndexAt (Point<int>) const;
/** Like getTextIndexAt, but doesn't snap to the beginning/end of the range for
points vertically outside the text.
*/
int getCharIndexForPoint (Point<int> point) const override;
/** Counts the number of characters in the text.
This is quicker than getting the text as a string if you just need to know
the length.
*/
int getTotalNumChars() const;
int getTotalNumChars() const override;
/** Returns the total width of the text, as it is currently laid-out.
@ -541,7 +550,7 @@ public:
The bounds are relative to the component's top-left and may extend beyond the bounds
of the component if the text is long and word wrapping is disabled.
*/
RectangleList<int> getTextBounds (Range<int> textRange);
RectangleList<int> getTextBounds (Range<int> textRange) const override;
//==============================================================================
void moveCaretToEnd();
@ -826,7 +835,6 @@ private:
void reinsert (int insertIndex, const OwnedArray<UniformTextSection>&);
void remove (Range<int>, UndoManager*, int caretPositionToMoveTo);
void getCharPosition (int index, Point<float>&, float& lineHeight) const;
Rectangle<float> getCaretRectangleFloat() const;
void updateCaretPosition();
void updateValueFromText();
void textWasChangedByValue();

View file

@ -71,21 +71,7 @@ private:
void setSelection (Range<int> r) override
{
if (r == codeEditorComponent.getHighlightedRegion())
return;
if (r.isEmpty())
{
codeEditorComponent.caretPos.setPosition (r.getStart());
return;
}
auto& doc = codeEditorComponent.document;
const auto cursorAtStart = r.getEnd() == codeEditorComponent.getHighlightedRegion().getStart()
|| r.getEnd() == codeEditorComponent.getHighlightedRegion().getEnd();
codeEditorComponent.selectRegion (CodeDocument::Position (doc, cursorAtStart ? r.getEnd() : r.getStart()),
CodeDocument::Position (doc, cursorAtStart ? r.getStart() : r.getEnd()));
codeEditorComponent.setHighlightedRegion (r);
}
String getText (Range<int> r) const override
@ -108,32 +94,7 @@ private:
RectangleList<int> getTextBounds (Range<int> textRange) const override
{
auto& doc = codeEditorComponent.document;
RectangleList<int> localRects;
CodeDocument::Position startPosition (doc, textRange.getStart());
CodeDocument::Position endPosition (doc, textRange.getEnd());
for (int line = startPosition.getLineNumber(); line <= endPosition.getLineNumber(); ++line)
{
CodeDocument::Position lineStart (doc, line, 0);
CodeDocument::Position lineEnd (doc, line, doc.getLine (line).length());
if (line == startPosition.getLineNumber())
lineStart = lineStart.movedBy (startPosition.getIndexInLine());
if (line == endPosition.getLineNumber())
lineEnd = { doc, line, endPosition.getIndexInLine() };
auto startPos = codeEditorComponent.getCharacterBounds (lineStart).getTopLeft();
auto endPos = codeEditorComponent.getCharacterBounds (lineEnd).getTopLeft();
localRects.add (startPos.x,
startPos.y,
jmax (1, endPos.x - startPos.x),
codeEditorComponent.getLineHeight());
}
const auto localRects = codeEditorComponent.getTextBounds (textRange);
RectangleList<int> globalRects;
@ -213,11 +174,23 @@ public:
return true;
}
void getHighlightArea (RectangleList<float>& area, float x, int y, int lineH, float characterWidth) const
Optional<Rectangle<float>> getHighlightArea (float x, int y, int lineH, float characterWidth) const
{
if (highlightColumnStart < highlightColumnEnd)
area.add (Rectangle<float> (x + (float) highlightColumnStart * characterWidth - 1.0f, (float) y - 0.5f,
(float) (highlightColumnEnd - highlightColumnStart) * characterWidth + 1.5f, (float) lineH + 1.0f));
return getHighlightArea (x, y, lineH, characterWidth, { highlightColumnStart, highlightColumnEnd });
}
Optional<Rectangle<float>> getHighlightArea (float x,
int y,
int lineH,
float characterWidth,
Range<int> highlightColumns) const
{
if (highlightColumns.isEmpty())
return {};
return Rectangle<float> (x + (float) highlightColumns.getStart() * characterWidth - 1.0f, (float) y - 0.5f,
(float) (highlightColumns.getEnd() - highlightColumns.getStart()) * characterWidth + 1.5f, (float) lineH + 1.0f);
}
void draw (CodeEditorComponent& owner, Graphics& g, const Font& fontToUse,
@ -533,14 +506,6 @@ void CodeEditorComponent::setTemporaryUnderlining (const Array<Range<int>>&)
jassertfalse; // TODO Windows IME not yet supported for this comp..
}
Rectangle<int> CodeEditorComponent::getCaretRectangle()
{
if (caret != nullptr)
return getLocalArea (caret.get(), caret->getLocalBounds());
return {};
}
void CodeEditorComponent::setLineNumbersShown (const bool shouldBeShown)
{
if (showLineNumbers != shouldBeShown)
@ -598,25 +563,26 @@ void CodeEditorComponent::paint (Graphics& g)
{
g.fillAll (findColour (CodeEditorComponent::backgroundColourId));
auto gutterSize = getGutterSize();
auto bottom = horizontalScrollBar.isVisible() ? horizontalScrollBar.getY() : getHeight();
auto right = verticalScrollBar.isVisible() ? verticalScrollBar.getX() : getWidth();
const auto gutterSize = getGutterSize();
const auto bottom = horizontalScrollBar.isVisible() ? horizontalScrollBar.getY() : getHeight();
const auto right = verticalScrollBar.isVisible() ? verticalScrollBar.getX() : getWidth();
g.reduceClipRegion (gutterSize, 0, right - gutterSize, bottom);
g.setFont (font);
auto clip = g.getClipBounds();
auto firstLineToDraw = jmax (0, clip.getY() / lineHeight);
auto lastLineToDraw = jmin (lines.size(), clip.getBottom() / lineHeight + 1);
auto x = (float) (gutterSize - xOffset * charWidth);
auto rightClip = (float) clip.getRight();
const auto clip = g.getClipBounds();
const auto firstLineToDraw = jmax (0, clip.getY() / lineHeight);
const auto lastLineToDraw = jmin (lines.size(), clip.getBottom() / lineHeight + 1);
const auto x = (float) (gutterSize - xOffset * charWidth);
const auto rightClip = (float) clip.getRight();
{
RectangleList<float> highlightArea;
for (int i = firstLineToDraw; i < lastLineToDraw; ++i)
lines.getUnchecked(i)->getHighlightArea (highlightArea, x, lineHeight * i, lineHeight, charWidth);
if (const auto area = lines.getUnchecked(i)->getHighlightArea (x, lineHeight * i, lineHeight, charWidth))
highlightArea.add (*area);
g.setColour (findColour (CodeEditorComponent::highlightColourId));
g.fillRectList (highlightArea);
@ -893,6 +859,37 @@ CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y) const
return CodeDocument::Position (document, line, index);
}
int CodeEditorComponent::getCharIndexForPoint (Point<int> point) const
{
return getPositionAt (point.x, point.y).getPosition();
}
RectangleList<int> CodeEditorComponent::getTextBounds (Range<int> textRange) const
{
RectangleList<int> localRects;
const CodeDocument::Position startPosition (document, textRange.getStart());
const CodeDocument::Position endPosition (document, textRange.getEnd());
for (int line = startPosition.getLineNumber(); line <= endPosition.getLineNumber(); ++line)
{
const CodeDocument::Position lineStartColumn0 { document, line, 0 };
const auto lineStart = line == startPosition.getLineNumber() ? lineStartColumn0.movedBy (startPosition.getIndexInLine())
: lineStartColumn0;
const CodeDocument::Position lineEnd { document, line, line == endPosition.getLineNumber() ? endPosition.getIndexInLine()
: document.getLine (line).length() };
const auto startPos = getCharacterBounds (lineStart).getTopLeft();
const auto endPos = getCharacterBounds (lineEnd) .getTopLeft();
localRects.add (startPos.x, startPos.y, jmax (1, endPos.x - startPos.x), getLineHeight());
}
return localRects;
}
//==============================================================================
void CodeEditorComponent::insertTextAtCaret (const String& newText)
{
@ -1336,8 +1333,13 @@ bool CodeEditorComponent::isHighlightActive() const noexcept
void CodeEditorComponent::setHighlightedRegion (const Range<int>& newRange)
{
selectRegion (CodeDocument::Position (document, newRange.getStart()),
CodeDocument::Position (document, newRange.getEnd()));
if (newRange == getHighlightedRegion())
return;
const auto cursorAtStart = newRange.getEnd() == getHighlightedRegion().getStart()
|| newRange.getEnd() == getHighlightedRegion().getEnd();
selectRegion (CodeDocument::Position (document, cursorAtStart ? newRange.getEnd() : newRange.getStart()),
CodeDocument::Position (document, cursorAtStart ? newRange.getStart() : newRange.getEnd()));
}
String CodeEditorComponent::getTextInRange (const Range<int>& range) const

View file

@ -92,8 +92,8 @@ public:
/** Returns the current caret position. */
CodeDocument::Position getCaretPos() const { return caretPos; }
/** Returns the position of the caret, relative to the editor's origin. */
Rectangle<int> getCaretRectangle() override;
/** Returns the total number of codepoints in the string. */
int getTotalNumChars() const override { return document.getNumCharacters(); }
/** Moves the caret.
If selecting is true, the section of the document between the current
@ -121,6 +121,26 @@ public:
/** Enables or disables the line-number display in the gutter. */
void setLineNumbersShown (bool shouldBeShown);
/** Returns the number of characters from the beginning of the document to the caret. */
int getCaretPosition() const override { return getCaretPos().getPosition(); }
/** @see getPositionAt */
int getCharIndexForPoint (Point<int> point) const override;
/** Returns the bounds of the caret at a particular location in the text. */
Rectangle<int> getCaretRectangleForCharIndex (int index) const override
{
return getCharacterBounds ({ document, index });
}
/** Returns the bounding box for a range of text in the editor. As the range may span
multiple lines, this method returns a RectangleList.
The bounds are relative to the component's top-left and may extend beyond the bounds
of the component if the text is long and word wrapping is disabled.
*/
RectangleList<int> getTextBounds (Range<int> textRange) const override;
//==============================================================================
bool moveCaretLeft (bool moveInWholeWordSteps, bool selecting);
bool moveCaretRight (bool moveInWholeWordSteps, bool selecting);