Subclassing NSTextStorage breaks list editing - cocoa

I have a basic Mac app with a standard NSTextView. I'm trying to implement and use a subclass of NSTextStorage, but even a very basic implementation breaks list editing behavior:
I add a bulleted list with two items
I copy & paste that list further down into the document
Pressing Enter in the pasted list breaks formatting for the last list item.
Here's a quick video:
Two issues:
The bullet points of the pasted list use a smaller font size
Pressing Enter after the second list item breaks the third item
This works fine when I don't replace the text storage.
Here's my code:
ViewController.swift
#IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
TestTextStorage.swift
class TestTextStorage: NSTextStorage {
let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}

You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
Type "aa", hit return, type "bb"
Do select all and format as a numbered list
Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
\t1)\taa\n\t2)\tbb
first the return is inserted:
\t1)\taa\n\n\t2)\tbb
and then an internal routine _reformListAtIndex: is called and it starts "renumbering". First it replaces \t1)\t with \t1) - the number hasn't changed. Then it inserts \t2)\t between the two new lines, as at this point we have:
\t1)\taa\n\t2)\t\n\t2)\tbb
and then it replaces the original \t2)\t with \t3)\t giving:
\t1)\taa\n\t2)\t\n\t3)\tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters() gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in #objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters().
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original \t2)\t to \t3)\t you will see a misstep, the range given for the original \t2)\t is what is was before the new \t2)\t was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original \t2)\t it is doing so on string which hasn't been altered by the previous insertion of the new \t2)\t.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage in Objective-C and go to bugreport.apple.com and report the bug.
HTH (more than it makes you dizzy)

Related

How did I manage to panic when using the Select Widget

