Merge pull request #35 from mappu/miqt-next

Improve uic functionality
This commit is contained in:
mappu 2024-10-05 18:25:57 +13:00 committed by GitHub
commit e41aa625bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 2320 additions and 163 deletions

View File

@ -12,7 +12,7 @@ MIQT is MIT-licensed Qt bindings for Go.
This is a straightforward binding of the Qt API using CGO. You must have a working Qt C++ development toolchain to use this Go binding. This is a straightforward binding of the Qt API using CGO. You must have a working Qt C++ development toolchain to use this Go binding.
These bindings were newly started in August 2024. The bindings are functional for all of QtCore, QtGui, and QtWidgets. But, they may be immature in some ways. Please try out the bindings and raise issues if you have trouble. These bindings were newly started in August 2024. The bindings are functional for all of QtCore, QtGui, and QtWidgets, and there is a uic/rcc implementation. But, the bindings may be immature in some ways. Please try out the bindings and raise issues if you have trouble.
## Supported platforms ## Supported platforms
@ -88,6 +88,12 @@ Qt class inherited types are projected as a Go embedded struct. For example, to
Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future. Some C++ idioms that were difficult to project were omitted from the binding. But, this can be improved in the future.
### Q6. Can I use Qt Designer and the Qt Resource system?
![](doc/architecture-uic.png)
MIQT has a custom implementation of Qt `uic` and `rcc` tools, to allow using [Qt Designer](https://doc.qt.io/qt-5/qtdesigner-manual.html) for form design and resource management. After running the `miqt-uic` and `miqt-rcc` tools once, you can rebuild any changes using the convenient `go generate` command.
## Building ## Building
### Linux (native) ### Linux (native)

55
cmd/miqt-rcc/miqt-rcc.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
#
# miqt-rcc compiles a Qt resource XML file (*.qrc) to a binary resource file and
# creates a Go stub to load it.
set -eu
main() {
if [[ $# -lt 1 ]] ; then
echo "Usage: miqt-rcc.sh input.qrc [output.rcc] [output.go] [packageName] [variableName]" >&2
exit 1
fi
local ARG_INPUT_QRC="$1"
local ARG_DEST_RCC="${2:-$(basename -s .qrc "$ARG_INPUT_QRC").rcc}"
local ARG_DEST_GO="${3:-$(basename -s .rcc "$ARG_DEST_RCC").go}"
local ARG_PACKAGENAME="${4:-main}"
local ARG_VARIABLENAME="${5:-_resourceRcc}"
if [[ ! -f ${ARG_INPUT_QRC} ]] ; then
echo "Input file ${ARG_INPUT_QRC} not found" >&2
exit 1
fi
# Compile qrc to binary resource file
rcc --binary -o "${ARG_DEST_RCC}" "$ARG_INPUT_QRC"
# Create Go file that loads the resource
cat > "${ARG_DEST_GO}" <<EOF
package ${ARG_PACKAGENAME}
//go:generate miqt-rcc.sh $@
import (
"embed"
"github.com/mappu/miqt/qt"
)
//go:embed ${ARG_DEST_RCC}
var ${ARG_VARIABLENAME} []byte
func init() {
_ = embed.FS{}
qt.QResource_RegisterResourceWithRccData(&${ARG_VARIABLENAME}[0])
}
EOF
gofmt -s -w "${ARG_DEST_GO}"
echo "RCC OK"
}
main "$@"

View File

@ -1,165 +1,168 @@
package main package main
func constructorFunctionFor(className string) (string, bool) { func constructorFunctionFor(className string) string {
// Rebuild this list via: // Rebuild this list via:
// grep -PRoh 'func New.+\(parent \*QWidget\)' ~/dev/miqt/qt/ | sed -re 's~^func New([^0-9]+)([0-9]*)\(.*~case "\1": return "New\1\2", true~' // grep -PRoh 'func New.+\(parent \*QWidget\)' ~/dev/miqt/qt/ | sed -re 's~^func New([^0-9]+)([0-9]*)\(.*~case "\1": return "New\1\2"~'
switch className { switch className {
// CODEGENERATED LIST START // CODEGENERATED LIST START
case "QListWidget": case "QListWidget":
return "NewQListWidget2", true return "NewQListWidget2"
case "QAbstractSpinBox": case "QAbstractSpinBox":
return "NewQAbstractSpinBox2", true return "NewQAbstractSpinBox2"
case "QStackedLayout": case "QStackedLayout":
return "NewQStackedLayout2", true return "NewQStackedLayout2"
case "QColumnView": case "QColumnView":
return "NewQColumnView2", true return "NewQColumnView2"
case "QProgressDialog": case "QProgressDialog":
return "NewQProgressDialog3", true return "NewQProgressDialog3"
case "QTabWidget": case "QTabWidget":
return "NewQTabWidget2", true return "NewQTabWidget2"
case "QLabel": case "QLabel":
return "NewQLabel3", true return "NewQLabel3"
case "QKeySequenceEdit": case "QKeySequenceEdit":
return "NewQKeySequenceEdit3", true return "NewQKeySequenceEdit3"
case "QDockWidget": case "QDockWidget":
return "NewQDockWidget5", true return "NewQDockWidget5"
case "QFontComboBox": case "QFontComboBox":
return "NewQFontComboBox2", true return "NewQFontComboBox2"
case "QTreeView": case "QTreeView":
return "NewQTreeView2", true return "NewQTreeView2"
case "QCalendarWidget": case "QCalendarWidget":
return "NewQCalendarWidget2", true return "NewQCalendarWidget2"
case "QLineEdit": case "QLineEdit":
return "NewQLineEdit3", true return "NewQLineEdit3"
case "QMenuBar": case "QMenuBar":
return "NewQMenuBar2", true return "NewQMenuBar2"
case "QFrame": case "QFrame":
return "NewQFrame2", true return "NewQFrame2"
case "QAbstractScrollArea": case "QAbstractScrollArea":
return "NewQAbstractScrollArea2", true return "NewQAbstractScrollArea2"
case "QSplitter": case "QSplitter":
return "NewQSplitter3", true return "NewQSplitter3"
case "QStackedWidget": case "QStackedWidget":
return "NewQStackedWidget2", true return "NewQStackedWidget2"
case "QWizard": case "QWizard":
return "NewQWizard2", true return "NewQWizard2"
case "QWizardPage": case "QWizardPage":
return "NewQWizardPage2", true return "NewQWizardPage2"
case "QMdiSubWindow": case "QMdiSubWindow":
return "NewQMdiSubWindow2", true return "NewQMdiSubWindow2"
case "QStatusBar": case "QStatusBar":
return "NewQStatusBar2", true return "NewQStatusBar2"
case "QToolButton": case "QToolButton":
return "NewQToolButton2", true return "NewQToolButton2"
case "QShortcut": case "QShortcut":
return "NewQShortcut", true return "NewQShortcut"
case "QSlider": case "QSlider":
return "NewQSlider3", true return "NewQSlider3"
case "QComboBox": case "QComboBox":
return "NewQComboBox2", true return "NewQComboBox2"
case "QScrollBar": case "QScrollBar":
return "NewQScrollBar3", true return "NewQScrollBar3"
case "QTabBar": case "QTabBar":
return "NewQTabBar2", true return "NewQTabBar2"
case "QTextBrowser": case "QTextBrowser":
return "NewQTextBrowser2", true return "NewQTextBrowser2"
case "QTreeWidget": case "QTreeWidget":
return "NewQTreeWidget2", true return "NewQTreeWidget2"
case "QDialog": case "QDialog":
return "NewQDialog2", true return "NewQDialog2"
case "QFormLayout": case "QFormLayout":
return "NewQFormLayout2", true return "NewQFormLayout2"
case "QToolBar": case "QToolBar":
return "NewQToolBar4", true return "NewQToolBar4"
case "QWidget": case "QWidget":
return "NewQWidget2", true return "NewQWidget2"
case "QRadioButton": case "QRadioButton":
return "NewQRadioButton3", true return "NewQRadioButton3"
case "QCheckBox": case "QCheckBox":
return "NewQCheckBox3", true return "NewQCheckBox3"
case "QSizeGrip": case "QSizeGrip":
return "NewQSizeGrip", true return "NewQSizeGrip"
case "QLCDNumber": case "QLCDNumber":
return "NewQLCDNumber3", true return "NewQLCDNumber3"
case "QFileDialog": case "QFileDialog":
return "NewQFileDialog3", true return "NewQFileDialog3"
case "QUndoView": case "QUndoView":
return "NewQUndoView4", true return "NewQUndoView4"
case "QGraphicsView": case "QGraphicsView":
return "NewQGraphicsView3", true return "NewQGraphicsView3"
case "QPushButton": case "QPushButton":
return "NewQPushButton4", true return "NewQPushButton4"
case "QColorDialog": case "QColorDialog":
return "NewQColorDialog3", true return "NewQColorDialog3"
case "QMessageBox": case "QMessageBox":
return "NewQMessageBox4", true return "NewQMessageBox4"
case "QSplashScreen": case "QSplashScreen":
return "NewQSplashScreen3", true return "NewQSplashScreen3"
case "QErrorMessage": case "QErrorMessage":
return "NewQErrorMessage2", true return "NewQErrorMessage2"
case "QListView": case "QListView":
return "NewQListView2", true return "NewQListView2"
case "QDateTimeEdit": case "QDateTimeEdit":
return "NewQDateTimeEdit5", true return "NewQDateTimeEdit5"
case "QTimeEdit": case "QTimeEdit":
return "NewQTimeEdit3", true return "NewQTimeEdit3"
case "QDateEdit": case "QDateEdit":
return "NewQDateEdit3", true return "NewQDateEdit3"
case "QMenu": case "QMenu":
return "NewQMenu3", true return "NewQMenu3"
case "QToolBox": case "QToolBox":
return "NewQToolBox2", true return "NewQToolBox2"
case "QTableWidget": case "QTableWidget":
return "NewQTableWidget3", true return "NewQTableWidget3"
case "QFocusFrame": case "QFocusFrame":
return "NewQFocusFrame2", true return "NewQFocusFrame2"
case "QHBoxLayout": case "QHBoxLayout":
return "NewQHBoxLayout2", true return "NewQHBoxLayout2"
case "QVBoxLayout": case "QVBoxLayout":
return "NewQVBoxLayout2", true return "NewQVBoxLayout2"
case "QInputDialog": case "QInputDialog":
return "NewQInputDialog2", true return "NewQInputDialog2"
case "QTableView": case "QTableView":
return "NewQTableView2", true return "NewQTableView2"
case "QMdiArea": case "QMdiArea":
return "NewQMdiArea2", true return "NewQMdiArea2"
case "QSpinBox": case "QSpinBox":
return "NewQSpinBox2", true return "NewQSpinBox2"
case "QDoubleSpinBox": case "QDoubleSpinBox":
return "NewQDoubleSpinBox2", true return "NewQDoubleSpinBox2"
case "QProgressBar": case "QProgressBar":
return "NewQProgressBar2", true return "NewQProgressBar2"
case "QTextEdit": case "QTextEdit":
return "NewQTextEdit3", true return "NewQTextEdit3"
case "QAbstractSlider": case "QAbstractSlider":
return "NewQAbstractSlider2", true return "NewQAbstractSlider2"
case "QDialogButtonBox": case "QDialogButtonBox":
return "NewQDialogButtonBox5", true return "NewQDialogButtonBox5"
case "QFontDialog": case "QFontDialog":
return "NewQFontDialog3", true return "NewQFontDialog3"
case "QMainWindow": case "QMainWindow":
return "NewQMainWindow2", true return "NewQMainWindow2"
case "QCommandLinkButton": case "QCommandLinkButton":
return "NewQCommandLinkButton4", true return "NewQCommandLinkButton4"
case "QDial": case "QDial":
return "NewQDial2", true return "NewQDial2"
case "QGridLayout": case "QGridLayout":
return "NewQGridLayout", true return "NewQGridLayout"
case "QPlainTextEdit": case "QPlainTextEdit":
return "NewQPlainTextEdit3", true return "NewQPlainTextEdit3"
case "QScrollArea": case "QScrollArea":
return "NewQScrollArea2", true return "NewQScrollArea2"
case "QGroupBox": case "QGroupBox":
return "NewQGroupBox3", true return "NewQGroupBox3"
// CODEGENERATED LIST END // CODEGENERATED LIST END
default: default:
return "", false // This class is not known to miqt-uic
// Assume it's a user promoted type from Qt Designer with a matching ctor
// of the form New{Class}(parent *QWidget)
return "New" + className
} }

View File

@ -5,21 +5,32 @@ import (
) )
type UiLayoutItem struct { type UiLayoutItem struct {
Row *int `xml:"row,attr"` Row *int `xml:"row,attr"`
Column *int `xml:"column,attr"` Column *int `xml:"column,attr"`
Widget UiWidget `xml:"widget"` RowSpan *int `xml:"rowspan,attr"`
ColSpan *int `xml:"colspan,attr"`
// A layout item either has a widget, or a spacer
Widget *UiWidget `xml:"widget"`
Spacer *UiSpacer `xml:"spacer"`
} }
type UiLayout struct { type UiLayout struct {
Class string `xml:"class,attr"` Class string `xml:"class,attr"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Items []UiLayoutItem `xml:"item"` Properties []UiProperty `xml:"property"`
Items []UiLayoutItem `xml:"item"`
} }
type UiPropertyContainer struct { type UiPropertyContainer struct {
Properties []UiProperty `xml:"property"` Properties []UiProperty `xml:"property"`
} }
type UiSpacer struct {
Name string `xml:"name,attr"`
Properties []UiProperty `xml:"property"`
}
type UiWidget struct { type UiWidget struct {
Class string `xml:"class,attr"` Class string `xml:"class,attr"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
@ -48,12 +59,30 @@ type UiString struct {
Notr bool `xml:"notr,attr,omitempty"` Notr bool `xml:"notr,attr,omitempty"`
} }
type UiIcon struct {
ResourceFile string `xml:"resource,attr"`
NormalOff *string `xml:"normaloff,omitempty"`
NormalOn *string `xml:"normalon,omitempty"`
ActiveOff *string `xml:"activeoff,omitempty"`
ActiveOn *string `xml:"activeon,omitempty"`
DisabledOff *string `xml:"disabledoff,omitempty"`
DisabledOn *string `xml:"disabledon,omitempty"`
SelectedOff *string `xml:"selectedoff,omitempty"`
SelectedOn *string `xml:"selectedon,omitempty"`
Base string `xml:",chardata"`
}
type UiProperty struct { type UiProperty struct {
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
StringVal *UiString `xml:"string,omitempty"` StringVal *UiString `xml:"string,omitempty"`
NumberVal *string `xml:"number,omitempty"` // Preserve as string literal NumberVal *string `xml:"number,omitempty"` // Preserve as string literal
BoolVal *bool `xml:"bool,omitempty"` // "true" or "false"
EnumVal *string `xml:"enum,omitempty"` EnumVal *string `xml:"enum,omitempty"`
RectVal *UiRect `xml:"rect,omitempty"` RectVal *UiRect `xml:"rect,omitempty"`
IconVal *UiIcon `xml:"iconset,omitempty"`
SetVal *string `xml:"set,omitempty"`
} }
type UiActionReference struct { type UiActionReference struct {
@ -71,12 +100,18 @@ type UiResources struct {
type UiConnections struct { type UiConnections struct {
} }
type UiLayoutDefault struct {
Spacing *int `xml:"spacing,attr,omitempty"`
Margin *int `xml:"margin,attr,omitempty"`
}
type UiFile struct { type UiFile struct {
XMLName xml.Name // should always be xml.Name{Local: "ui"} XMLName xml.Name // should always be xml.Name{Local: "ui"}
Class string `xml:"class"` Class string `xml:"class"`
Version string `xml:"version,attr"` // e.g. 4.0 Version string `xml:"version,attr"` // e.g. 4.0
Widget UiWidget `xml:"widget"` // There's only one root widget Widget UiWidget `xml:"widget"` // There's only one root widget
Resources UiResources `xml:"resources"` Resources UiResources `xml:"resources"`
Connections UiConnections `xml:"connections"` Connections UiConnections `xml:"connections"`
LayoutDefault *UiLayoutDefault `xml:"layoutdefault,omitempty"`
} }

View File

@ -7,18 +7,30 @@ import (
"strings" "strings"
) )
func collectClassNames_Widget(u UiWidget) []string { var (
RootWindowName = ""
DefaultGridMargin = 11
DefaultSpacing = 6
IconCounter = 0
)
func collectClassNames_Widget(u *UiWidget) []string {
var ret []string var ret []string
if u.Name != "" { if u.Name != "" {
ret = append(ret, u.Name+" *qt."+u.Class) ret = append(ret, u.Name+" *qt."+u.Class)
} }
for _, w := range u.Widgets { for _, w := range u.Widgets {
ret = append(ret, collectClassNames_Widget(w)...) ret = append(ret, collectClassNames_Widget(&w)...)
} }
if u.Layout != nil { if u.Layout != nil {
ret = append(ret, u.Layout.Name+" *qt."+u.Layout.Class) ret = append(ret, u.Layout.Name+" *qt."+u.Layout.Class)
for _, li := range u.Layout.Items { for _, li := range u.Layout.Items {
ret = append(ret, collectClassNames_Widget(li.Widget)...) if li.Widget != nil {
ret = append(ret, collectClassNames_Widget(li.Widget)...)
}
if li.Spacer != nil {
ret = append(ret, li.Spacer.Name+" *qt.QSpacerItem")
}
} }
} }
for _, a := range u.Actions { for _, a := range u.Actions {
@ -45,47 +57,169 @@ func qwidgetName(name string, class string) string {
return name + ".QWidget" return name + ".QWidget"
} }
func generateWidget(w UiWidget, parentName string, parentClass string) (string, error) { func normalizeEnumName(s string) string {
ret := strings.Builder{} if strings.HasPrefix(s, `Qt::`) {
s = s[4:]
ctor, ok := constructorFunctionFor(w.Class)
if !ok {
return "", fmt.Errorf("No known constructor function for %q class %q", w.Name, w.Class)
} }
ret.WriteString(` return `qt.` + strings.Replace(s, `::`, `__`, -1)
ui.` + w.Name + ` = qt.` + ctor + `(` + qwidgetName(parentName, parentClass) + `) }
ui.` + w.Name + `.SetObjectName(` + strconv.Quote(w.Name) + `)
`)
// Properties func renderIcon(iconVal *UiIcon, ret *strings.Builder) string {
for _, prop := range w.Properties {
iconName := fmt.Sprintf("icon%d", IconCounter)
IconCounter++
ret.WriteString(iconName + " := qt.NewQIcon()\n")
// A base entry is a synonym for NormalOff. Don't need them both
if iconVal.NormalOff != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOff) + ", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)\n")
} else {
ret.WriteString(iconName + ".AddFile(" + strconv.Quote(strings.TrimSpace(iconVal.Base)) + ")\n")
}
if iconVal.NormalOn != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__On)\n")
}
if iconVal.ActiveOff != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Active, qt.QIcon__Off)\n")
}
if iconVal.ActiveOn != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Active, qt.QIcon__On)\n")
}
if iconVal.DisabledOff != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Disabled, qt.QIcon__Off)\n")
}
if iconVal.DisabledOn != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Disabled, qt.QIcon__On)\n")
}
if iconVal.SelectedOff != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Selected, qt.QIcon__Off)\n")
}
if iconVal.SelectedOn != nil {
ret.WriteString(iconName + ".AddFile4(" + strconv.Quote(*iconVal.NormalOn) + ", qt.NewQSize(), qt.QIcon__Selected, qt.QIcon__On)\n")
}
return iconName
}
func renderProperties(properties []UiProperty, ret *strings.Builder, targetName, parentClass string, isLayout bool) error {
contentsMargins := [4]int{DefaultGridMargin, DefaultGridMargin, DefaultGridMargin, DefaultGridMargin} // left, top, right, bottom
customContentsMargins := false
customSpacing := false
for _, prop := range properties {
setterFunc := `.Set` + strings.ToUpper(string(prop.Name[0])) + prop.Name[1:] setterFunc := `.Set` + strings.ToUpper(string(prop.Name[0])) + prop.Name[1:]
if prop.Name == "geometry" { if prop.Name == "geometry" {
if !(prop.RectVal.X == 0 && prop.RectVal.Y == 0) { if !(prop.RectVal.X == 0 && prop.RectVal.Y == 0) {
// Set all 4x properties // Set all 4x properties
ret.WriteString(`ui.` + w.Name + `.SetGeometry(qt.NewQRect(` + fmt.Sprintf("%d, %d, %d, %d", prop.RectVal.X, prop.RectVal.Y, prop.RectVal.Width, prop.RectVal.Height) + "))\n") ret.WriteString(`ui.` + targetName + `.SetGeometry(qt.NewQRect(` + fmt.Sprintf("%d, %d, %d, %d", prop.RectVal.X, prop.RectVal.Y, prop.RectVal.Width, prop.RectVal.Height) + "))\n")
} else if !(prop.RectVal.Width == 0 && prop.RectVal.Height == 0) { } else if !(prop.RectVal.Width == 0 && prop.RectVal.Height == 0) {
// Only width/height were supplied // Only width/height were supplied
ret.WriteString(`ui.` + w.Name + `.Resize(` + fmt.Sprintf("%d, %d", prop.RectVal.Width, prop.RectVal.Height) + ")\n") ret.WriteString(`ui.` + targetName + `.Resize(` + fmt.Sprintf("%d, %d", prop.RectVal.Width, prop.RectVal.Height) + ")\n")
} }
} else if prop.Name == "leftMargin" {
contentsMargins[0] = mustParseInt(*prop.NumberVal)
customContentsMargins = true
} else if prop.Name == "topMargin" {
contentsMargins[1] = mustParseInt(*prop.NumberVal)
customContentsMargins = true
} else if prop.Name == "rightMargin" {
contentsMargins[2] = mustParseInt(*prop.NumberVal)
customContentsMargins = true
} else if prop.Name == "bottomMargin" {
contentsMargins[3] = mustParseInt(*prop.NumberVal)
customContentsMargins = true
} else if prop.StringVal != nil { } else if prop.StringVal != nil {
// "windowTitle", "title", "text" // "windowTitle", "title", "text"
ret.WriteString(`ui.` + w.Name + setterFunc + `(` + generateString(prop.StringVal, parentClass) + ")\n") ret.WriteString(`ui.` + targetName + setterFunc + `(` + generateString(prop.StringVal, parentClass) + ")\n")
} else if prop.NumberVal != nil {
// "currentIndex"
if prop.Name == "spacing" {
customSpacing = true
}
ret.WriteString(`ui.` + targetName + setterFunc + `(` + *prop.NumberVal + ")\n")
} else if prop.BoolVal != nil {
// "childrenCollapsible"
ret.WriteString(`ui.` + targetName + setterFunc + `(` + formatBool(*prop.BoolVal) + ")\n")
} else if prop.EnumVal != nil { } else if prop.EnumVal != nil {
// "frameShape" // "frameShape"
ret.WriteString(`ui.` + w.Name + setterFunc + `(qt.` + strings.Replace(*prop.EnumVal, `::`, `__`, -1) + ")\n")
// Newer versions of Qt Designer produce the fully qualified enum
// names (A::B::C) but miqt changed to use the short names. Need to
// detect the case and convert it to match
ret.WriteString(`ui.` + targetName + setterFunc + `(` + normalizeEnumName(*prop.EnumVal) + ")\n")
} else if prop.SetVal != nil {
// QDialogButtonBox::"standardButtons"
// <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
parts := strings.Split(*prop.SetVal, `|`)
for i, p := range parts {
parts[i] = normalizeEnumName(p)
}
emit := "0"
if len(parts) > 0 {
emit = strings.Join(parts, `|`)
}
ret.WriteString(`ui.` + targetName + setterFunc + `(` + emit + ")\n")
} else if prop.IconVal != nil {
iconName := renderIcon(prop.IconVal, ret)
ret.WriteString(`ui.` + targetName + setterFunc + `(` + iconName + ")\n")
} else { } else {
ret.WriteString("/* miqt-uic: no handler for " + w.Name + " property '" + prop.Name + "' */\n") ret.WriteString("/* miqt-uic: no handler for " + targetName + " property '" + prop.Name + "' */\n")
} }
} }
if customContentsMargins || isLayout {
ret.WriteString(`ui.` + targetName + `.SetContentsMargins(` + fmt.Sprintf("%d, %d, %d, %d", contentsMargins[0], contentsMargins[1], contentsMargins[2], contentsMargins[3]) + ")\n")
}
if !customSpacing && isLayout {
// Layouts must specify spacing, unless, we specified it already
ret.WriteString(`ui.` + targetName + `.SetSpacing(` + fmt.Sprintf("%d", DefaultSpacing) + ")\n")
}
return nil
}
func generateWidget(w UiWidget, parentName string, parentClass string) (string, error) {
ret := strings.Builder{}
ctor := constructorFunctionFor(w.Class)
ret.WriteString(`
ui.` + w.Name + ` = qt.` + ctor + `(` + qwidgetName(parentName, parentClass) + `)
ui.` + w.Name + `.SetObjectName(` + strconv.Quote(w.Name) + `)
`)
if RootWindowName == "" {
RootWindowName = `ui.` + w.Name
}
// Properties
err := renderProperties(w.Properties, &ret, w.Name, parentClass, false)
if err != nil {
return "", err
}
// Attributes // Attributes
for _, attr := range w.Attributes { for _, attr := range w.Attributes {
@ -95,76 +229,113 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
} else if w.Class == "QDockWidget" && parentClass == "QMainWindow" && attr.Name == "dockWidgetArea" { } else if w.Class == "QDockWidget" && parentClass == "QMainWindow" && attr.Name == "dockWidgetArea" {
ret.WriteString(parentName + `.AddDockWidget(qt.DockWidgetArea(` + *attr.NumberVal + `), ui.` + w.Name + `)` + "\n") ret.WriteString(parentName + `.AddDockWidget(qt.DockWidgetArea(` + *attr.NumberVal + `), ui.` + w.Name + `)` + "\n")
} else if w.Class == "QToolBar" && parentClass == "QMainWindow" && attr.Name == "toolBarArea" {
ret.WriteString(parentName + `.AddToolBar(` + normalizeEnumName(*attr.EnumVal) + `, ui.` + w.Name + `)` + "\n")
} else if parentClass == "QTabWidget" && attr.Name == "icon" {
// This will be handled when we call .AddTab() on the parent QTabWidget
} else { } else {
ret.WriteString("/* miqt-uic: no handler for " + w.Name + " attribute '" + attr.Name + "' */\n") ret.WriteString("/* miqt-uic: no handler for " + w.Name + " attribute '" + attr.Name + "' */\n")
} }
} }
// TODO
// w.Attributes
// Layout // Layout
if w.Layout != nil { if w.Layout != nil {
ctor, ok := constructorFunctionFor(w.Layout.Class) ctor := constructorFunctionFor(w.Layout.Class)
if !ok {
return "", fmt.Errorf("No known constructor function for %q class %q", w.Layout.Name, w.Layout.Class)
}
ret.WriteString(` ret.WriteString(`
ui.` + w.Layout.Name + ` = qt.` + ctor + `(` + qwidgetName("ui."+w.Name, w.Class) + `) ui.` + w.Layout.Name + ` = qt.` + ctor + `(` + qwidgetName("ui."+w.Name, w.Class) + `)
ui.` + w.Layout.Name + `.SetObjectName(` + strconv.Quote(w.Layout.Name) + `) ui.` + w.Layout.Name + `.SetObjectName(` + strconv.Quote(w.Layout.Name) + `)
`) `)
for _, child := range w.Layout.Items { // Layout->Properties
// Layout items have the parent as the real QWidget parent and are err := renderProperties(w.Layout.Properties, &ret, w.Layout.Name, parentClass, true) // Always emit spacing/padding calls
// separately assigned to the layout afterwards if err != nil {
return "", err
}
nest, err := generateWidget(child.Widget, `ui.`+w.Name, w.Class) // Layout->Items
if err != nil {
return "", fmt.Errorf(w.Name+": %w", err) for i, child := range w.Layout.Items {
// A layout item is either a widget, or a spacer
if child.Spacer != nil {
ret.WriteString("/* miqt-uic: no handler for spacer */\n")
} }
ret.WriteString(nest) if child.Widget != nil {
// Assign to layout // Layout items have the parent as the real QWidget parent and are
// separately assigned to the layout afterwards
switch w.Layout.Class { nest, err := generateWidget(*child.Widget, `ui.`+w.Name, w.Class)
case `QFormLayout`: if err != nil {
// Row and Column are always populated. return "", fmt.Errorf(w.Name+"/Layout/Item[%d]: %w", i, err)
rowPos := fmt.Sprintf("%d", *child.Row)
var colPos string
if *child.Column == 0 {
colPos = `qt.QFormLayout__LabelRole`
} else if *child.Column == 1 {
colPos = `qt.QFormLayout__FieldRole`
} else {
ret.WriteString("/* miqt-uic: QFormLayout does not understand column index */\n")
continue
} }
// For QFormLayout it's SetWidget ret.WriteString(nest)
ret.WriteString(`
ui.` + w.Layout.Name + `.SetWidget(` + rowPos + `, ` + colPos + `, ` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `)
`)
case `QGridLayout`: // Assign to layout
// For QGridLayout it's AddWidget2
// FIXME in Miqt this function has optionals, needs to be called with the correct arity
// TODO support rowSpan, columnSpan
ret.WriteString(`
ui.` + w.Layout.Name + `.AddWidget2(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `, ` + fmt.Sprintf("%d, %d", *child.Row, *child.Column) + `)
`)
case "QVBoxLayout", "QHBoxLayout": switch w.Layout.Class {
// For box layout it's AddWidget case `QFormLayout`:
ret.WriteString(` // Row and Column are always populated.
ui.` + w.Layout.Name + `.AddWidget(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `) rowPos := fmt.Sprintf("%d", *child.Row)
`) var colPos string
if *child.Column == 0 {
colPos = `qt.QFormLayout__LabelRole`
} else if *child.Column == 1 {
colPos = `qt.QFormLayout__FieldRole`
} else {
ret.WriteString("/* miqt-uic: QFormLayout does not understand column index */\n")
continue
}
default: // For QFormLayout it's SetWidget
ret.WriteString("/* miqt-uic: no handler for layout '" + w.Layout.Class + "' */\n") ret.WriteString(`
ui.` + w.Layout.Name + `.SetWidget(` + rowPos + `, ` + colPos + `, ` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `)
`)
case `QGridLayout`:
if child.ColSpan != nil || child.RowSpan != nil {
// If either are present, use full four-value AddWidget3
rowSpan := 1
if child.RowSpan != nil {
rowSpan = *child.RowSpan
}
colSpan := 1
if child.ColSpan != nil {
colSpan = *child.ColSpan
}
ret.WriteString(`
ui.` + w.Layout.Name + `.AddWidget3(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `, ` + fmt.Sprintf("%d, %d, %d, %d", *child.Row, *child.Column, rowSpan, colSpan) + `)
`)
} else {
// Row and Column are always present in the .ui file
// For row/column it's AddWidget2
ret.WriteString(`
ui.` + w.Layout.Name + `.AddWidget2(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `, ` + fmt.Sprintf("%d, %d", *child.Row, *child.Column) + `)
`)
}
case "QVBoxLayout", "QHBoxLayout":
// For box layout it's AddWidget
ret.WriteString(`
ui.` + w.Layout.Name + `.AddWidget(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `)
`)
default:
ret.WriteString("/* miqt-uic: no handler for layout '" + w.Layout.Class + "' */\n")
}
} }
} }
} }
@ -185,6 +356,11 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
if prop, ok := propertyByName(a.Properties, "shortcut"); ok { if prop, ok := propertyByName(a.Properties, "shortcut"); ok {
ret.WriteString("ui." + a.Name + `.SetShortcut(qt.NewQKeySequence2(` + generateString(prop.StringVal, w.Class) + `))` + "\n") ret.WriteString("ui." + a.Name + `.SetShortcut(qt.NewQKeySequence2(` + generateString(prop.StringVal, w.Class) + `))` + "\n")
} }
if prop, ok := propertyByName(a.Properties, "icon"); ok {
iconName := renderIcon(prop.IconVal, &ret)
ret.WriteString(`ui.` + a.Name + `.SetIcon(` + iconName + ")\n")
}
} }
// Items // Items
@ -195,14 +371,29 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
// Check for a "text" property and update the item's text // Check for a "text" property and update the item's text
// Do this as a 2nd step so that the SetItemText can be trapped for retranslateUi() // Do this as a 2nd step so that the SetItemText can be trapped for retranslateUi()
// TODO Abstract for all SetItem{Foo} properties // TODO Abstract for all SetItem{Foo} properties
if prop, ok := propertyByName(itm.Properties, "text"); ok { for _, prop := range itm.Properties {
ret.WriteString("ui." + w.Name + `.SetItemText(` + fmt.Sprintf("%d", itemNo) + `, ` + generateString(prop.StringVal, w.Class) + `)` + "\n") if prop.Name == "text" {
ret.WriteString("ui." + w.Name + `.SetItemText(` + fmt.Sprintf("%d", itemNo) + `, ` + generateString(prop.StringVal, w.Class) + `)` + "\n")
} else {
ret.WriteString("/* miqt-uic: no handler for item property '" + prop.Name + "' */\n")
}
} }
} }
// Columns // Columns
// TODO
// w.Columns for colNo, col := range w.Columns {
for _, prop := range col.Properties {
if prop.Name == "text" {
ret.WriteString("ui." + w.Name + ".HeaderItem().SetText(" + fmt.Sprintf("%d", colNo) + ", " + generateString(prop.StringVal, w.Class) + ")\n")
} else {
ret.WriteString("/* miqt-uic: no handler for column property '" + prop.Name + "' */\n")
}
}
}
// Recurse children // Recurse children
var ( var (
@ -211,10 +402,10 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
setStatusBar = false setStatusBar = false
) )
for _, child := range w.Widgets { for i, child := range w.Widgets {
nest, err := generateWidget(child, `ui.`+w.Name, w.Class) nest, err := generateWidget(child, `ui.`+w.Name, w.Class)
if err != nil { if err != nil {
return "", fmt.Errorf(w.Name+": %w", err) return "", fmt.Errorf(w.Name+"/Widgets[%d]: %w", i, err)
} }
ret.WriteString(nest) ret.WriteString(nest)
@ -233,6 +424,15 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
setCentralWidget = true setCentralWidget = true
} }
if w.Class == "QSplitter" || w.Class == "QStackedWidget" {
// We need to manually AddWidget on every child of QSplitter
if child.Class == "QWidget" {
ret.WriteString(`ui.` + w.Name + `.AddWidget(ui.` + child.Name + `)` + "\n")
} else {
ret.WriteString(`ui.` + w.Name + `.AddWidget(ui.` + child.Name + `.QWidget)` + "\n")
}
}
if w.Class == "QMainWindow" && child.Class == "QMenuBar" && !setMenuBar { if w.Class == "QMainWindow" && child.Class == "QMenuBar" && !setMenuBar {
ret.WriteString(`ui.` + w.Name + `.SetMenuBar(ui.` + child.Name + `)` + "\n") ret.WriteString(`ui.` + w.Name + `.SetMenuBar(ui.` + child.Name + `)` + "\n")
setMenuBar = true setMenuBar = true
@ -244,8 +444,17 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
// QTabWidget->QTab handling // QTabWidget->QTab handling
if w.Class == `QTabWidget` { if w.Class == `QTabWidget` {
ret.WriteString(`ui.` + w.Name + `.AddTab(` + qwidgetName(`ui.`+child.Name, child.Class) + `, "")` + "\n") if icon, ok := propertyByName(child.Attributes, "icon"); ok {
// AddTab() overload with icon
iconName := renderIcon(icon.IconVal, &ret)
ret.WriteString(`ui.` + w.Name + `.AddTab2(` + qwidgetName(`ui.`+child.Name, child.Class) + `, ` + iconName + `, "")` + "\n")
} else {
// AddTab() overload without icon
ret.WriteString(`ui.` + w.Name + `.AddTab(` + qwidgetName(`ui.`+child.Name, child.Class) + `, "")` + "\n")
}
} }
} }
// AddActions // AddActions
@ -261,7 +470,7 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
// If we are a menubar, then <addaction> refers to top-level QMenu instead of QAction // If we are a menubar, then <addaction> refers to top-level QMenu instead of QAction
if w.Class == "QMenuBar" { if w.Class == "QMenuBar" {
ret.WriteString("ui." + w.Name + ".AddMenu(ui." + a.Name + ")\n") ret.WriteString("ui." + w.Name + ".AddMenu(ui." + a.Name + ")\n")
} else if w.Class == "QMenu" { } else if w.Class == "QMenu" || w.Class == "QToolBar" {
// QMenu has its own .AddAction() implementation that takes plain string // QMenu has its own .AddAction() implementation that takes plain string
// That's convenient, but it shadows the AddAction version that takes a QAction* // That's convenient, but it shadows the AddAction version that takes a QAction*
// We need to use the underlying QWidget.AddAction explicitly // We need to use the underlying QWidget.AddAction explicitly
@ -272,11 +481,32 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
} }
} }
if w.Class == "QDialogButtonBox" {
// TODO make this using a native connection instead of a C++ -> Go -> C++ roundtrip
ret.WriteString(`ui.` + w.Name + `.OnAccepted(` + RootWindowName + ".Accept)\n")
ret.WriteString(`ui.` + w.Name + `.OnRejected(` + RootWindowName + ".Reject)\n")
}
return ret.String(), nil return ret.String(), nil
} }
func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, error) { func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, error) {
ret := strings.Builder{} ret := strings.Builder{}
// Update globals for layoutdefault, if present
if u.LayoutDefault != nil {
if u.LayoutDefault.Spacing != nil {
DefaultSpacing = *u.LayoutDefault.Spacing
}
if u.LayoutDefault.Margin != nil {
DefaultGridMargin = *u.LayoutDefault.Margin
}
}
// Header
ret.WriteString(`// Generated by miqt-uic. To update this file, edit the .ui file in ret.WriteString(`// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'. // Qt Designer, and then run 'go generate'.
// //
@ -289,7 +519,7 @@ import (
) )
type ` + u.Class + `Ui struct { type ` + u.Class + `Ui struct {
` + strings.Join(collectClassNames_Widget(u.Widget), "\n") + ` ` + strings.Join(collectClassNames_Widget(&u.Widget), "\n") + `
} }
// New` + u.Class + `Ui creates all Qt widget classes for ` + u.Class + `. // New` + u.Class + `Ui creates all Qt widget classes for ` + u.Class + `.
@ -305,17 +535,24 @@ func New` + u.Class + `Ui() *` + u.Class + `Ui {
// Don't emit any of the lines that included .Tr(), move them into the // Don't emit any of the lines that included .Tr(), move them into the
// retranslateUi() function // retranslateUi() function
var translateFunc []string var translateFunc []string
var setCurrentIndex []string
for _, line := range strings.Split(nest, "\n") { for _, line := range strings.Split(nest, "\n") {
if strings.Contains(line, `_Tr(`) { if strings.Contains(line, `_Tr(`) {
translateFunc = append(translateFunc, line) translateFunc = append(translateFunc, line)
} else if strings.Contains(line, `.SetCurrentIndex(`) {
setCurrentIndex = append(setCurrentIndex, line)
} else { } else {
ret.WriteString(line + "\n") ret.WriteString(line + "\n")
} }
} }
ret.WriteString("\nui.Retranslate()\n\n")
for _, sci := range setCurrentIndex {
ret.WriteString(sci + "\n")
}
ret.WriteString(` ret.WriteString(`
ui.Retranslate()
return ui return ui
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
) )
@ -82,3 +83,19 @@ func propertyByName(check []UiProperty, search string) (UiProperty, bool) {
return UiProperty{}, false return UiProperty{}, false
} }
func formatBool(b bool) string {
if b {
return "true"
}
return "false"
}
func mustParseInt(s string) int {
val, err := strconv.ParseInt(s, 10, 64)
if err != nil {
panic("parseInt(" + s + "): " + err.Error())
}
return int(val) // n.b. might do 32-bit truncation(!)
}

File diff suppressed because one or more lines are too long

BIN
doc/architecture-uic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB