I have a view-based NSOutlineView with a dataSource/delegate model instead of binding to a tree controller (I want control over the insert/update animations).
I'm replacing an item in my model and would like to update that specific row in the outline view without having to call reloadData().
I cannot get this to work. Either the item does not update at all or the item's expanded state doesn't update. There seems to be some caching being done inside of NSOutlineView according to this, but even with these suggestions, I could not get it to work.
What I have:
(1) The outline view represents a folder structure
(2) At first, there is a singe file:
(3) The file is then replaced with a folder item:
// Model update
let oldFileItem = rootItem.children.first!
rootItem.children.remove(at: 0)
rootItem.children.append(Item(title: "Folder", children:[], isExpandable:true))
Expected result:
Actual result (reloadItem):
outlineView.reloadItem(oldFileItem) // I kept a reference
Icon and title reloaded, but note that the expansion triangle is missing.
I can somewhat understand that reloadItem() might not work in this case, because the old item is not part of the data model anymore. Strangely enough, the item's title and icon update, but not the expansion state.
Actual result (reloadData(forRowIndexes:columnIndexes:):
outlineView.reloadData(forRowIndexes: IndexSet(integer:0), columnIndexes: IndexSet(integer:0))
No effect whatsoever. This is the one that I would have expected to work.
Actual result (remove/insert):
outlineView.removeItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
outlineView.insertItems(at: IndexSet(integer:0), inParent: rootItem, withAnimation: [])
No effect whatsoever.
The docs say about removeItems(): "The method does nothing if parent is not expanded." and isExpanded does indeed return false for the root node, although its children are visible. Is this special behavior for items that are direct children of the root node? What am I missing here?
For reference, my data model:
class Item:NSObject {
var title:String
var children:[Item]
var isExpandable:Bool
init(title:String, children:[Item], isExpandable:Bool) {
self.title = title
self.children = children
self.isExpandable = isExpandable
}
}
For reference:
It turned out to be an issue with how I used the API. NSOutlineView.removeItems/insertItems expect nil for the inParent parameter for the root item. I was handing in the actual root item. Using nil instead of the root item solved the problem.
Related
I wish to have a list of text items in a PySimpleGUI that I can update later. That is, I want to have a key for the list. This might be vertical or horizontal, and I do not know how many items there will be.
I end up with different use cases, but the current one is to make a single line of text items with different colors. Other times, I need to write and update a customized table, just different enough that the table widget does not work.
Conceptually, I want to do something like this:
layout = [ [sg.T('Titles and Things')], sg.ListThing(key='-data-', [[]]) ]
so that I can:
window['-data-'].update(values=[ [sg.T(v, color=c)] for (v,c) in my_data ])
Another, invalid syntax, way of saying what I want is to use [key="-data-", sg.T('Item1'), sg.T('Item2')].
Is this possible?
You can update individual layout elements but you cannot dynamically change the layout itself.
It is possible to create 2 or more elements, whereby only one of them is visible, and switch them later as needed. Or you can close and re-create the window with another layout. Or combine both approaches.
An example of switching layouts:
def change_layout():
left_col_1 = sg.Column([[sg.Text(f'Text {i}') for i in range(4)]], visible=True, key='col_1')
left_col_2 = sg.Column([[sg.Text(f'Text {i}')] for i in range(6)], visible=False, key='col_2')
visible_1 = True
layout = [[sg.Column([[left_col_1, left_col_2]]), sg.Button('Change layout', key='change')]]
window = sg.Window('window', layout=layout, finalize=True)
while True:
event, values = window.read()
print(event)
print(values)
print(visible_1)
if event in ('Exit', sg.WIN_CLOSED):
break
if event == 'change':
window['col_1'].update(visible=not visible_1)
window['col_2'].update(visible=visible_1)
visible_1 = not visible_1
Please notice that the alternative layouts for the left part (left_col_1, left_col_2) need to be enclosed in a container (column, frame) to keep their position in the window in the moment they are invisible.
I'm trying to store the actual collapsed/expanded state of items in a NSOutlineView, such that it can be restored later. There are two methods available on NSOutlineViewDelegate:
outlineViewItemDidExpand(Notification)
outlineViewItemDidCollapse(Notification)
The problem is that these methods are not only called for the item the user clicks on, but for collapsible children as well. Example:
- a
-- b
--- c
When a is collapsed outlineViewItemDidCollapse is called twice, once for b and once for a. Marking both as collapsed is incorrect, since b should still be expanded and c should be visible after expanding a again. So the actual state for b should be expanded.
When a user Option-clicks on a all children are collapsed as well (outlineView.collapseItem(item, collapseChildren: true)). After expanding a again, b should stay collapsed. The state for b should be collapsed in this case.
The two different states:
a: collapsed, b: expanded (but hidden due to parent)
a: collapsed, b: collapsed (and hidden due to parent)
Is there any way to differentiate between these two actions/states, such that I can properly restore it later?
Some ideas:
NSOutlineView can save and restore the expanded items (autosaveExpandedItems). The settings can be retrieved from NSUserDefaults. The key is NSOutlineView Items <autosaveName>.
Subclass NSOutlineView and override expandItem(_:expandChildren:) and collapseItem(_:collapseChildren:). The methods are not called for the children.
It might be possible to figure out which item is expanded or collapsed using the current event in outlineViewItemWillExpand(_:) and outlineViewItemWillCollapse(_:).
Edited to give a revised answer...
Apparently there is no easy way to recover whether a given container is expanded or collapsed once its parent object has been collapsed. Clearly something in the inner workings of the outline view remembers — possibly it's something as simple as storing the state of the cell view's disclosure button cell, or possibly it sets up a flag in the tree controller or its nodes — but in any case there's no direct programmatic interface. I suspect you'll have to keep track of it in the model object.
To do that, add a boolean property to your model item, such as:
#property BOOL currentlyExpanded;
Then you'll want to implement the two delegate methods outlineViewItemDidExpand: and outlineViewItemWillCollapse:, like so (this is assuming you are using a tree controller for the outline view):
- (void)outlineViewItemDidExpand:(NSNotification *)notification {
NSTreeNode * node = [notification.userInfo objectForKey:#"NSObject"];
NSOutlineView * ov = notification.object;
MyModelItem * item = [node representedObject];
/*
because we can only expand a visible container, we merely note
that this container is now expanded in our view. This will be
called for every container that is expanded, so we don't have to
think about it much.
*/
item.currentlyExpanded = YES;
}
- (void)outlineViewItemWillCollapse:(NSNotification *)notification {
NSTreeNode * node = [notification.userInfo objectForKey:#"NSObject"];
NSOutlineView * ov = notification.object;
MyModelItem * item = [node representedObject];
/*
Elements are collapsed from top to bottom. A collapsed parent
means the collapse started someplace farther up the chain than
our current item, so the expansion state of the current item is
not going to change unless the option key is held down, or you
implement a collapseItem:collapseChildren: with the second
parameter as YES. This accounts for the first; you'll have to
deal with the second in code.
*/
BOOL optionKeyIsDown = [[NSApp currentEvent] modifierFlags] && NSEventModifierFlagOption;
if ([ov isItemExpanded:[node parentNode]] || optionKeyIsDown) {
item.currentlyExpanded = NO;
}
}
These should keep the model item property currentlyExpanded synced with the outline view's internal expansion table (whatever that is). If you want to refer to it or store it in a database you can access it straight from the model objects.
The way I handled the bitmask throws a warning, but I'm too lazy to fix it...
Preserving this last part after the edit, because I think it's good info...
Normally you do not have to worry about any of this; NSOutlineView will 'do the right thing' of its own accord. If the user clicks the disclosure triangle of a container and then reopens it, all of the subcontainers will retain their expanded/collapsed states; if a user option-clicks the control triangle, all of the subcontainers will be marked as expanded or collapsed (depending on whether the user is option-opening or option-closing the parent). Don't bother with it unless you want some specialized behavior (which you would generally set up in the delegate methods outlineView:shouldCollapseItem: and outlineView:shouldExpandItem:).
If you're trying to retain the expansion state across app invocations, set the NSOutlineView property autosaveExpandedItems to true. No bookkeeping necessary...
I am writing an UI test case, in which I need to perform an action, and then on the current page, scroll the only UITableView to the bottom to check if specific text shows up inside the last cell in the UITableView.
Right now the only way I can think of is to scroll it using app.tables.cells.element(boundBy: 0).swipeUp(), but if there are too many cells, it doesn't scroll all the way to the bottom. And the number of cells in the UITableView is not always the same, I cannot swipe up more than once because there might be only one cell in the table.
One way you could go about this is by getting the last cell from the tableView. Then, run a while loop that scrolls and checks to see if the cell isHittable between each scroll. Once it's determined that isHittable == true, the element can then be asserted against.
https://developer.apple.com/documentation/xctest/xcuielement/1500561-ishittable
It would look something like this (Swift answer):
In your XCTestCase file, write a query to identify the table. Then, a subsequent query to identify the last cell.
let tableView = app.descendants(matching: .table).firstMatch
guard let lastCell = tableView.cells.allElementsBoundByIndex.last else { return }
Use a while loop to determine whether or not the cell isHittable/is on screen. Note: isHittable relies on the cell's userInteractionEnabled property being set to true
//Add in a count, so that the loop can escape if it's scrolled too many times
let MAX_SCROLLS = 10
var count = 0
while lastCell.isHittable == false && count < MAX_SCROLLS {
apps.swipeUp()
count += 1
}
Check the cell's text using the label property, and compare it against the expected text.
//If there is only one label within the cell
let textInLastCell = lastCell.descendants(matching: .staticText).firstMatch
XCTAssertTrue(textInLastCell.label == "Expected Text" && textInLastCell.isHittable)
Blaines answer lead me to dig a little bit more into this topic and I found a different solution that worked for me:
func testTheTest() {
let app = XCUIApplication()
app.launch()
// Opens a menu in my app which contains the table view
app.buttons["openMenu"].tap()
// Get a handle for the tableView
let listpagetableviewTable = app.tables["myTableView"]
// Get a handle for the not yet existing cell by its content text
let cell = listpagetableviewTable.staticTexts["This text is from the cell"]
// Swipe down until it is visible
while !cell.exists {
app.swipeUp()
}
// Interact with it when visible
cell.tap()
}
One thing I had to do for this in order to work is set isAccessibilityElement to true and also assign accessibilityLabel as a String to the table view so it can be queried by it within the test code.
This might not be best practice but for what I could see in my test it works very well. I don't know how it would work when the cell has no text, one might be able to reference the cell(which is not really directly referenced here) by an image view or something else. It's obviously missing the counter from Blaines answer but I left it out for simplicity reasons.
I am trying to create an outlineview in a MacOS app that has mutliple levels that are summaries for a set of data held in SQLite3. I have an outlineview working with a treecontroller with a very simple NSMutuableDictionary based on a model class.
import Cocoa
class Summary: NSObject {
#objc dynamic var name: String
#objc dynamic var trades: Int
#objc dynamic var avgPL: Double
#objc dynamic var pandl: Double
#objc dynamic var parent: String
#objc dynamic var isLeaf: Bool
#objc dynamic var childCount: Int
#objc dynamic var children: [Summary] = []
init(name: String, trades: Int, avgPL: Double, pandl: Double, parent: String, isLeaf: Bool,childCount: Int) {
self.name = name
self.trades = trades
self.avgPL = avgPL
self.pandl = pandl
self.parent = parent
self.isLeaf = isLeaf
self.childCount = childCount
}
#objc func add(child: Summary) {
children.append(child)
}
}
My simple example data is:
let root: [String : Any] = ["name": "Overall","trades":5,"avgPL":200,"pandl":500,"parent":"","isLeaf": false,"childCount": 2 ]
let dict: NSMutableDictionary = NSMutableDictionary(dictionary: root)
let l2a = Summary(name: "L2a", trades: 3, avgPL: 100, pandl: 300, parent: "L1",isLeaf: true,childCount: 0)
let l2b = Summary(name: "L2b", trades: 2, avgPL: 100, pandl: 200, parent: "L1",isLeaf: true,childCount: 0)
dict.setObject([l2a,l2b], forKey: "children" as NSCopying)
I pass the dictionary to the treeController:
treeController.addObject(dict)
And that works nicely giving me a collapsible outline:
But I have no idea how to add more levels or children to the children. I want to have up to four levels deep in the outline. I have all the SQL summaries working and I have tried so many variations of populating arrays and trying to create a dictionary with the data to no avail. I have children and childCount and isLeaf set on everything but treecontroller does not like the array complaining that isLeaf is not KVO compliant. My data in an array looks like this (not all of the data but enough to see what I'm doing) The main level and all of the subsequent children are all based on the Summary model class above. Can I simply convert this array to a dictionary? Or, can I make it KVO compliant by adding keys to the model class or something? I have all of the 4 levels in separate arrays I use to build the resultant array if that is useful :
I should add that I have an NSObject defined as an NSMutableArray and its content tied to the treeController. My treeController is bound to each variable in the model class and at the top level has:
If I pass the array I have built to the treeController I get the following error:
Failed to set (contentViewController) user defined inspected property on (NSWindow): [<_TtGCs23_ContiguousArrayStorageC11outlinetest7Summary_ 0x604000445160> addObserver:forKeyPath:options:context:] is not supported. Key path: isLeaf
After building out my NSOutlineView without an NSTreeController and getting everything working I still wanted to get back to this and implement the treeController in order to take advantage of the sorting mechanism it provides. And I did find as per my last comment that I did have something wrong in InterfaceBuilder that was causing it to complain about KVO compliance. I had everything wired correctly except for the Content Array binding on the treeController. Here I bound it to my ViewController and added my data array reportSummary to the Model Key Path.
I also no longer needed to manually add my data array to the treeController using treeController.addObject(reportSummary). Once this was working I was then able to implement sorting and everything is working well. I should point out two things.
Setup of sorting on the TreeController is slightly different than on an ArrayController tied to a TableView. With the tableview it was sufficient to specify which columns are sortable in the identity inspector in IB. But in the outlineView scenario I also needed to setup bindings in IB to the treeController and change the Controller Key from arrangedObjects to sortDescriptors.
While testing my tree controlled outlineview I ran into a problem when I double-clicked on a summary row. I had implemented Double Action on the outlineView in IB in order to control the expanding and collapsing of summary sections. Note that I read about doing this in a thread here and someone mentioned that you would need to maintain multiple arrays and track indexes because once a row is collapsed or expanded that changes the row number of all the subsequent rows. But I figured out that the solution is simply to iterate through rows in reverse order and expand or collapse them working back up the tree starting from outlineView.numberOfRows-1. This works well and along with Double Action (clicking) to expand and collapse I also added an NSSlider which tracks to the expansion level and lets me collapse all the lowest levels moving back up the tree instead of clicking all of the little arrows on each row. This broke when I implemented the treeController. I received an error
Could not cast value of type 'NSKVONotifying_NSTreeControllerTreeNode'
This line of code was the problem
let summary = reportOutline.item(atRow: x) as! Summary
I had to change this to
let node = reportOutline.item(atRow: x) as! NSTreeNode
let summary = node.representedObject as! Summary
And that is it. Working beautifully now.
I want to display some items in my sidebar, with the count of each tag also displayed:
How do I do this efficiently and automatically? The easy option would be to use cocoa bindings, but I'm not sure what the best way to do this: would each button needs it's own NSArrayController with a fetch predicate set for the 'tag'? That could end up with X number of NSArrayControllers (one for each tag) which would be pretty heavy-weight (I would think).
The other option is to create fetch requests manually, then refetch for every change in managed object context. But that seems a bit messy and not-as-automatic.
Is there a simpler solution for this? I've googled around and haven't found anything.
Let's assume that in your NSOutlineView you have a childrenKeyPath of "children" and the children have a boolean isNew attribute. What you want is a nice numberOfNewItems bubble in the cell view for one object class. I'll call that the parent object.
If you just want the number of objects in the childrenKeyPath, heck, that's even easier, but I'll take you through the more complex case of tracking a specific boolean property on the children because it's easy enough to simplify this pattern.
In the table cell view for the parent object add a recessed button with the title bound to objectValue.numberOfNewItems and hidden bound to objectValue.NumberOfNewItems with value transformer NSNegateBoolean. If you just want the number of children, swith those keypaths to objectValue.children.count and you're done. If you want to track a property like isNew, let's continue...
In the parent object class, there is this code:
- (NSNumber*) numberOfNewItems
{
// Collection operator on boolean returns total number of new children
return [self valueForKeyPath:#"children.#sum.isNew"];
}
// This setter does nothing, but with KVO it causes bindings
// to numberOfNewItems to call the above getter
- (void) setNumberOfNewItems:(NSNumber*)number { }
// This ensures that changes to the children set causes KVO calls to the above getter
+ (NSSet*) keyPathsForValuesAffectingNumberOfNewItems
{
return [[NSSet alloc] initWithObjects:#"children", nil];
}
What that does is cause numberOfNewItems to be recalculated any time the table cell's objectValue gets a new child added to or removed from its children to-many relationship.
In the childItem class, there's this in the one place that the childItem is transitioned from isNew to not-New:
// If collapsing an item, mark it as not new
if (!isExpanded.boolValue && self.isNewValue) {
self.isNew = #NO;
[self.parent setNumberOfNewItems:nil]; // Triggers KVO for button binding
}
... and what that does is use the parent's empty setNumberOfNewItems setter to force the button binding to call the getter. So the whole to-many children relationship is enumerated each time an item is marked not-new. I supposed that could be improved, but I haven't played around with that yet.
I took advantage of the fact that an item is marked not-new only one place in my code. If you have several things resetting or setting isNew in the child, you might override setIsNew in the childItem class to call self.parent setNumberOfNewItems:nil instead.
The trick here is that having the parent add itself as a KVO observer for the isNew keypath for all children would be a terrible pain. So I wanted to avoid that. If you simply have the children call the empty setter in the parent, the parent can own the calculation, and there's no KVO outside what the bindings use.
Looks like this: