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...
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 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.
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 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:
I have a TreeViewer with an ILazyTreeContentProvider. I want to control the expansion of the top-level tree items: Initially they are unexpanded, but when an Action "Expand" is selected, the item should be expanded.
In the action.run() I currently do
final EntityTreeNode projectNode = (EntityTreeNode) ((ITreeSelection)
viewer.getSelection()).getFirstElement();
// notify the content provider that the node should be expanded; it creates child nodes
// and updates the child count and calls viewer.setChildCount
((LazyEntityModelViewContentProvider) viewer.getContentProvider()).expandProject(projectNode);
viewer.expandToLevel(projectNode, 2);
viewer.refresh(projectNode);
But the node is not expanded. It should at least change it's icon to show that it has children...