New to Go and Fyne, and stumbling trying to get what I need out of Fyne. Sorry, this will be long.
My problem is this. I’m writing a application that gets a list of commands from a server, telling it to create a series of widgets and display them. They are of various types – Label, Button, Entry, Select, etc.
But these aren’t standard widgets; I need to extend their behavior a bit. For one thing, when operated by the user, they each need access to some per-widget information. Button click, for example, has to reference some data specific to that button so it knows what to do. We’ll call this additional information the About struct.
Secondly, every widget needs to be able to take a right click and drop down a widget specific menu. Yes, even buttons and labels need to be able to provide drop down menus. In the case of a widget like Entry, I know this design is going to condemn me to having to write my own menu choices for Paste, Copy and the other operations Entry normally offers on a right click, but I’m ok with that.
I have all this working, but in the process I broke Select (and probably there will be breakage for other widgets.) and I can’t see how to fix it.
Problem: trying to send the Select widget causes a panic: interface conversion: fyne.Canvas is nil, not *glfw.glCanvas
Approach:
type GenericWidget struct {
fyne.Widget //I’m some kind of widget
about *About //here’s my personal “About” data
//function pointers
OnRightClickp func(gw *GenericWidget, pe *fyne.PointEvent)
OnLeftClickp func(gw *GenericWidget, pe *fyne.PointEvent)
…other “function pointers” for OnRunep and so on…
}
And now I have to catch all “events” so GenericWidget will see them:
func (gw *GenericWidget) TappedSecondary(pe *fyne.PointEvent) {
if (gw.OnRightClickp != nil) {gw.OnRightClickp(gw, pe)}
}
func (gw *GenericWidget) Tapped(pe *fyne.PointEvent) {
if (gw.OnLeftClickp != nil){gw.OnLeftClickp(gw, pe)}
}
//type Focusable interface
//etc….
This should represent any single widget, regardless of type. It’s not complicated: When Tapped is invoked by the driver, GenericWidget.Tapped gets called. If this widget has a function pointer for “OnLeftClickp” set, call it. Crucially, we pass a pointer to the widget itself when that happens, because all the event handlers I write will need access to the *About and maybe anything else I add to GenericWidget.
Creation is simple enough:
func NewGenericWidget(w fyne.Widget, about *About) *GenericWidget {
gw := GenericWidget{w, about, nil, nil, nil, nil, nil, nil, nil, nil}
return &gw
}
And when it’s time to create a Label, I do that and fold it into a GenericWidget
func NewExtendedLabelWithStyle( //Label with catachable left and right click
text string,
about *About,
alignment fyne.TextAlign,
style fyne.TextStyle,
tappedLeft func(*GenericWidget, *fyne.PointEvent),
tappedRight func(*GenericWidget, *fyne.PointEvent)) *GenericWidget {
e := NewGenericWidget(widget.NewLabelWithStyle(text, alignment, style), about)
e.OnLeftClickp = tappedLeft
e.OnRightClickp = tappedRight
return e
}
All these GenericWidgets work fine – I add them to the appropriate Box and the window paints as I’d expect. I can right click on a label and if OnRightClickp is set, which is generally is, code gets called and is given access to *GenericWidget, which leads to *About, which means the menu that gets put up can offer all the right stuff for what’s in this label.
Of course, Labels don’t normally care about clicking, so the fact that I’ve stolen all the calls in Tappable doesn’t matter.
But a Select widget does care about left clicks, so the fact that GenericWidget is intercepting the call to Tapped() means I’d never see the dropdown appear.
No problem, I thought. When I create the Select widget and the surrounding GenericWidget, I’ll just specify I want to call Select’s Tapped myself:
func NewExtendedSelect(about *About, sel func(*GenericWidget, string)) *GenericWidget {
//build the options. NewSelect demands a slice of strings, so...
st := make([]string, 64)
for e := about.theList.Front(); e != nil; e = e.Next() {
st = append(st, e.Value.(string))
}
s := widget.NewSelect(st, func(c string){})
//make sure it selects the text it should, initially
s.SetSelected(about.value)
//wrap it
e := NewGenericWidget(s, about)
//e.OnChangedp = sel //not set up yet (and don’t know how)
//HERE BE DRAGONS --------------
//But we don't want to break left click. So when we intercept it,
// pass it down to the actual Selection widget:
e.OnLeftClickp = func(me* GenericWidget, pe *fyne.PointEvent) {
fmt.Println("select tapped")
//call the Tapped that Select knows about:
s.Tapped(pe) //PANIC!
}
// -----------------------------
//Handle right click with our usual menu magic
e.OnRightClickp =func(me* GenericWidget, pe *fyne.PointEvent) {
//defer used because not sure what to do with the return value, so make it Go's problem
defer widget.NewPopUpMenuAtPosition(me.GetRightClickMenu(),
me.about.sheet.window.Canvas(), pe.AbsolutePosition)
}
return e //here's your GenericWidget, ready to drop into a Fyne container.
}
Compiles fine, Selects get displayed, but when I left click it prints the expected “select tapped” and then immediately panics:
select tapped
panic: interface conversion: fyne.Canvas is nil, not *glfw.glCanvas
I’m lost. My GenericWidget is just a widget; I thought that’s what composition did, and all the GenericWidgets I create are put in a Box which is in a Box which is SetContent into the window. But the error is suggesting to me that somehow this Select object wasn’t set up right and when it goes to draw the options, something is missing. What did I do wrong?
It’s possible my whole approach is wrong (I do a lot of C++ and Python and I take an OO view of things). In that case, how do I do all this?
This turns out to have been a Fyne bug, which has since been fixed.
It does not look like the select is being composed - your extended select is just creating a new one, tapping it later.
The crash is coming from it trying to display a pop up next to a select that has not been found on the canvas.
If you are extending builtin widgets you also need to call ExtendBaseWidget so that the driver can lookup your widget instead of the default one.
That said a single widget that can extend any other type of widget kind of goes against the strongly types design of Fyne APIs and you may run in to trouble.

Modify reference content of an object in swift

Given a stackView object, I got an array of stackView.arrangedSubviews
which is a get only property. Hence I can't REPLACE one item of it by merely doing
stackView.arrangedSubviews[i] = newSubView
what came to my mind is I want to replace the content of the reference at stackView.arrangedSubviews[i] with the content of newSubView. For instance if this was in C, I would have done something like
*arrangedSubViews[i] = *newSubView
I know there is a way to do what I need by removing arrangedSubViews and utilizing addArrangedSubView but it won't be very efficient.
You can't modify an immutable array, and this is a good thing in many ways.
You can create an extension method to replace the view like this:
#available(iOS 9.0, *)
extension UIStackView {
func replaceView(atIndex index: Int, withView view: UIView) {
let viewToRemove = arrangedSubviews[index]
removeArrangedSubview(viewToRemove)
insertArrangedSubview(view, atIndex: index)
}
}
Then, instead of using stackView.arrangedSubviews[i] = newSubView you can use this code:
stackView.replaceView(atIndex: i, withView: newSubView)
Performance-wise there's no negative impact on doing it this way.

In OSX accessing (optional?) property "identifier" of NSView subclass leads to bad access

Since I am fairly new to Swift programming on OSX, this question may contain several points that needs clarification.
I have a method which iterates over all subviews of a given NSView instance. For this, I get the array of subviews which is of type [AnyObject] and process one element at a time.
At some point I would like to access the identifier property of each instance. This property is implemented from a protocol in NSView named NSUserInterfaceItemIdentification, which type is given in the documentation as (optional) String?. In order to get that identifier I would have written
var view : NSView = subview as NSView;
var viewIdent : String = view.identifier!;
The second line is marked by the compiler with an error stating that identifier is not of an optional type, but instead of type String, and hence the post-fix operator ! cannot be applied.
Removing this operator compiles fine, but leads to a runtime error EXC_BAD_ACCESS (code=1, address=0x0) because identifier seems to be nil for some NSButton instance.
I cannot even test for this property, because the compiler gives me a String is not convertible to UInt8 while I try
if (view.identifier != nil) {viewIdent = view.identifier;}
My questions are
Is the documentation wrong? I.g. the property identifier is not optional?
How can I ship around this problem and get code that runs robust?
If the documentation states that view.identifier is an Optional, it means it can be nil. So it's not a surprise that for some button instances it is indeed nil for you.
Force unwrapping this element that can be nil will lead your app to crash, you can use safe unwrapping instead:
if let viewIdent = view.identifier {
// do something with viewIdent
} else {
// view.identifier was nil
}
You can easily check the type of an element in Xcode: click on the element while holding the ALT key. It will reveal a popup with informations, including the type. You can verify there that your element is an Optional or not.
Tip: you can safe unwrap several items on one line, it's rather convenient:
if let view = subview as? NSView, viewIdent = view.identifier {
// you can do something here with `viewIdent` because `view` and `view.identifier` were both not nil
} else {
// `view` or `view.identifier` was nil, handle the error here
}
EDIT:
You have to remove this line of yours before using my example:
var viewIdent : String = view.identifier!
Because if you keep this line before my examples, it won't work because you transform what was an Optional in a non-Optional by adding this exclamation mark.
Also it forces casting to a String, but maybe your identifier is an Int instead, so you shouldn't use this kind of declaration but prefer if let ... to safe unwrap and cast the value.
EDIT2:
You say my example doesn't compile... I test every answer I make on SO. I tested this one in a Playground before answering, here's a screenshot:
Also, after checking it, I confirm that the identifier is an Optional String, that's the type given by Xcode when using ALT+CLICK on the property. The documentation is right.
So if it's different for you, it means you have a different problem unrelated to this one; but my answer for this precise question remains the same.

How can I force Xcode to retain my indentation when I copy and paste?

