How to Get Notified When the Insertion Point Leaves a NSComboBox - macos

I want to gain control when the insertion point leaves a combobox so that I can test if the text matches an entry in the combox's list. I've already added a delegate to my view controller for other combobox functions, for example:
func comboBox(_ aComboBox: NSComboBox, completedString string: String) -> String?
The following function looks like what I want:
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool
My App Code IDE identifies it as being in the API. When I run my code, it doesn't get invoked (I have a print statement in it). I tried changing "control" to "comboBox" without success. Do I need to do something additional to get this function to be called?
Update
As was pointed out by #Mangerlahn, the function needs to be in the datasource. I moved it there with no effect. It was in the delegate.
extension TrackDialogViewController: NSComboBoxDataSource {
...
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool {...}
...
}

Stupid User Error. I lost a RAID drive and had to restart. XCode got messed up and was displaying two window for my project. The one I was looking at didn't have the console.

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.

NSTextField Loses Focus When Neighboring NSTableView Reloaded

I have a search field (NSTextField) called searchField and when you type in it, it refreshes the data shown in a NSTableView. The problem is that this refresh also triggers the selection of a table row, and that takes the focus out of the NSTextField.
The user types in the search field:
func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSTextField, field == searchField{
refreshData()
}
}
Then the NSTableView gets reloaded here:
func refreshData(){
//Process search term and filter data array
//...
//Restore previously selected row (if available)
let index = tableView.selectedRow
tableView.reloadData()
//Select the previously selected row
tableView.selectRowIndexes(NSIndexSet(index: index) as IndexSet, byExtendingSelection: false)
}
If I comment-out both the reloadData() and the selectRowIndexes then the search field behaves as intended (I can keep typing and it keeps the focus in the field). But if include either or both of those methods, the search field loses focus after I type the first character and refreshData() is called.
How can I keep focus in my search field and not let the table reload hijack the focus?
Ugh... it turns out I could never get the NSTableView to let go of the focus because it wasn't set to allow an Empty selection state. Checking a box in Interface Builder fixed it.

Preselecting a NSComboBox Entry

I have a modal window that contains a NSComboBox. "Uses Data Source" is set to turn and the combobox correctly lists the entries. The view controller is the delegate for the data source. When I call the modal window to update an existing object I would like the combobox to display the selected entry if there is one. How do I do this?
I've tried to access the entries in viewWillAppear. I get and error saying there are no entries. The various print statements I have in the code indicate that the values aren't loaded until the drop down arrow is clicked. The two function I'm supplying as the delegate are:
func numberOfItems(in comboBox: NSComboBox) -> Int
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any?
Would it be possible to set the selected entry in the second function above?
As NSComboBox is a subclass of NSTextField you should be able to set its text by setting the stringValue property of your comboBox.
self.comboBox.stringValue = "Hello World"

Swift 2.0 and Xcode-beta 7.0 – Using UITextField for numeric input

Completely frustrated noob here. Surely this isn't as hard as it looks?
I want to use a number entered by the user, perform a calculation, and send the result back to the screen.
I have code working that can use a string forced in by code, convert it to double, do the math and send the result to the screen. For example:
#IBAction func buttonPressed() {
NSLog("Button Pressed")
let decimalAsString = "123.45"
let decimalAsDouble = NSNumberFormatter().numberFromString(decimalAsString as String)!.doubleValue
TempLabel.text = "\(decimalAsDouble+2.45)"
}
This simply adds 2.45 to the string "123.45" and sends the result 125.9 back as a string to my label for display, all when the button is pressed. Great. This simpler form also works:
let decimalAsDouble = Double(decimalAsString)
What I have been struggling with is using a number entered into the UITextField.
My UITextField uses a decimal pad for entry, and I've always had a number entered there when the errors were thrown. (Or did I? The numbers show on screen but are they really "entered"? Hmmm...)
No matter what I try, I cannot find code that will both compile and then not blow up at execution, when the button is pressed. The error I get generally complains about unwrapping an optional nil.
I can detail some of the things that DON'T work, if that helps.
OK, well I've finally solved my own problem. My problem was with closing the entry field and dismissing the numeric entry pad. Once I did that, the entered value became available.
The critical code, in the view controller, was:
func textFieldShouldReturn(textField: UITextField!) -> Bool { //delegate method
textField.resignFirstResponder()
return true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?){
view.endEditing(true)
super.touchesBegan(touches, withEvent: event)
}
This dismisses the entry pad when the user clicks anywhere in the view except for the field itself or a button. Programming that looks for the entered value doesn't blow up.

NSTextField controlTextDidEndEditing: called while being edited (inside an NSOutlineView)

In my NSOutlineView, I have a NSTextField inside a NSTableCellView. I am listening for the controlTextDidEndEditing: notification to happen when the user finishes the editing. However, in my case, this notification is being fired even while the user is in the middle of typing, or takes even a second-long pause in typing. This seems bizarre. I tested a NSTextField in the same view, but outside of the NSOutlineView, and it doesn't behave this way; it only calls controlTextDidEndEditing: if the user pressed the Tab or Enter keys (as expected).
Is there something I can do to prevent the NSTextField from sending controlTextDidEndEditing: unless a Enter or Tab key is pressed?
Found a solution for this:
- (void)controlTextDidEndEditing:(NSNotification *) notification {
// to prevent NSOutlineView from calling controlTextDidEndEditing by itself
if ([notification.userInfo[#"NSTextMovement"] unsignedIntegerValue]) {
....
It's an old question, but for reference, I ran into a similar problem where controlTextDidEndEditing: was called at the beginning of the editing session.
My workaround is to check if the text field still has the focus (i.e. cursor):
func controlTextDidEndEditing(_ obj: Notification) {
guard
let textField = obj.object as? NSTextField,
!textField.isFocused
else {
return
}
...
}
public extension NSTextField
{
public var isFocused:Bool {
if
window?.firstResponder is NSTextView,
let fieldEditor = window?.fieldEditor(false, for: nil),
let delegate = fieldEditor.delegate as? NSTextField,
self == delegate
{
return true
}
return false
}
}
Note to self:
I ran into this problem when adding a new item to NSOutlineView and making it editable with NSOutlineView.editColumn(row:,with:,select).
controlTextDidEndEditing() would be called right away at the start of the editing session.
It turns out it was a first responder/animation race condition. I used a NSTableView.AnimationOptions.slideDown animation when inserting the row and made the row editable afterwards.
The problem here is that the row is made editable while it is still animating. When the animation finishes, the first responder changes to the window and back to the text field, which causes controlTextDidEndEditing() to be called.
outlineView.beginUpdates()
outlineView.insertItems(at: IndexSet(integer:atIndex),
inParent: intoParent == rootItem ? nil : intoParent,
withAnimation: .slideDown) // Animating!
outlineView.endUpdates()
// Problem: the animation above won't have finished leading to first responder issues.
self.outlineView.editColumn(0, row: insertedRowIndex, with: nil, select: true)
Solution 1:
Don't use an animation when inserting the row.
Solution 2:
Wrap beginUpdates/endUpdates into an NSAnimationContext group, add a completion handler to only start editing once the animation finished.
Debugging tips:
Observe changes to firstResponder in your window controller
Put a breakpoint in controlTextDidEndEditing() and take a very close look at the stack trace to see what is causing it to be called. What gave it away in my case were references to animation calls.
To reproduce, wrap beginUpdates/endUpdates in an NSAnimationContext and increase the animation duration to a few seconds.

Resources