Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
91 changes: 41 additions & 50 deletions cmd/genbindings/emitcabi.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,22 +1086,13 @@ extern "C" {
// real Qt parameters, in case there are protected enum types
// (e.g. QAbstractItemView::CursorAction)

var parametersCabi []string
for _, p := range m.Parameters {
parametersCabi = append(parametersCabi, p.RenderTypeCabi()+" "+p.cParameterName())
}
vbpreamble, vbforwarding := emitParametersCABI2CppForwarding(m.Parameters, "\t\t")

vbCallTarget := methodPrefixName + "::" + m.CppCallTarget() + "(" + vbforwarding + ")"
// Because (in the Go projection) this is only exposed as a
// super() argument to a real virtual override, we know that
// the pointer type correctly points to our subclass and
// therefore no dynamic_cast<> validation is required

ret.WriteString(
"\t// Wrapper to allow calling protected method\n" +
"\t" + m.ReturnType.RenderTypeCabi() + " virtualbase_" + m.SafeMethodName() + "(" + strings.Join(parametersCabi, ", ") + ") " + ifv(m.IsConst, "const ", "") + "{\n" +
vbpreamble + "\n" +
emitAssignCppToCabi("\t\treturn ", m.ReturnType, vbCallTarget) + "\n" +
"\t}\n" +

"\n",
"\tfriend " + m.ReturnType.RenderTypeCabi() + " " + cabiVirtualBaseName(c, m) + "(" + emitParametersCabi(m, ifv(m.IsConst, "const ", "")+"void*") + ");\n\n",
)

}
Expand Down Expand Up @@ -1293,6 +1284,30 @@ extern "C" {

}

// FIXME(hack): In some platforms (Android Qt 5), instantiating a
// protected enum fails in friend context:
//
// QAbstractItemView::State _ret = self_cast->state();
// error: 'State' is a protected member of 'QAbstractItemView'
//
// @ref https://stackoverflow.com/q/52191903
// However, it works fine on most other platforms. Probably this
// is a GCC vs Clang difference.
fixupProtectedReferences := func(assignStmts string) string {

// Work around it for this specific class (fingers-crossed) by
// referencing the protected enum via its subclass name
ret := strings.Replace(assignStmts, c.ClassName+`::`, cppSubclassName(c)+`::`, -1)

// Also need to scan parent classes (e.g. QColumnView friend
// functions refer to its parent QAbstractItemView::State)
for _, classInherit := range c.AllInheritsClassInfo() {
ret = strings.Replace(ret, classInherit.Class.ClassName+`::`, cppSubclassName(c)+`::`, -1)
}

return ret
}