I find myself wasting so much time dealing with Xcodes auto indentation that I have to ask if there is something I just have wrong in my settings. Basically if I spend time indenting code within a method, and then copy that entire method and paste it, the newly pasted method does not retain any of the white space that I applied to the original.
For example, here is a screenshot where the top method I indented all the objects of an array so they are lined up properly. Then I selected the entire method, copied and pasted, and you can see the method below has the indentation all messed up.
I am using Xcode 4.4.1, here are my settings:
Works as intended.
…Objects: and forKeys: should be aligned as they form part of the same method signature.
It might be easier to format your code if you use the new object literal syntax:
- (int)minBrokenPieces {
NSDictionary *mapping = [NSDictionary dictionaryWithObjects:#[#"3", #"4", #"4", #"5", #"6", #"7", #"8"]
forKeys:[Note types]];
[(NSString *)mapping[self.note.type] integerValue];
}
As for the code itself, it seems a bit dangerous to define these constants in one place and the note types elsewhere. Also, why use strings, when NSNumbers would suffice?
(This code assumes this function is only called from one thread).
- (int)minBrokenPieces {
static NSDictionary *mappings;
if (!mappings) {
mappings = #{
noteType1 : #3,
noteType2 : #4,
noteType3 : #4,
noteType4 : #5,
noteType5 : #6,
noteType6 : #7,
noteType7 : #8,
};
}
NSAssert(mappings[self.note.type] != nil, #"Used a note type for which there is no minBrokenPieces defined");
return [mappings[self.note.type] intValue];
}

Capitalize first letter of sentence in CKeditor

I wish to capitalize the first letter of a sentence, on-the-fly, as the user types the text in a CKEditor content instance.
The strategy consists in catching each keystroke and try to replace it when necessary, that is for instance, when the inserted character follows a dot and a space. I'm fine with catching the event, but can't find a way to parse characters surrounding the caret position:
var instance = CKEDITOR.instances.htmlarea
instance.document.getBody().on('keyup', function(event) {
console.log(event);
// Would like to parse here from the event object...
event.data.preventDefault();
});
Any help would be much appreciated including a strategy alternative.
You should use keydown event (close to what you proposed):
var editor = CKEDITOR.instances.editor1;
editor.document.getBody().on('keydown', function(event) {
if (event.data.getKeystroke() === 65 /*a*/ && isFirstLetter()) {
// insert 'A' instead of 'a'
editor.insertText('A');
event.data.preventDefault();
}
});
Now - how should isFirstLetter() look like?
You have to start from editor.getSelection().getRanges() to get caret position.
You're interested only in the first range from the collection.
To extract text content from before the caret use small trick:
move start of the range to the beginning of document: range.setStartAt( editor.document.getBody(), CKEDITOR.POSITION_AFTER_START ),
use CKEDITOR.dom.walker to traverse through DOM tree in source order,
collect text nodes and find out what's before caret (is it /\. $/) - remember that you have to skip inline tags and stop on block tags - hint: return false from guard function to stop traversing.
Example of how you can use walker on range:
var range, walker, node;
range = editor.getSelection().getRanges()[0];
range.setStartAt(editor.document.getBody(), CKEDITOR.POSITION_AFTER_START);
walker = new CKEDITOR.dom.walker(range);
walker.guard = function(node) {
console.log(node);
};
while (node = walker.previous()) {}
And now few sad things.
We assumed that selection is empty when you type - that doesn't have to be true. When selection is not collapsed (empty) then you'll have to manually remove its content before calling insertText. You can use range#deleteContents to do this.
But this is not all - after deleting range's content you have to place caret in correct position - this isn't trivial. Basically you can use range#select (on the range after deleteContents), but in some cases it can place caret in incorrect place - like between paragraphs. Fixing this is... is not doable without deeeeeeeep knowledge about HTML+editables+insertions+other things :).
This solution is not complete - you have to handle paste event, deleting content (one can delete words from the start of sentence), etc, etc.
I guess there are couple of other problems I didn't even thought about :P.
So this approach isn't realistic. If you still want to implement this feature I think that you should set timer and by traversing DOM (you can use walker on range containing entire document, or recently typed text (hard to find out where it is)) find all sentences starting from lower letter and fix them.
This is what worked for me in Ckeditor 4.
var editor = CKEDITOR.instances.editor1;
editor.document.getBody().on('keydown', function(event) {
if (event.data.getKeystroke() >= 65 && event.data.getKeystroke()<=91 && encodeURI(this.getText())=="%0A" && this.getText().length==1 ) {
//uppercase the char
editor.insertText(String.fromCharCode(event.data.getKeystroke()));
event.data.preventDefault();
}
});

Resources