diff options
author | Neil Hodgson <nyamatongwe@gmail.com> | 2016-11-05 20:42:12 +1100 |
---|---|---|
committer | Neil Hodgson <nyamatongwe@gmail.com> | 2016-11-05 20:42:12 +1100 |
commit | ab0bd10bf685d2868b245893afd7512ddadc5fb1 (patch) | |
tree | 2512c11643a54285979a0dd67103fb79962b88e2 | |
parent | 8a62263409f5d222ac0d0ccf7bf0e7e0261224a8 (diff) | |
download | scintilla-mirror-ab0bd10bf685d2868b245893afd7512ddadc5fb1.tar.gz |
Support for NSAccessibility protocol added sufficient for the VoiceOver
screen reader.
-rw-r--r-- | cocoa/ScintillaCocoa.h | 8 | ||||
-rw-r--r-- | cocoa/ScintillaCocoa.mm | 95 | ||||
-rw-r--r-- | cocoa/ScintillaView.h | 3 | ||||
-rw-r--r-- | cocoa/ScintillaView.mm | 231 | ||||
-rw-r--r-- | doc/ScintillaDoc.html | 19 | ||||
-rw-r--r-- | doc/ScintillaHistory.html | 2 |
6 files changed, 344 insertions, 14 deletions
diff --git a/cocoa/ScintillaCocoa.h b/cocoa/ScintillaCocoa.h index 1bf05c9f0..25b9807d7 100644 --- a/cocoa/ScintillaCocoa.h +++ b/cocoa/ScintillaCocoa.h @@ -196,8 +196,12 @@ public: virtual void IdleWork(); virtual void QueueIdleWork(WorkNeeded::workItems items, int upTo); int InsertText(NSString* input); - NSRange PositionsFromCharacters(NSRange range) const; - NSRange CharactersFromPositions(NSRange range) const; + NSRange PositionsFromCharacters(NSRange rangeCharacters) const; + NSRange CharactersFromPositions(NSRange rangePositions) const; + NSString *RangeTextAsString(NSRange rangePositions) const; + NSInteger VisibleLineForIndex(NSInteger index); + NSRange RangeForVisibleLine(NSInteger lineVisible); + NSRect FrameForRange(NSRange rangeCharacters); void SelectOnlyMainSelection(); void ConvertSelectionVirtualSpace(); bool ClearAllSelections(); diff --git a/cocoa/ScintillaCocoa.mm b/cocoa/ScintillaCocoa.mm index 20f0e4440..3ae4ab575 100644 --- a/cocoa/ScintillaCocoa.mm +++ b/cocoa/ScintillaCocoa.mm @@ -1689,6 +1689,79 @@ int ScintillaCocoa::TargetAsUTF8(char *text) //-------------------------------------------------------------------------------------------------- +// Returns the text in the range converted to an NSString. +NSString *ScintillaCocoa::RangeTextAsString(NSRange rangePositions) const { + const std::string text = RangeText(static_cast<int>(rangePositions.location), + static_cast<int>(NSMaxRange(rangePositions))); + if (IsUnicodeMode()) + { + return [NSString stringWithUTF8String: text.c_str()]; + } + else + { + // Need to convert + const CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), + vs.styles[STYLE_DEFAULT].characterSet); + CFStringRef cfsVal = CFStringCreateWithBytes(kCFAllocatorDefault, + reinterpret_cast<const UInt8 *>(text.c_str()), + text.length(), encoding, false); + + return (NSString *)cfsVal; + } +} + +//-------------------------------------------------------------------------------------------------- + +// Return character range of a line. +NSRange ScintillaCocoa::RangeForVisibleLine(NSInteger lineVisible) { + const Range posRangeLine = RangeDisplayLine(static_cast<int>(lineVisible)); + return CharactersFromPositions(NSMakeRange(posRangeLine.First(), + posRangeLine.Last() - posRangeLine.First())); +} + +//-------------------------------------------------------------------------------------------------- + +// Returns visible line number of a text position in characters. +NSInteger ScintillaCocoa::VisibleLineForIndex(NSInteger index) { + const NSRange rangePosition = PositionsFromCharacters(NSMakeRange(index, 0)); + const int lineVisible = DisplayFromPosition(static_cast<int>(rangePosition.location)); + return lineVisible; +} + +//-------------------------------------------------------------------------------------------------- + +// Returns a rectangle that frames the range for use by the VoiceOver cursor. +NSRect ScintillaCocoa::FrameForRange(NSRange rangeCharacters) { + const NSRange posRange = PositionsFromCharacters(rangeCharacters); + + NSUInteger rangeEnd = NSMaxRange(posRange); + const bool endsWithLineEnd = rangeCharacters.length && + (pdoc->GetColumn(static_cast<int>(rangeEnd)) == 0); + + Point ptStart = LocationFromPosition(static_cast<int>(posRange.location)); + const PointEnd peEndRange = static_cast<PointEnd>(peSubLineEnd|peLineEnd); + Point ptEnd = LocationFromPosition(static_cast<int>(rangeEnd), peEndRange); + + NSRect rect = NSMakeRect(ptStart.x, ptStart.y, + ptEnd.x - ptStart.x, + ptEnd.y - ptStart.y); + + rect.size.width += 2; // Shows the last character better + if (endsWithLineEnd) { + // Add a block to the right to indicate a line end is selected + rect.size.width += 20; + } + + rect.size.height += vs.lineHeight; + + // Adjust for margin and scroll + rect.origin.x = rect.origin.x - vs.textStart + vs.fixedColumnWidth; + + return rect; +} + +//-------------------------------------------------------------------------------------------------- + // Translates a UTF8 string into the document encoding. // Return the length of the result in bytes. int ScintillaCocoa::EncodedFromUTF8(char *utf8, char *encoded) const @@ -2013,6 +2086,15 @@ void ScintillaCocoa::NotifyParent(SCNotification scn) notifyProc(notifyObj, WM_NOTIFY, GetCtrlID(), (uintptr_t) &scn); if (delegate) [delegate notification:&scn]; + if (scn.nmhdr.code == SCN_UPDATEUI) { + NSView *content = ContentView(); + if (scn.updated & SC_UPDATE_CONTENT) { + NSAccessibilityPostNotification(content, NSAccessibilityValueChangedNotification); + } + if (scn.updated & SC_UPDATE_SELECTION) { + NSAccessibilityPostNotification(content, NSAccessibilitySelectedTextChangedNotification); + } + } } //-------------------------------------------------------------------------------------------------- @@ -2217,12 +2299,12 @@ int ScintillaCocoa::InsertText(NSString* input) /** * Convert from a range of characters to a range of bytes. */ -NSRange ScintillaCocoa::PositionsFromCharacters(NSRange range) const +NSRange ScintillaCocoa::PositionsFromCharacters(NSRange rangeCharacters) const { - long start = pdoc->GetRelativePositionUTF16(0, static_cast<int>(range.location)); + long start = pdoc->GetRelativePositionUTF16(0, static_cast<int>(rangeCharacters.location)); if (start == INVALID_POSITION) start = pdoc->Length(); - long end = pdoc->GetRelativePositionUTF16(static_cast<int>(start), static_cast<int>(range.length)); + long end = pdoc->GetRelativePositionUTF16(static_cast<int>(start), static_cast<int>(rangeCharacters.length)); if (end == INVALID_POSITION) end = pdoc->Length(); return NSMakeRange(start, end - start); @@ -2233,10 +2315,11 @@ NSRange ScintillaCocoa::PositionsFromCharacters(NSRange range) const /** * Convert from a range of characters from a range of bytes. */ -NSRange ScintillaCocoa::CharactersFromPositions(NSRange range) const +NSRange ScintillaCocoa::CharactersFromPositions(NSRange rangePositions) const { - const long start = pdoc->CountUTF16(0, static_cast<int>(range.location)); - const long len = pdoc->CountUTF16(static_cast<int>(range.location), static_cast<int>(NSMaxRange(range))); + const long start = pdoc->CountUTF16(0, static_cast<int>(rangePositions.location)); + const long len = pdoc->CountUTF16(static_cast<int>(rangePositions.location), + static_cast<int>(NSMaxRange(rangePositions))); return NSMakeRange(start, len); } diff --git a/cocoa/ScintillaView.h b/cocoa/ScintillaView.h index ec2c8ee14..079e42116 100644 --- a/cocoa/ScintillaView.h +++ b/cocoa/ScintillaView.h @@ -74,7 +74,8 @@ extern NSString *const SCIUpdateUINotification; NSTextInputClient, NSUserInterfaceValidations, NSDraggingSource, - NSDraggingDestination> + NSDraggingDestination, + NSAccessibilityStaticText> { @private ScintillaView* mOwner; diff --git a/cocoa/ScintillaView.mm b/cocoa/ScintillaView.mm index 51cb0af5f..491aba572 100644 --- a/cocoa/ScintillaView.mm +++ b/cocoa/ScintillaView.mm @@ -64,6 +64,7 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) [currentCursors addObject: [reverseArrowCursor retain]]; } [self setClientView:[aScrollView documentView]]; + self.accessibilityLabel = @"Scintilla Margin"; } return self; } @@ -122,6 +123,12 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) owner.backend->MouseUp(theEvent); } +// Not a simple button so return failure +- (BOOL)accessibilityPerformPress +{ + return NO; +} + /** * This method is called to give us the opportunity to define our mouse sensitive rectangle. */ @@ -167,6 +174,14 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) [self registerForDraggedTypes: [NSArray arrayWithObjects: NSStringPboardType, ScintillaRecPboardType, NSFilenamesPboardType, nil]]; + + // Set up accessibility in the text role + self.accessibilityElement = TRUE; + self.accessibilityEnabled = TRUE; + self.accessibilityLabel = NSLocalizedString(@"Scintilla", nil); // No real localization + self.accessibilityRoleDescription = @"source code editor"; + self.accessibilityRole = NSAccessibilityTextAreaRole; + self.accessibilityIdentifier = @"Scintilla"; } return self; @@ -931,6 +946,222 @@ sourceOperationMaskForDraggingContext: (NSDraggingContext) context return mOwner.backend->WndProc(SCI_GETREADONLY, 0, 0) == 0; } +#pragma mark - NSAccessibility + +//-------------------------------------------------------------------------------------------------- + +// Adoption of NSAccessibility protocol. +// NSAccessibility wants to pass ranges in UTF-16 code units, not bytes (like Scintilla) +// or characters. +// Needs more testing with non-ASCII and non-BMP text. +// Needs to take account of folding and wraping. + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Text of the whole document as a string. + */ +- (id) accessibilityValue { + const sptr_t length = [mOwner message: SCI_GETLENGTH]; + return mOwner.backend->RangeTextAsString(NSMakeRange(0,static_cast<int>(length))); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Line of the caret. + */ +- (NSInteger) accessibilityInsertionPointLineNumber { + const int caret = static_cast<int>([mOwner message: SCI_GETCURRENTPOS]); + const NSRange rangeCharactersCaret = mOwner.backend->CharactersFromPositions(NSMakeRange(caret, 0)); + return mOwner.backend->VisibleLineForIndex(rangeCharactersCaret.location); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Not implemented and not called by VoiceOver. + */ +- (NSRange)accessibilityRangeForPosition:(NSPoint)point { + return NSMakeRange(0,0); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Number of characters in the whole document. + */ +- (NSInteger) accessibilityNumberOfCharacters { + sptr_t length = [mOwner message: SCI_GETLENGTH]; + const NSRange posRange = mOwner.backend->CharactersFromPositions(NSMakeRange(length, 0)); + return posRange.location; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : The selection text as a string. + */ +- (NSString *) accessibilitySelectedText { + const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; + const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; + const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); + return mOwner.backend->RangeTextAsString(posRangeSel); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : The character range of the main selection. + */ +- (NSRange) accessibilitySelectedTextRange { + const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; + const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; + const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); + return mOwner.backend->CharactersFromPositions(posRangeSel); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : The setter for accessibilitySelectedTextRange. + * This method is the only setter required for reasonable VoiceOver behaviour. + */ +- (void) setAccessibilitySelectedTextRange: (NSRange) range { + NSRange rangePositions = mOwner.backend->PositionsFromCharacters(range); + [mOwner message: SCI_SETSELECTION wParam: rangePositions.location lParam:NSMaxRange(rangePositions)]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Range of the glyph at a character index. + * Currently doesn't try to handle composite characters. + */ +- (NSRange) accessibilityRangeForIndex: (NSInteger)index { + sptr_t length = [mOwner message: SCI_GETLENGTH]; + const NSRange rangeLength = mOwner.backend->CharactersFromPositions(NSMakeRange(length, 0)); + NSRange rangePositions = NSMakeRange(length, 0); + if (index < rangeLength.location) { + rangePositions = mOwner.backend->PositionsFromCharacters(NSMakeRange(index, 1)); + } + return mOwner.backend->CharactersFromPositions(rangePositions); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : All the text ranges. + * Currently only returns the main selection. + */ +- (NSArray<NSValue *>*) accessibilitySelectedTextRanges { + const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; + const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; + const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); + NSRange rangeCharacters = mOwner.backend->CharactersFromPositions(posRangeSel); + NSValue *valueRange = [NSValue valueWithRange:(NSRange)rangeCharacters]; + return @[valueRange]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Character range currently visible. + */ +- (NSRange) accessibilityVisibleCharacterRange { + const sptr_t lineTopVisible = [mOwner message: SCI_GETFIRSTVISIBLELINE]; + const sptr_t lineTop = [mOwner message:SCI_DOCLINEFROMVISIBLE wParam:lineTopVisible]; + const sptr_t lineEndVisible = lineTopVisible + [mOwner message: SCI_LINESONSCREEN] - 1; + const sptr_t lineEnd = [mOwner message:SCI_DOCLINEFROMVISIBLE wParam:lineEndVisible]; + const sptr_t posStartView = [mOwner message: SCI_POSITIONFROMLINE wParam: lineTop]; + const sptr_t posEndView = [mOwner message: SCI_GETLINEENDPOSITION wParam: lineEnd]; + const NSRange posRangeSel = NSMakeRange(posStartView, posEndView-posStartView); + return mOwner.backend->CharactersFromPositions(posRangeSel); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Character range of a line. + */ +- (NSRange)accessibilityRangeForLine:(NSInteger)line { + return mOwner.backend->RangeForVisibleLine(line); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Line number of a text position in characters. + */ +- (NSInteger)accessibilityLineForIndex:(NSInteger)index { + return mOwner.backend->VisibleLineForIndex(index); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : A rectangle that covers a range which will be shown as the + * VoiceOver cursor. + * Producing a nice rectangle is a little tricky particularly when including new + * lines. Needs to improve the case where parts of two lines are included. + */ +- (NSRect)accessibilityFrameForRange:(NSRange)range { + const NSRect rectInView = mOwner.backend->FrameForRange(range); + const NSRect rectInWindow = [[[self superview] superview] convertRect:rectInView toView:nil]; + return [self.window convertRectToScreen:rectInWindow]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : A range of text as a string. + */ +- (NSString *) accessibilityStringForRange:(NSRange)range { + const NSRange posRange = mOwner.backend->PositionsFromCharacters(range); + return mOwner.backend->RangeTextAsString(posRange); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : A range of text as an attributed string. + * Currently no attributes are set. + */ +- (NSAttributedString *) accessibilityAttributedStringForRange:(NSRange)range { + const NSRange posRange = mOwner.backend->PositionsFromCharacters(range); + NSString *result = mOwner.backend->RangeTextAsString(posRange); + return [[[NSMutableAttributedString alloc] initWithString:result] autorelease]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * NSAccessibility : Show the context menu at the caret. + */ +- (BOOL)accessibilityPerformShowMenu { + const sptr_t caret = [mOwner message: SCI_GETCURRENTPOS]; + NSRect rect; + rect.origin.x = [mOwner message: SCI_POINTXFROMPOSITION wParam: 0 lParam: caret]; + rect.origin.y = [mOwner message: SCI_POINTYFROMPOSITION wParam: 0 lParam: caret]; + rect.origin.y += [mOwner message: SCI_TEXTHEIGHT wParam: 0 lParam: 0]; + rect.size.width = 1.0; + rect.size.height = 1.0; + NSRect rectInWindow = [[[self superview] superview] convertRect:rect toView:nil]; + NSPoint pt = rectInWindow.origin; + NSEvent *event = [NSEvent mouseEventWithType: NSRightMouseDown + location: pt + modifierFlags: 0 + timestamp: 0 + windowNumber: [[self window] windowNumber] + context: nil + eventNumber: 0 + clickCount: 1 + pressure: 0.0]; + NSMenu *menu = mOwner.backend->CreateContextMenu(event); + [NSMenu popUpContextMenu:menu withEvent:event forView:self]; + return YES; +} + //-------------------------------------------------------------------------------------------------- - (void) dealloc diff --git a/doc/ScintillaDoc.html b/doc/ScintillaDoc.html index 87064b938..9d60800cd 100644 --- a/doc/ScintillaDoc.html +++ b/doc/ScintillaDoc.html @@ -347,22 +347,24 @@ <tr> <td>o <a class="toc" href="#Notifications">Notifications</a></td> - <td>o <a class="toc" href="#Images">Images</a></td> + <td>o <a class="toc" href="#Accessibility">Accessibility</a></td> - <td>o <a class="toc" href="#GTK">GTK+</a></td> + <td>o <a class="toc" href="#Images">Images</a></td> </tr> <tr> + <td>o <a class="toc" href="#GTK">GTK+</a></td> + <td>o <a class="toc" href="#ProvisionalMessages"><span class="provisional">Provisional messages</span></a></td> <td>o <a class="toc" href="#DeprecatedMessages">Deprecated messages</a></td> + </tr> + <tr> <td>o <a class="toc" href="#EditMessagesNeverSupportedByScintilla">Edit messages never supported by Scintilla</a></td> - </tr> - <tr> <td>o <a class="toc" href="#BuildingScintilla">Building Scintilla</a></td> </tr> @@ -7672,6 +7674,15 @@ for line = lineStart to lineEnd do SCI_ENSUREVISIBLE(line) next <a class="jump" href="#SCN_AUTOCSELECTION">SCN_AUTOCSELECTION</a></code> notification.</p> + <h2 id="Accessibility">Accessibility</h2> + + <p>Scintilla supports some platform accessibility features. + This support differs between platforms. + On GTK+ and Cocoa the platform accessibility APIs are implemented sufficiently to + make screen readers work. + On Win32, the system caret is manipulated to help screen readers. + </p> + <h2 id="Images">Images</h2> <p>Two formats are supported for images used in margin markers and autocompletion lists, RGBA and XPM.</p> diff --git a/doc/ScintillaHistory.html b/doc/ScintillaHistory.html index a1e7a9d82..17880f5ee 100644 --- a/doc/ScintillaHistory.html +++ b/doc/ScintillaHistory.html @@ -524,7 +524,7 @@ Released 16 October 2016. </li> <li> - Accessibility supported on GTK+. + Accessibility support allowing screen readers to work added on GTK+ and Cocoa. </li> <li> INDIC_POINT and INDIC_POINTCHARACTER indicators added to display small arrows |