diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 6c10fb41a..9170b1db5 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -721,8 +721,9 @@ var kDelegateRespondsTo_textShouldBeginEditing For \c -toggleRuler:, the item's title becomes "Hide Ruler" or "Show Ruler" to reflect the ruler's visibility, and the item is enabled only when the text view uses a ruler and lies in a scroll view. \c -copyRuler: is enabled in rich text, and \c -pasteRuler: in editable - rich text when the ruler pasteboard holds a ruler. The superscript, subscript and baseline - actions are enabled in editable rich text. Every other item is enabled. + rich text when the ruler pasteboard holds a ruler. \c -copyFont: is enabled in rich text, and + \c -pasteFont: in editable rich text when the font pasteboard holds a style. The superscript, + subscript and baseline actions are enabled in editable rich text. Every other item is enabled. @param anItem the item to validate @return \c YES if the item should be enabled */ @@ -748,6 +749,13 @@ var kDelegateRespondsTo_textShouldBeginEditing if (action === @selector(pasteRuler:)) return [self isRichText] && [self isEditable] && !![[CPPasteboard pasteboardWithName:CPRulerPboard] availableTypeFromArray:[CPRulerPboardType]]; + // As in Cocoa, styles can be copied and pasted only in rich text. + if (action === @selector(copyFont:)) + return [self isRichText]; + + if (action === @selector(pasteFont:)) + return [self isRichText] && [self isEditable] && !![[CPPasteboard pasteboardWithName:CPFontPboard] availableTypeFromArray:[CPFontPboardType]]; + if (action === @selector(toggleRuler:)) { if ([anItem respondsToSelector:@selector(setTitle:)]) @@ -2080,6 +2088,104 @@ Sets the selection to a range of characters in response to user action. [[CPFontManager sharedFontManager] addFontTrait:CPItalicFontMask]; } +// MARK: - +// MARK: Style methods + +/*! + Copies the character attributes at the start of the selection, or the typing attributes + when nothing is selected, to the font pasteboard, so that Paste Style can apply them to + other text. The attributes include the font, colours, underline and baseline. As in Cocoa, + the paragraph style is not included: Copy Ruler copies it. + @param aSender the object requesting this +*/ +- (@action)copyFont:(id)aSender +{ + var attributes = _selectionRange.length ? [_textStorage attributesAtIndex:_selectionRange.location effectiveRange:nil] : _typingAttributes, + pasteboard = [CPPasteboard pasteboardWithName:CPFontPboard]; + + [pasteboard declareTypes:[CPFontPboardType] owner:nil]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self _characterStyleFromAttributes:attributes]] forType:CPFontPboardType]; +} + +/*! + Applies the character attributes on the font pasteboard to the selected text, or to the + text typed next when nothing is selected. They replace the text's own character + attributes. Each paragraph keeps its paragraph style. + @param aSender the object requesting this +*/ +- (@action)pasteFont:(id)aSender +{ + var data = [[CPPasteboard pasteboardWithName:CPFontPboard] dataForType:CPFontPboardType]; + + if (!data) + return; + + var characterStyle = [CPKeyedUnarchiver unarchiveObjectWithData:data], + range = CPMakeRangeCopy(_selectionRange); + + if (!range.length) + { + var typingAttributes = [characterStyle mutableCopy], + paragraphStyle = [_typingAttributes objectForKey:CPParagraphStyleAttributeName]; + + if (paragraphStyle) + [typingAttributes setObject:paragraphStyle forKey:CPParagraphStyleAttributeName]; + + [self setTypingAttributes:typingAttributes]; + return; + } + + if (![self _beginCharacterAttributeChangeInRange:range]) + return; + + // Each run keeps the attributes that are not part of a style: its paragraph style and, for an + // attachment character, the attachment. + var location = range.location, + end = CPMaxRange(range); + + while (location < end) + { + var effectiveRange = CPMakeRange(0, 0), + runAttributes = [_textStorage attributesAtIndex:location effectiveRange:effectiveRange], + runRange = CPIntersectionRange(effectiveRange, range), + newAttributes = [characterStyle mutableCopy], + keptKeys = [CPParagraphStyleAttributeName, CPAttachmentAttributeName, _CPAttachmentView, _CPAttachmentInvisible]; + + if (!runRange.length) + runRange = CPMakeRange(location, end - location); + + for (var i = 0; i < keptKeys.length; i++) + { + var value = [runAttributes objectForKey:keptKeys[i]]; + + if (value) + [newAttributes setObject:value forKey:keptKeys[i]]; + } + + [_textStorage setAttributes:newAttributes range:runRange]; + + location = CPMaxRange(runRange); + } + + [self _endCharacterAttributeChangeInRange:range]; +} + +/* @ignore + Returns a copy of someAttributes without the attributes that are not part of a character + style: the paragraph style and the attachment. +*/ +- (CPDictionary)_characterStyleFromAttributes:(CPDictionary)someAttributes +{ + var characterStyle = [someAttributes mutableCopy] || [CPDictionary dictionary]; + + [characterStyle removeObjectForKey:CPParagraphStyleAttributeName]; + [characterStyle removeObjectForKey:CPAttachmentAttributeName]; + [characterStyle removeObjectForKey:_CPAttachmentView]; + [characterStyle removeObjectForKey:_CPAttachmentInvisible]; + + return characterStyle; +} + // MARK: - // MARK: Baseline methods diff --git a/Tests/AppKit/CPTextViewTest.j b/Tests/AppKit/CPTextViewTest.j index 4c79fb6d5..876046d6c 100644 --- a/Tests/AppKit/CPTextViewTest.j +++ b/Tests/AppKit/CPTextViewTest.j @@ -208,6 +208,68 @@ [self assertTrue:([[textView typingAttributes] objectForKey:CPSuperscriptAttributeName] == nil)]; } +- (void)testCopyAndPasteStyle +{ + var font = [CPFont boldSystemFontOfSize:20.0], + color = [CPColor redColor], + paragraphStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + + [paragraphStyle setAlignment:CPCenterTextAlignment]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttributes:@{CPFontAttributeName: font, CPForegroundColorAttributeName: color} range:CPMakeRange(0, 2)]; + [[textView textStorage] addAttribute:CPParagraphStyleAttributeName value:paragraphStyle range:CPMakeRange(3, 6)]; + + [textView setSelectedRange:CPMakeRange(0, 2)]; + [textView copyFont:nil]; + + [textView setSelectedRange:CPMakeRange(3, 6)]; + [textView pasteFont:nil]; + + var storage = [textView textStorage]; + + [self assert:[font cssString] equals:[[storage attribute:CPFontAttributeName atIndex:5 effectiveRange:nil] cssString]]; + [self assert:[color cssString] equals:[[storage attribute:CPForegroundColorAttributeName atIndex:5 effectiveRange:nil] cssString]]; + [self assert:CPCenterTextAlignment equals:[[storage attribute:CPParagraphStyleAttributeName atIndex:5 effectiveRange:nil] alignment] message:@"Paste Style keeps the paragraph style"]; + [self assertTrue:([storage attribute:CPForegroundColorAttributeName atIndex:9 effectiveRange:nil] == nil) message:@"Paste Style changes only the selection"]; +} + +- (void)testPasteStyleWithoutSelectionChangesTypingAttributes +{ + var font = [CPFont boldSystemFontOfSize:20.0]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttribute:CPFontAttributeName value:font range:CPMakeRange(0, 2)]; + + [textView setSelectedRange:CPMakeRange(0, 2)]; + [textView copyFont:nil]; + + [textView setSelectedRange:CPMakeRange(5, 0)]; + [textView pasteFont:nil]; + + [self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString]]; +} + +- (void)testStyleActionsAreEnabledOnlyInRichText +{ + var copyItem = [[CPMenuItem alloc] initWithTitle:@"Copy Style" action:@selector(copyFont:) keyEquivalent:@""], + pasteItem = [[CPMenuItem alloc] initWithTitle:@"Paste Style" action:@selector(pasteFont:) keyEquivalent:@""]; + + [textView setRichText:NO]; + [self assertFalse:[textView validateUserInterfaceItem:copyItem]]; + [self assertFalse:[textView validateUserInterfaceItem:pasteItem]]; + + [textView setRichText:YES]; + [textView copyFont:nil]; + [self assertTrue:[textView validateUserInterfaceItem:copyItem]]; + [self assertTrue:[textView validateUserInterfaceItem:pasteItem]]; + + [textView setEditable:NO]; + [self assertFalse:[textView validateUserInterfaceItem:pasteItem]]; +} + - (void)testBaselineActionsAreEnabledOnlyInEditableRichText { var item = [[CPMenuItem alloc] initWithTitle:@"Superscript" action:@selector(superscript:) keyEquivalent:@""]; diff --git a/Tests/Manual/TextEditor/WORKLIST.md b/Tests/Manual/TextEditor/WORKLIST.md index 01afce87b..5d0b31859 100644 --- a/Tests/Manual/TextEditor/WORKLIST.md +++ b/Tests/Manual/TextEditor/WORKLIST.md @@ -28,7 +28,6 @@ TextEditor is a document-based test application and a tutorial. When the app use How the text system works, for the entries below: `CPTextView` does not use CSS text layout. `CPSimpleTypesetter` measures text with the canvas `measureText` (`sizeWithFont:inWidth:` in `CPStringDrawing.j`), measuring growing substrings so that each character's advancement includes the browser's kerning. It then breaks lines, places tabs, and applies indents and alignment itself (`CPTypesetter.j`). `_CPLineFragment` (`CPLayoutManager.j`) draws each run of identical attributes as one absolutely positioned ``, at the computed position, with `white-space: pre` and the run's font, colour and underline in CSS. Inside a span the browser shapes the glyphs, including its own kerning and ligatures. -- **Format > Font > Copy Style and Paste Style raise an exception.** They send `copyFont:` and `pasteFont:`, which `CPText` declares as abstract methods that raise (`CPText.j:210` and `:261`). `CPTextView` does not implement them, so the menu items are enabled and choosing one raises. `CPTextView` needs Cocoa's implementations: copy the font and character attributes at the selection to the font pasteboard (`CPFontPboard`), and apply them to the selection. - **Kerning and ligature settings are not implemented.** Format > Font > Kern sends `useStandardKerning:`, `turnOffKerning:`, `tightenKerning:` and `loosenKerning:`, and Format > Font > Ligatures sends `useStandardLigatures:`, `turnOffLigatures:` and `useAllLigatures:`. `CPTextView` implements none of them, so the items are disabled. `CPText.j` defines `CPKernAttributeName` and `CPLigatureAttributeName`, but only the RTF producer reads them. A complete implementation needs: the seven actions, changing the attributes over the selection or the typing attributes with undo, as the baseline actions do; the typesetter adding a run's `CPKernAttributeName` value to each character's advancement, and measuring with the same ligature setting it draws with; and `_CPLineFragment` setting CSS `letter-spacing` and `font-variant-ligatures` on each run's span to match. - **The font panel needs polish.** Found during the Format menu review on 24 October: the panel works with the text view, but its appearance and behaviour need refinement. The specific problems are not yet recorded; list them here, compared with the native font panel, when the panel is reviewed. `CPFontPanel` is in `AppKit/CPTextView/CPFontPanel.j`. @@ -83,4 +82,5 @@ How the text system works, for the entries below: `CPTextView` does not use CSS - Format > Text > Copy Ruler and Paste Ruler apply one paragraph's indents and tab stops to others. - Format > Font > Baseline: superscript, subscript, raised and lowered baselines lay out correctly, and Use Default restores the text, for an exact or a wider selection. - Format > Text > Justify: wrapped lines fill the width between the indents, with the spare width spread over the spaces; the last line of a paragraph stays left aligned. Centred text is centred between the indents, and spaces at the end of a wrapped line hang outside the alignment. +- Format > Font > Copy Style and Paste Style: the style of the first character of the selection is applied to the whole target selection, which keeps its paragraph style. - Builds with both the legacy toolchain and capp-build.