Making an NSTableView behave like a WPF StackPanel - cocoa

I'm trying to implement a vertical StackPanel equivalent in my MonoMac project and am having trouble figuring it out. I am not using interface builder but creating the controls programmatically (this is a restriction). I tried using a CollectionView but all items in that control are sizing to the same height.
Looking around the internet, it seems NSTableView is the way to go, but I'm not sure how to do it, especially with C# while using a MonoMac target. CollectionView was somewhat more straightforward with the CollectionViewItem.ItemPrototype allowing me to create the views I want to render. But with an NSTableView, it seems like I can only specify a data source that returns the NSObjects I want to display. How do I grab this data and then bind them to the view I want to stick in there?
I would prefer C# code but I'm at a stage where I'll accept any and all help!

I was finally able to get it working. Here is some code for anyone who wants to try it out. Basically, we need to write NSTableViewDelegates for the required functions. This implementation also doesn't cache the controls or anything. The Cocoa API documentation mentioned using an identifier to reuse the control, or something, but the identifier field is get-only in MonoMac.
I also ended up implementing my NSTableViewDelegate functions in my data-source itself which I am sure is not kosher at all, but I'm not sure what the best practice is.
Here's the data source class:
class MyTableViewDataSource : NSTableViewDataSource
{
private NSObject[] _data;
// I'm coming from an NSCollectionView, so my data is already in this format
public MyTableViewDataSource(NSObject[] data)
{
_data = data;
}
public override int GetRowCount(NSTableView tableView)
{
return _data.Length;
}
#region NSTableViewDelegate Methods
public NSView GetViewForItem(NSTableView tableView, NSTableColumn tableColumn, int row)
{
// MyViewClass extends NSView
MyViewClass result = tableView.MakeView("MyView", this) as MyViewClass;
if (result == null)
{
result = new MyViewClass(_data[row]);
result.Frame = new RectangleF(0, 0, tableView.Frame.Width, 100); // height doesn't matter since that is managed by GetRowHeight
result.NeedsDisplay = true;
// result.Identifier = "MyView"; // this line doesn't work because Identifier only has a getter
}
return result;
}
public float GetRowHeight(NSTableView tableView, int row)
{
float height = FigureOutHeightFromData(_data[row]); // run whatever algorithm you need to get the row's height
return height;
}
#endregion
}
And here's the snippet that programmatically creates the table:
var tableView = new NSTableView();
var dataSource = new MyTableViewDataSource();
tableView.DataSource = dataSource;
tableView.HeaderView = null; // get rid of header row
tableView.GetViewForItem = dataSource.GetViewForItem;
tableView.GetRowHeight = dataSource.GetRowHeight;
AddSubView(tableView);
So, it is not a perfect StackPanel because one needs to manually calculate row heights, but it's better than nothing.

Related

create a subclass of NSView to enable setTag()