// Virtual override helpers
for _, m := range virtualMethods {

Expand All @@ -1319,26 +1334,24 @@ extern "C" {

if !m.IsPureVirtual {
// This is not generally exposed in the Go binding, but when overriding
// the method, allows Go code to call super()
// the method, allows Go code to call super().

// It uses CABI-CABI, the CABI-QtC++ type conversion will be done
// inside the class method so as to allow for accessing protected
// types.
// Both the parameters and return type are given in CABI format.
// This calls the target Qt C++ method directly using fully
// qualified syntax (`MiqtSubclass->QFoo::Bar()`). This method
// takes and returns CABI types.

var parameterNames []string
for _, param := range m.Parameters {
parameterNames = append(parameterNames, param.cParameterName())
var parametersCabi []string
for _, p := range m.Parameters {
parametersCabi = append(parametersCabi, p.RenderTypeCabi()+" "+p.cParameterName())
}
vbpreamble, vbforwarding := emitParametersCABI2CppForwarding(m.Parameters, "\t")

// callTarget is an rvalue representing the full C++ function call.
// These are never static

callTarget := "( (" + ifv(m.IsConst, "const ", "") + cppClassName + "*)(self) )->virtualbase_" + m.SafeMethodName() + "(" + strings.Join(parameterNames, `, `) + ")"
callTarget := "( (" + ifv(m.IsConst, "const ", "") + cppClassName + "*)(self) )->" + c.ClassName + "::" + m.CppCallTarget() + "(" + vbforwarding + ")"

ret.WriteString(
m.ReturnType.RenderTypeCabi() + " " + cabiVirtualBaseName(c, m) + "(" + emitParametersCabi(m, ifv(m.IsConst, "const ", "")+"void*") + ") {\n" +
"\t" + ifv(m.ReturnType.Void(), "", "return ") + callTarget + ";\n" +
vbpreamble + "\n" +
fixupProtectedReferences(emitAssignCppToCabi("\treturn ", m.ReturnType, callTarget)) + "\n" +
"}\n" +
"\n",
)
Expand All @@ -1358,28 +1371,6 @@ extern "C" {
vbpreamble, vbforwarding := emitParametersCABI2CppForwarding(m.Parameters, "\t\t")
vbCallTarget := "self_cast->" + m.CppCallTarget() + "(" + vbforwarding + ")"

assignStmts := emitAssignCppToCabi("\treturn ", m.ReturnType, vbCallTarget)

// FIXME(hack): In some platforms, instantiating a protected
// enum fails in friend context:
//
// QAbstractItemView::State _ret = self_cast->state();
// error: 'State' is a protected member of 'QAbstractItemView'
//
// @ref https://stackoverflow.com/q/52191903
// However, it works fine on most other platforms. Probably this
// is a GCC vs Clang difference.
//
// Work around it for this specific class (fingers-crossed) by
// referencing the protected enum via its subclass name
assignStmts = strings.Replace(assignStmts, c.ClassName+`::`, cppSubclassName(c)+`::`, -1)

// Also need to scan parent classes (e.g. QColumnView friend
// functions refer to its parent QAbstractItemView::State)
for _, classInherit := range c.AllInheritsClassInfo() {
assignStmts = strings.Replace(assignStmts, classInherit.Class.ClassName+`::`, cppSubclassName(c)+`::`, -1)
}

//

ret.WriteString(
Expand All @@ -1393,7 +1384,7 @@ extern "C" {
"\t\n" +
"\t*_dynamic_cast_ok = true;\n" +
"\t" + vbpreamble + "\n" +
assignStmts + "\n" +
fixupProtectedReferences(emitAssignCppToCabi("\treturn ", m.ReturnType, vbCallTarget)) + "\n" +
"}\n" +
"\n",
)
Expand Down
109 changes: 109 additions & 0 deletions examples/customsortfiltermodel/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"fmt"
qt "github.com/mappu/miqt/qt6"
"os"
)

func GetTreeView(parent *qt.QWidget) *qt.QTreeView {
treeView := qt.NewQTreeView(parent)
treeView.SetAlternatingRowColors(true)
treeView.SetSortingEnabled(true)
treeView.SetColumnWidth(0, 300)
treeView.SetContentsMargins(0, 0, 0, 0)
treeView.Header().SetDefaultSectionSize(200)
treeView.Header().SetStretchLastSection(true)
treeView.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)

return treeView
}

func PopulateData(rootItem *TreeItem) {
cellData := make([]*TreeCell, 0)
cellData = append(cellData, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("Big File")))
cellData = append(cellData, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("10.7 MiB")))
item := NewTreeItem(cellData, rootItem, false)
err := item.SetData(1, qt.NewQVariant6(11225400), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
err = item.SetData(0, qt.NewQVariant11("Big File"), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
rootItem.AppendChild(&item)

cellData2 := make([]*TreeCell, 0)
cellData2 = append(cellData2, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("Small File")))
cellData2 = append(cellData2, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("110 Bytes")))
item2 := NewTreeItem(cellData2, rootItem, false)
err = item2.SetData(1, qt.NewQVariant6(110), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
err = item.SetData(0, qt.NewQVariant11("Small File"), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
rootItem.AppendChild(&item2)

cellData3 := make([]*TreeCell, 0)
cellData3 = append(cellData3, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("Medium File")))
cellData3 = append(cellData3, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("109.5 KiB")))
item3 := NewTreeItem(cellData3, rootItem, false)
err = item3.SetData(1, qt.NewQVariant6(112200), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
err = item.SetData(0, qt.NewQVariant11("Medium File"), qt.UserRole)
if err != nil {
fmt.Printf("Error setting UserData: %v\n", err)
}
rootItem.AppendChild(&item3)
}

func GetDialog() *qt.QDialog {
dialog := qt.NewQDialog3(nil, qt.Dialog)
dialog.SetWindowFlags(qt.WindowStaysOnTopHint)
dialogLayout := qt.NewQVBoxLayout2()
dialogLayout.SetSpacing(0)
dialogLayout.SetContentsMargins(0, 0, 0, 0)
dialog.SetContentsMargins(10, 10, 10, 10)
dialog.SetLayout(dialogLayout.Layout())
dialog.SetWindowTitle("Example Sort Model Usage")

treeView := GetTreeView(dialog.QWidget)

// Model
model, rootItem := GetModel(dialog.QObject)
PopulateData(rootItem)

sortModel := qt.NewQSortFilterProxyModel2(treeView.QObject)
sortModel.SetSortRole(int(qt.UserRole))
sortModel.OnLessThan(func(super func(source_left *qt.QModelIndex, source_right *qt.QModelIndex) bool, source_left *qt.QModelIndex, source_right *qt.QModelIndex) bool {
// In our case we know that the data from our TreeItem will come back as an int for our UserRole
leftData := sortModel.SourceModel().Data(source_left, sortModel.SortRole())
rightData := sortModel.SourceModel().Data(source_right, sortModel.SortRole())

if source_left.Column() == 0 {
return leftData.ToString() < rightData.ToString()
}
return leftData.ToLongLong() < rightData.ToLongLong()
})

sortModel.SetSourceModel(model)
treeView.SetModel(sortModel.QAbstractItemModel)

dialogLayout.AddWidget3(treeView.QWidget, 0, qt.AlignCenter)
return dialog
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the default view, the Size column is initially truncated, so I went to resize the window larger. But something about the QSizePolicy is meaning the treeview will not grow when the window is resized (although it does shrink). I think this could be fixed in a 1 line change, but I haven't checked exactly.

image

image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I took a deeper look at this.

I can't figure out how to make this work as I would expect.

The only way I can get it working is with the following hack:

dialog.OnResizeEvent(func(super func(event *qt.QResizeEvent), event *qt.QResizeEvent) {
		dialog.ResizeWithQSize(event.Size())
                // Width minus dialog.ContentMargins Left/Right
		treeView.Resize(event.Size().Width()-20, treeView.Size().Height())
	})

But having written Qt before, obviously that is a messy hack.

Is there a possibility that the default Resize events aren't being propagated down to child widgets?

}

func main() {
qt.NewQApplication(os.Args)

dialog := GetDialog()
dialog.Show()

qt.QApplication_Exec()
}
94 changes: 94 additions & 0 deletions examples/customsortfiltermodel/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
qt "github.com/mappu/miqt/qt6"
"slices"
"unsafe"
)

func GetModel(parent *qt.QObject) (*qt.QAbstractItemModel, *TreeItem) {
model := qt.NewQAbstractItemModel2(parent)

data := make([]*TreeCell, 0)
data = append(data, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("Name")))
data = append(data, NewTreeCell(qt.DisplayRole, *qt.NewQVariant11("Size")))
rootItem := NewTreeItem(data, nil, true)

model.OnData(func(index *qt.QModelIndex, role int) *qt.QVariant {
if !index.IsValid() {
return qt.NewQVariant()
}
if !slices.Contains([]qt.ItemDataRole{qt.DisplayRole, qt.UserRole}, qt.ItemDataRole(role)) {
return qt.NewQVariant()
}
item := (*TreeItem)(index.InternalPointer())
return item.Data(index.Column(), qt.ItemDataRole(role))
})

model.OnHeaderData(func(super func(section int, orientation qt.Orientation, role int) *qt.QVariant, section int, orientation qt.Orientation, role int) *qt.QVariant {
if qt.ItemDataRole(role) != qt.DisplayRole {
return qt.NewQVariant()
}
if orientation == qt.Horizontal && role == int(qt.DisplayRole) {
return rootItem.Data(section, qt.DisplayRole)
}
return qt.NewQVariant()
})

model.OnIndex(func(row int, column int, parent *qt.QModelIndex) *qt.QModelIndex {
if !model.HasIndex3(row, column, parent) {
return qt.NewQModelIndex()
}
var parentItem *TreeItem
if !parent.IsValid() {
parentItem = &rootItem
} else {
parentItem = (*TreeItem)(parent.InternalPointer())
}
child, err := parentItem.Child(row)
if err != nil {
return qt.NewQModelIndex()
}
newIndex := model.CreateIndex2(row, column, uintptr(unsafe.Pointer(child)))
return &newIndex
})

model.OnParent(func(child *qt.QModelIndex) *qt.QModelIndex {
if !child.IsValid() {
return qt.NewQModelIndex()
}
parentItem := (*TreeItem)(child.InternalPointer()).ParentItem()
if parentItem != &rootItem {
row, err := parentItem.Row()
if err != nil {
return qt.NewQModelIndex()
}
index := model.CreateIndex2(row, 0, uintptr(unsafe.Pointer(parentItem)))
return &index
}
return qt.NewQModelIndex()
})

model.OnRowCount(func(parent *qt.QModelIndex) int {
if parent.Column() > 0 {
return 0
}
var item *TreeItem
if !parent.IsValid() {
item = &rootItem
} else {
item = (*TreeItem)(parent.InternalPointer())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some extra rules when passing pointers between Go and C. One of the rules is: If there is no Go pointer to the TreeItem, only a C++ pointer, then Go might GC the variable, and this could become a use-after-free.

Is there any other place this TreeItem is reachable other than from Qt (C++) memory? If not, a workaround is to either (A) ensure that the TreeItems are stored in some long-lived application struct or global variable; or (B) call runtime.Keepalive() at some later point.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or (C) Wrap the thing in a cgo.Handle before putting it in Qt space, and unwrap it when pulling it out again. This is what Miqt mostly does internally.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so the TreeItem is created in PopulateData and the rootItem keeps track of its children.
And we keep a reference to the rootItem.

I don't know if that is enough to keep it alive.

I am not super clear if pulling in a a pointer from the QModelIndex and then casting it to a TreeItem pointer is the right way to do that, I am open to another way to get the modelItem from a QModelIndex if there is one.

item = (*TreeItem)(parent.InternalPointer())
If I call runtime.KeepAlive(item) everytime that I get the InternalPointer, will that cause a memory leak?
Isn't QModelIndex.internalPointer() a go pointer anyways?

}
return item.ChildCount()
})

model.OnColumnCount(func(parent *qt.QModelIndex) int {
if parent.IsValid() {
item := (*TreeItem)(parent.InternalPointer())
return item.ColumnCount()
}
return rootItem.ColumnCount()
})

return model, &rootItem
}
33 changes: 33 additions & 0 deletions examples/customsortfiltermodel/tree_cell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"fmt"
qt "github.com/mappu/miqt/qt6"
"strconv"
)

type TreeCell struct {
data map[qt.ItemDataRole]string
}

func NewTreeCell(role qt.ItemDataRole, value qt.QVariant) *TreeCell {
data := make(map[qt.ItemDataRole]string)
data[role] = value.ToString()
return &TreeCell{data: data}
}

func (cell *TreeCell) SetData(data qt.QVariant, role qt.ItemDataRole) {
cell.data[role] = data.ToString()
}

func (cell *TreeCell) Data(role qt.ItemDataRole) *qt.QVariant {
if role == qt.UserRole {
number, err := strconv.ParseInt(cell.data[role], 10, 64)
if err != nil {
fmt.Printf("Error converting %s to int: %v\n", number, err)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When starting up the example, I got these terminal messages:

Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax
Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax
Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax
Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax
Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax
Error converting %!s(int=0) to int: strconv.Atoi: parsing "": invalid syntax

They repeat when attempting to sort by the Name column,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks I will fix that!

return qt.NewQVariant11(cell.data[role])
}
return qt.NewQVariant6(number)
}
return qt.NewQVariant11(cell.data[role])
}
Loading