Application I'm trying to develop is heavily built around being able to have editable TableViews. I'm beginning to conclude that most if not all of my problems stem from the fact that I do not see allowsSelectionDuringEditing available for NSTableView as it is for UITableView.
First, would like to get insight into why.
Second, how do I go about implementing one in my NSTableView class?
Finally, I have a NSSegmentedControl in one of my columns. So I need to implement allowSelectiongWhenFocused property somewhere, because while I have focus on the button and allowing user to use <- and -> with spacebar to select switch(es), I don't want mouse / keyboard from changing the selected row.
As an aside, while I now know how to write custom UI classes and hook them into Interface Builder, I'm struggling on when/whether to customize NSTableView, NSTableRowView, NSTableCellView or NSSegmentedControl. I've tried to understand how refuseFirstResponder works as well. Trial and error is not getting me anywhere - I fix something and break something somwehre else. If someone can suggest any other reading besides Apple documentation (sometimes I suspect if it is in English) would really appreciate it.
Here's what I would do:
Subclass NSTableRowView and override hitTest(_:). If the row is not selected then the row view returns itself instead of a control. The first click in a row selects the row and doesn't change the value of a control.
override func hitTest(_ point: NSPoint) -> NSView? {
let view = super.hitTest(point)
if view == nil || self.isSelected {
return view
}
else {
return self
}
}
Implement NSTableViewDelegate's tableView(_:rowViewForRow:) to use the custom row view:
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
return MyTableRowView()
}
Implement NSTableViewDelegate's selectionShouldChange(in:) so a row can't be deselected if the values aren't valid or other conditions aren't met.
func selectionShouldChange(in tableView: NSTableView) -> Bool {
let row = tableView.selectedRow
if (row >= 0) {
return self.validateRow(row)
}
else {
return true
}
}
Related
I created custom Poster view so it can be reused in multiple collection view cells (just like TVPosterView in TVUIKit). I add it directly to cell content view with all needed constraints.
The problem is when cell is focused this subview doesn't receive focus update (didUpdateFocus..) so I cannot customize it's focused / unfocused constraints etc. It's odd btw that image view inside is getting floating effect.
In case if I specify cell's preferredFocusEnvironments to return [self.posterView] + super. preferredFocusEnvironments, UI behaves as expected, but the collection view delegate method didSelect not called!
Thanks in advance for any help!
Seems didUpdateFocus not called on all subviews for the focused cell and it's system design. From docs:
After the focus is updated to a new view, the focus engine calls this
method on all focus environments that contain either the previously
focused view, the next focused view, or both, in ascending order. You
should override this method to update your app’s state in response to
changes in focus. Use the provided animation coordinator to animate
changes in visual appearance related to the update. For more
information on animation coordinators, see
UIFocusAnimationCoordinator.
Note: So it means didUpdateFocus will be called first on UICollectionViewCell, than on UIViewController subclasses, in ascending order. For subviews you need to manually register customDidUpdateFocus method that will be triggered in notification update. E.g. to update it's appearance we can use notifications (tvOS 11+), please see the example below.
func customDidUpdateFocus(isFocused: Bool, with coordinator: UIFocusAnimationCoordinator) { /* Custom logic to customize appearance */ }
// Register observer
observerToken = NotificationCenter.default.addObserver(forName: UIFocusSystem.didUpdateNotification, object: nil, queue: .main) { [weak self] (note) in
guard let self = self else { return }
guard let context = note.userInfo?[UIFocusSystem.focusUpdateContextUserInfoKey] as? UIFocusUpdateContext else { return }
guard let coordinator = note.userInfo?[UIFocusSystem.animationCoordinatorUserInfoKey] as? UIFocusAnimationCoordinator else { return }
if let prev = context.previouslyFocusedView, self.isDescendant(of: prev) {
self.didUpdateFocus(isFocused: false, with: coordinator)
} else if let next = context.nextFocusedView, self.isDescendant(of: next) {
self.didUpdateFocus(isFocused: true, with: coordinator)
}
}
I am implementing a category mapper. There are 2 TreeViews. Both contain categories from different sources. (Even they look like from the same source)
The user should be able to map ONE category from the left to multiple of the right treeview. This gets stored in a config file.
However, when the view is initially loaded and the user clicks on a category on the left, I want to preselect the mapped categories on the right, loaded from the config file.
I saw that I can do this with ONE selection, but I don't see an option to do this for multiple ones.
How can I achive this?
Here a ootb running demo implementation
I went through your gist and it seemed that the problem was binding selections right? I did some light digging and found that binding observable lists isn't easy. I don't think I even saw a solution that wasn't just adding listeners to mock a binding. However, wrapping that list in a SimpleListProperty seemed to do the trick. Here's a demo:
class TestView : View() {
// This mocks your target list in the ViewModel
val targetList = SimpleListProperty(listOf<String>().asObservable())
override val root = vbox(10) {
// Both table views and tree views use an Observable Selection list
listview(listOf("asefas", "asefasef", "asefasefasefase").asObservable()) {
// Wrap in SimpleListProperty, then bind
targetList.bind(SimpleListProperty(selectionModel.selectedItems))
selectionModel.selectionMode = SelectionMode.MULTIPLE
}
setPrefSize(300.0, 300.0)
}
init {
// Target list now reflects changes made to selection model
targetList.addListener { _, _, change ->
println("Selections changed to: $change")
}
}
}
I want to use .lookup() so that I can create an event for when the content of a TextArea is clicked, but I get null when I use textArea.lookup(".content"). After searching why this is, I found out that it returns null if called before stage.show(). My next reaction was to somehow check for an event that is cast once the stage is shown, but that event is only accessible if you have access to the stage itself, which I do not in this case. What else can I do?
Don't register the handler at the content node. Let TextArea deal with the creation of the content node on its own, register a event handler at the TextArea directly and use the pickResult of the event to determine, if the click happened inside the node with style class content.
textArea.setOnMouseClicked(evt -> {
Node n = evt.getPickResult().getIntersectedNode();
while (n != textArea) {
if (n.getStyleClass().contains("content")) {
// do something with content node
System.out.println("content: " + n);
break;
}
n = n.getParent();
}
});
Generate a layout pass on the node:
node.applyCss();
node.layout();
as defined in the answer to:
Get the height of a node in JavaFX (generate a layout pass)
After that, your lookup functions on the node should work as expected.
I have a grid I created in Gridx which lists a bunch of users. Upon clicking a ROW in the grid (any part of that row), a dialog pops up and shows additional information about that user and actions that can be done for that user (disable user, ignore user, etc.) - when one of these options is selected from the pop up, I want to DISABLE that row. The logic for getting the row, etc. I can take care of, but I can't figure out how to make a grid row actually "appear" disabled and how to make that row no longer clickable.
Is there a simple way to do this? If you aren't familiar with gridx, solutions that apply to EnhancedGrids or other Dojo grids are also appreciated.
Alright now that I have a little more information here is a solution:
Keep a list of all the rows you have disabled so far either inside the Grid widget or in its parent code. Then on the onRowClick listener I would write code like this:
on(grid, "onRowClick", function(e) {
if(disabledRows[rowIndex]) {
return;
}
// Do whatever pop up stuff you want and after
// a user selects the value, you can "disable"
// your row afterwards by adding it to the disabled
// list so that it can no longer be clicked on.
var rowIndex = e.rowIndex;
disabledRows[rowIndex] = true;
// This is just some random class I made up but
// you can use css to stylize the row however you want
var rowNode = e.rowNode;
domClass.add(rowNode, "disabled");
});
Note that domClass is what I named "dojo/dom-class". Hope this helps!
This is perhaps not exactly what you are seaching for:
If you want to hide one or more rows by your own filterfunction you could just add to these rows in the DOM your own class for nodisplay. Here I show you a function for display only those rows which have in a choiceable field/column a value inside your filterlist.
function hideRowFilter(gridId, fieldName, filterList)
{
var store = gridId.store;
var rowId;
store.query(function(object){
rowId = gridId.row(object.id,true).node();
if (filterList.indexOf(object[fieldName]) == -1)
domClass.add(rowId, "noDisplay"); // anzeigen
else
domClass.remove(rowId, "noDisplay"); // verstecken
});
}
CSS:
.noDisplay { display: none; }
So I can for example display only the entries with a myState of 3 or 4 with this call:
hideRowFilter(gridId, 'myState', [3, 4]);
Note that domClass is what I named "dojo/dom-class"
I have a simple list and a background refresh protocol.
When the list is scrolled down, the refresh scrolls it back to the top. I want to stop this.
I have tried catching the COLLECTION_CHANGE event and
validateNow(); // try to get the component to reset to the new data
list.ensureIndexIsVisible(previousIndex); // actually, I search for the previous data id in the IList, but that's not important
This fails because the list resets itself after the change (in DataGroup.commitProperties).
I hate to use a Timer, ENTER_FRAME, or callLater(), but I cannot seem to figure out a way.
The only other alternatives I can see is sub-classing the List so it can catch the dataProviderChanged event the DataGroup in the skin is throwing.
Any ideas?
Actually MUCH better solution to this is to extend DataGroup. You need to override this.
All the solutions here create a flicker as the scrollbar gets resetted to 0 and the it's set back to the previous value. That looks wrong. This solution works without any flicker and the best of all, you just change DataGroup to FixedDataGroup in your code and it works, no other changes in code are needed ;).
Enjoy guys.
public class FixedDataGroup extends spark.components.DataGroup
{
private var _dataProviderChanged:Boolean;
private var _lastScrollPosition:Number = 0;
public function FixedDataGroup()
{
super();
}
override public function set dataProvider(value:IList):void
{
if ( this.dataProvider != null && value != this.dataProvider )
{
dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
super.dataProvider = value;
if ( value != null )
{
value.addEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
}
override protected function commitProperties():void
{
var lastScrollPosition:Number = _lastScrollPosition;
super.commitProperties();
if ( _dataProviderChanged )
{
verticalScrollPosition = lastScrollPosition;
}
}
private function onDataProviderChanged(e:CollectionEvent):void
{
_dataProviderChanged = true;
invalidateProperties();
}
override public function set verticalScrollPosition(value:Number):void
{
super.verticalScrollPosition = value;
_lastScrollPosition = value;
}
}
I ll try to explain my approach...If you are still unsure let me know and I ll give you the source code as well.
1) Create a variable to store the current scroll position of the viewport.
2) Add Event listener for Event.CHANGE and MouseEvent.MOUSE_WHEEL on the scroller and update the variable created in step 1 with the current scroll position;
3) Add a event listener on your viewport for FlexEvent.UpdateComplete and set the scroll position to the variable stored.
In a nutshell, what we are doing is to have the scroll position stored in variable every time user interacts with it and when our viewport is updated (due to dataprovider change) we just set the scroll position we have stored previously in the variable.
I have faced this problem before and solved it by using a data proxy pattern with a matcher. Write a matcher for your collection that supports your list by updating only changed objects and by updating only attributes for existing objects. The goal is to avoid creation of new objects when your data source refreshes.
When you have new data for the list (after a refresh), loop through your list of new data objects, copying attributes from these objects into the objects in the collection supporting your list. Typically you will match the objects based on id. Any objects in the new list that did not exist in the old one get added. Your scroll position will normally not change and any selections are usually kept.
Here is an example.
for each(newObject:Object in newArrayValues){
var found:Boolean = false;
for each(oldObject:Object in oldArrayValues){
if(oldObject.id == newObject.id){
found = true;
oldObject.myAttribute = newObject.myAttribute;
oldObject.myAttribute2 = newObject.myAttribute2;
}
}
if(!found){
oldArrayValues.addItem(newObject);
}
}
My solution for this problem was targeting a specific situation, but it has the advantage of being very simple so perhaps you can draw something that fits your needs from it. Since I don't know exactly what issue you're trying to solve I'll give you a description of mine:
I had a List that was progressively loading data from the server. When the user scrolled down and the next batch of items would be added to the dataprovider, the scrollposition would jump back to the start.
The solution for this was as simple as stopping the propagation of the COLLECTION_CHANGE event so that the List wouldn't catch it.
myDataProvider.addEventListener(
CollectionEvent.COLLECTION_CHANGE, preventRefresh
);
private function preventRefresh(event:CollectionEvent):void {
event.stopImmediatePropagation();
}
You have to know that this effectively prevents a redraw of the List component, hence any added items would not be shown. This was not an issue for me since the items would be added at the end of the List (outside the viewport) and when the user would scroll, the List would automatically be redrawn and the new items would be displayed. Perhaps in your situation you can force the redraw if need be.
When all items had been loaded I could then remove the event listener and return to the normal behavior of the List component.