I am using xamarin to develop a Mac app, and somewhere in my program I want to set the tag of a NSView. However, the tag property is readonly for NSView, so I'm searching for a way to create a subclass where tag is writable. Is there any suggestion about how I should write the subclass? thanks
public class MyNSView : NSView
{
public nint _tag;
public new nint Tag
{
get
{
return _tag;
}
set
{
_tag = value;
}
}
Be aware that this is not longer the NSView Tag.
Using your custom NSView Tag
var view = new MyNSView ();
view.Tag = 100;
I found a workaround to get viewWithTag working:
#IBInspectable
override var tag: Int {
get { return super.tag }
set { super.tag = newValue }
}
In IB the NSView's Tag is still greyed out, but there is also a subclass's Tag available for setting a value.
Still, to my best effort, I can't find a logical reason for Tag to be greyed out in IB.

How to drag and drop rows in NSTableView in Mac

I have struck with one point i.e now I have got a list of data using NSTableView but what my requirement is, able to drag and drop that rows from one row position to another row position. please give any suggestion for get out this problem. Thanks in advance.
My sample code
Within your NSTableViewDataSource subclass implement WriteRows, ValidateDrop and AcceptDrop and register which Drag/Drop targets your NSTableView accepts. In this case you are only accepting Drops from within your own NSTableView.
Assign a name that will be used for valid drag operations on this NSTableView:
// Any name can be registered, I find using the class name
// of the items in the datasource is cleaner than a const string
string DragDropType = typeof(Product).FullName;
Register the drag types for your NSTableView:
ProductTable.RegisterForDraggedTypes(new string[] { DragDropType });
Implement drag/drop methods on your NSTableViewDataSource:
public override bool WriteRows(NSTableView tableView, NSIndexSet rowIndexes, NSPasteboard pboard)
{
var data = NSKeyedArchiver.ArchivedDataWithRootObject(rowIndexes);
pboard.DeclareTypes(new string[] { DragDropType }, this);
pboard.SetDataForType(data, DragDropType);
return true;
}
public override NSDragOperation ValidateDrop(NSTableView tableView, NSDraggingInfo info, nint row, NSTableViewDropOperation dropOperation)
{
tableView.SetDropRowDropOperation(row, dropOperation);
return NSDragOperation.Move;
}
public override bool AcceptDrop(NSTableView tableView, NSDraggingInfo info, nint row, NSTableViewDropOperation dropOperation)
{
var rowData = info.DraggingPasteboard.GetDataForType(DragDropType);
if (rowData == null)
return false;
var dataArray = NSKeyedUnarchiver.UnarchiveObject(rowData) as NSIndexSet;
Console.WriteLine($"{dataArray}");
// Move hack for this example... you need to handle the complete NSIndexSet
tableView.BeginUpdates();
var tmpProduct = Products[(int)dataArray.FirstIndex];
Products.RemoveAt((int)dataArray.FirstIndex);
if (Products.Count == row - 1)
Products.Insert((int)row - 1 , tmpProduct);
else
Products.Insert((int)row, tmpProduct);
tableView.ReloadData();
tableView.EndUpdates();
return true;
}

Ripple Effect gone after adding TapGestureRecognizer to ViewCell

I added a custom LongPressGestureRecognizer to the ViewCell's root layout to handle certain cases, but after adding it, I find that the ripple effect when tapping the ViewCell is gone on Android. I tried to add back the animation by getting the native view, set background drawable to Android.Resource.Attribute.SelectableItemBackground by using below code
int[] attrs = { Android.Resource.Attribute.SelectableItemBackground };
var ta = CrossCurrentActivity.Current.Activity.ObtainStyledAttributes(attrs);
var drawable = ta.GetDrawable(0);
nativeView.SetBackgroundDrawable(drawable);
ta.Recycle();
Even this doesn't work. Any other way to make it work?
For those who want to know, I discarded the custom long press gesture recognizer way of achieving the goal, since it's the wrong way of doing things. On Android, we should use ItemLongClick event instead. Here is what I did, first, find out the native ListView through some method, my way is to first get the renderer of the ListView, then get underlying ListView. Another way is to use below code to find the ListView, but this way requires more work if you have multiple ListView
public static List<T> FindViews<T>(this ViewGroup viewGroup) where T : View
{
var result = new List<T>();
var count = viewGroup.ChildCount;
for (int i = 0; i < count; i++)
{
var child = viewGroup.GetChildAt(i);
var item = child as T;
if (item != null)
{
result.Add(item);
}
else if (child is ViewGroup)
{
var innerResult = FindViews<T>(child as ViewGroup);
if (innerResult != null)
{
result.AddRange(innerResult);
}
}
}
return result;
}
var rootView =(ViewGroup)CurrentActivity.Window.DecorView.RootView
var nativeListView = rootView.FindView<Android.Widget.ListView>();
Then override the OnAppearing method of the Page, in it, attach ItemLongClick event handler. Also override OnDisappearing method, in it, detach the ItemLongClick event handler. This is important. Simply add ItemLongClick event handler in constructor seems not working.

Drag and drop function swift OSX

This is a bit complex but I hope that someone can help me.
I am trying to build a drag and drop function for my OSX application.
This is how it is looking at the moment.
So there is just a single textfield which the user can drag and drop around the view. It is simple enough with just one textfield but if there are several textfields it is getting complicated and I don't know how to approach.
This is what I currently have:
#IBOutlet weak var test: NSTextField!
#IBAction override func mouseDragged(theEvent: NSEvent) {
NSCursor.closedHandCursor().set()
var event_location = theEvent.locationInWindow
test.frame.origin.x = event_location.x - 192
test.frame.origin.y = event_location.y
}
Test is the name of my NSTextField. I know the name of it so it is simple to move it arround. But if the user adds more textfields (see on the left pane) then I don't know how to address this textfield because I have no name of it (like "test" for the first input).
I am adding the textfields via this code:
let input = NSTextField(frame: CGRectMake(width, height, 100, 22))
self.MainView.addSubview(input)
How can I determine which textfield (if there are multiple on the view) was selected and then move the appropriate via drag and drop?
The drag and drop is working for that single static textfield
I have prepared a sample app, so consider this:
https://github.com/melifaro-/DraggableNSTextFieldSample
The idea is to introduce SelectableTextField which inherits NSTextField. SelectableTextField provides facility for subscription of interested listener on text field selection event. It has didSelectCallback block variable, where you need to set you handling code. Something like this:
textField.didSelectCallback = { (textField) in
//this peace of code will be performed once mouse down event
//was detected on the text field
self.currentTextField = textField
}
By using mentioned callback mechanism, once text field selected, we can store it in currentTextField variable. So that when mouseDragged function of ViewController is called we are aware of currentTextField and we can handle it appropriatelly. In case of sample app we need adjust currentTextField origin according drag event shift. Hope it became better now.
P.S. NSTextField is opened for inheriting from it, so you can freely use our SelectableTextField everywhere where you use NSTextField, including Interface Builder.
EDIT
I have checked out your sample. Unfortuantly I am not able to commit /create pull request into you repository, so find my suggestion here:
override func viewDidLoad() {
super.viewDidLoad()
didButtonSelectCallback = { (button) in
if let currentButton = self.currentButton {
currentButton.highlighted = !currentButton.highlighted
if currentButton == button {
self.currentButton = nil
} else {
self.currentButton = button
}
} else {
self.currentButton = button
}
button.highlighted = !button.highlighted
}
addButtonAtRandomePlace()
addButtonAtRandomePlace()
didButtonSelectCallback(button: addButtonAtRandomePlace())
}
override func mouseDragged(theEvent: NSEvent) {
guard let button = currentButton else {
return
}
NSCursor.closedHandCursor().set()
button.frame.origin.x += theEvent.deltaX
button.frame.origin.y -= theEvent.deltaY
}
private func addButtonAtRandomePlace() -> SelectableButton {
let viewWidth = self.view.bounds.size.width
let viewHeight = self.view.bounds.size.height
let x = CGFloat(rand() % Int32((viewWidth - ButtonWidth)))
let y = CGFloat(rand() % Int32((viewHeight - ButtonHeight)))
let button = SelectableButton(frame: CGRectMake(x, y, ButtonWidth, ButtonHeight))
button.setButtonType(NSButtonType.ToggleButton)
button.alignment = NSCenterTextAlignment
button.bezelStyle = NSBezelStyle.RoundedBezelStyle
button.didSelectCallback = didButtonSelectCallback
self.view.addSubview(button)
return button
}

Multiple Custom Cell's in a ListView (Cross Platform)

Currently with ListView's I've only found that you can create a template for cells, which makes each cell look exactly the same. You can't have multiple custom cells in the listview. There are work-arounds like hiding the content in the cell depending on the content, but this seems pretty hacky.
The reason I want to use a listview over a tableview is because we plan on doing inserts, deletions, dynamically showing certain cells, and listview's can be binded to a data source.
Create your own ViewCell which overrides binding context change method. When the binding changes set the ViewCell's view to one that matches the type of view model and also set the height of the cell. Below is a quick sample that should give you an idea how to accomplish it.
public class DataTemplateCell1 : ViewCell
{
protected override void OnBindingContextChanged()
{
var vm1 = this.BindingContext as ViewModel1;
if (vm1 != null)
{
this.View = new View1() { HeightRequest = 40 };
this.Height = this.View.HeightRequest;
return;
}
var vm2 = this.BindingContext as ViewModel2;
if (vm2 != null)
{
this.View = new View2() { HeightRequest = 80 };
this.Height = this.View.HeightRequest;
return;
}
base.OnBindingContextChanged();
}
}

Resources