mirror of
https://github.com/mappu/miqt.git
synced 2025-01-20 21:50:38 +00:00
commit
e41aa625bd
@ -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.
|
||||
|
||||
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
|
||||
|
||||
@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
### Linux (native)
|
||||
|
55
cmd/miqt-rcc/miqt-rcc.sh
Executable file
55
cmd/miqt-rcc/miqt-rcc.sh
Executable 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 "$@"
|
@ -1,165 +1,168 @@
|
||||
package main
|
||||
|
||||
func constructorFunctionFor(className string) (string, bool) {
|
||||
func constructorFunctionFor(className string) string {
|
||||
|
||||
// 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 {
|
||||
|
||||
// CODEGENERATED LIST START
|
||||
|
||||
case "QListWidget":
|
||||
return "NewQListWidget2", true
|
||||
return "NewQListWidget2"
|
||||
case "QAbstractSpinBox":
|
||||
return "NewQAbstractSpinBox2", true
|
||||
return "NewQAbstractSpinBox2"
|
||||
case "QStackedLayout":
|
||||
return "NewQStackedLayout2", true
|
||||
return "NewQStackedLayout2"
|
||||
case "QColumnView":
|
||||
return "NewQColumnView2", true
|
||||
return "NewQColumnView2"
|
||||
case "QProgressDialog":
|
||||
return "NewQProgressDialog3", true
|
||||
return "NewQProgressDialog3"
|
||||
case "QTabWidget":
|
||||
return "NewQTabWidget2", true
|
||||
return "NewQTabWidget2"
|
||||
case "QLabel":
|
||||
return "NewQLabel3", true
|
||||
return "NewQLabel3"
|
||||
case "QKeySequenceEdit":
|
||||
return "NewQKeySequenceEdit3", true
|
||||
return "NewQKeySequenceEdit3"
|
||||
case "QDockWidget":
|
||||
return "NewQDockWidget5", true
|
||||
return "NewQDockWidget5"
|
||||
case "QFontComboBox":
|
||||
return "NewQFontComboBox2", true
|
||||
return "NewQFontComboBox2"
|
||||
case "QTreeView":
|
||||
return "NewQTreeView2", true
|
||||
return "NewQTreeView2"
|
||||
case "QCalendarWidget":
|
||||
return "NewQCalendarWidget2", true
|
||||
return "NewQCalendarWidget2"
|
||||
case "QLineEdit":
|
||||
return "NewQLineEdit3", true
|
||||
return "NewQLineEdit3"
|
||||
case "QMenuBar":
|
||||
return "NewQMenuBar2", true
|
||||
return "NewQMenuBar2"
|
||||
case "QFrame":
|
||||
return "NewQFrame2", true
|
||||
return "NewQFrame2"
|
||||
case "QAbstractScrollArea":
|
||||
return "NewQAbstractScrollArea2", true
|
||||
return "NewQAbstractScrollArea2"
|
||||
case "QSplitter":
|
||||
return "NewQSplitter3", true
|
||||
return "NewQSplitter3"
|
||||
case "QStackedWidget":
|
||||
return "NewQStackedWidget2", true
|
||||
return "NewQStackedWidget2"
|
||||
case "QWizard":
|
||||
return "NewQWizard2", true
|
||||
return "NewQWizard2"
|
||||
case "QWizardPage":
|
||||
return "NewQWizardPage2", true
|
||||
return "NewQWizardPage2"
|
||||
case "QMdiSubWindow":
|
||||
return "NewQMdiSubWindow2", true
|
||||
return "NewQMdiSubWindow2"
|
||||
case "QStatusBar":
|
||||
return "NewQStatusBar2", true
|
||||
return "NewQStatusBar2"
|
||||
case "QToolButton":
|
||||
return "NewQToolButton2", true
|
||||
return "NewQToolButton2"
|
||||
case "QShortcut":
|
||||
return "NewQShortcut", true
|
||||
return "NewQShortcut"
|
||||
case "QSlider":
|
||||
return "NewQSlider3", true
|
||||
return "NewQSlider3"
|
||||
case "QComboBox":
|
||||
return "NewQComboBox2", true
|
||||
return "NewQComboBox2"
|
||||
case "QScrollBar":
|
||||
return "NewQScrollBar3", true
|
||||
return "NewQScrollBar3"
|
||||
case "QTabBar":
|
||||
return "NewQTabBar2", true
|
||||
return "NewQTabBar2"
|
||||
case "QTextBrowser":
|
||||
return "NewQTextBrowser2", true
|
||||
return "NewQTextBrowser2"
|
||||
case "QTreeWidget":
|
||||
return "NewQTreeWidget2", true
|
||||
return "NewQTreeWidget2"
|
||||
case "QDialog":
|
||||
return "NewQDialog2", true
|
||||
return "NewQDialog2"
|
||||
case "QFormLayout":
|
||||
return "NewQFormLayout2", true
|
||||
return "NewQFormLayout2"
|
||||
case "QToolBar":
|
||||
return "NewQToolBar4", true
|
||||
return "NewQToolBar4"
|
||||
case "QWidget":
|
||||
return "NewQWidget2", true
|
||||
return "NewQWidget2"
|
||||
case "QRadioButton":
|
||||
return "NewQRadioButton3", true
|
||||
return "NewQRadioButton3"
|
||||
case "QCheckBox":
|
||||
return "NewQCheckBox3", true
|
||||
return "NewQCheckBox3"
|
||||
case "QSizeGrip":
|
||||
return "NewQSizeGrip", true
|
||||
return "NewQSizeGrip"
|
||||
case "QLCDNumber":
|
||||
return "NewQLCDNumber3", true
|
||||
return "NewQLCDNumber3"
|
||||
case "QFileDialog":
|
||||
return "NewQFileDialog3", true
|
||||
return "NewQFileDialog3"
|
||||
case "QUndoView":
|
||||
return "NewQUndoView4", true
|
||||
return "NewQUndoView4"
|
||||
case "QGraphicsView":
|
||||
return "NewQGraphicsView3", true
|
||||
return "NewQGraphicsView3"
|
||||
case "QPushButton":
|
||||
return "NewQPushButton4", true
|
||||
return "NewQPushButton4"
|
||||
case "QColorDialog":
|
||||
return "NewQColorDialog3", true
|
||||
return "NewQColorDialog3"
|
||||
case "QMessageBox":
|
||||
return "NewQMessageBox4", true
|
||||
return "NewQMessageBox4"
|
||||
case "QSplashScreen":
|
||||
return "NewQSplashScreen3", true
|
||||
return "NewQSplashScreen3"
|
||||
case "QErrorMessage":
|
||||
return "NewQErrorMessage2", true
|
||||
return "NewQErrorMessage2"
|
||||
case "QListView":
|
||||
return "NewQListView2", true
|
||||
return "NewQListView2"
|
||||
case "QDateTimeEdit":
|
||||
return "NewQDateTimeEdit5", true
|
||||
return "NewQDateTimeEdit5"
|
||||
case "QTimeEdit":
|
||||
return "NewQTimeEdit3", true
|
||||
return "NewQTimeEdit3"
|
||||
case "QDateEdit":
|
||||
return "NewQDateEdit3", true
|
||||
return "NewQDateEdit3"
|
||||
case "QMenu":
|
||||
return "NewQMenu3", true
|
||||
return "NewQMenu3"
|
||||
case "QToolBox":
|
||||
return "NewQToolBox2", true
|
||||
return "NewQToolBox2"
|
||||
case "QTableWidget":
|
||||
return "NewQTableWidget3", true
|
||||
return "NewQTableWidget3"
|
||||
case "QFocusFrame":
|
||||
return "NewQFocusFrame2", true
|
||||
return "NewQFocusFrame2"
|
||||
case "QHBoxLayout":
|
||||
return "NewQHBoxLayout2", true
|
||||
return "NewQHBoxLayout2"
|
||||
case "QVBoxLayout":
|
||||
return "NewQVBoxLayout2", true
|
||||
return "NewQVBoxLayout2"
|
||||
case "QInputDialog":
|
||||
return "NewQInputDialog2", true
|
||||
return "NewQInputDialog2"
|
||||
case "QTableView":
|
||||
return "NewQTableView2", true
|
||||
return "NewQTableView2"
|
||||
case "QMdiArea":
|
||||
return "NewQMdiArea2", true
|
||||
return "NewQMdiArea2"
|
||||
case "QSpinBox":
|
||||
return "NewQSpinBox2", true
|
||||
return "NewQSpinBox2"
|
||||
case "QDoubleSpinBox":
|
||||
return "NewQDoubleSpinBox2", true
|
||||
return "NewQDoubleSpinBox2"
|
||||
case "QProgressBar":
|
||||
return "NewQProgressBar2", true
|
||||
return "NewQProgressBar2"
|
||||
case "QTextEdit":
|
||||
return "NewQTextEdit3", true
|
||||
return "NewQTextEdit3"
|
||||
case "QAbstractSlider":
|
||||
return "NewQAbstractSlider2", true
|
||||
return "NewQAbstractSlider2"
|
||||
case "QDialogButtonBox":
|
||||
return "NewQDialogButtonBox5", true
|
||||
return "NewQDialogButtonBox5"
|
||||
case "QFontDialog":
|
||||
return "NewQFontDialog3", true
|
||||
return "NewQFontDialog3"
|
||||
case "QMainWindow":
|
||||
return "NewQMainWindow2", true
|
||||
return "NewQMainWindow2"
|
||||
case "QCommandLinkButton":
|
||||
return "NewQCommandLinkButton4", true
|
||||
return "NewQCommandLinkButton4"
|
||||
case "QDial":
|
||||
return "NewQDial2", true
|
||||
return "NewQDial2"
|
||||
case "QGridLayout":
|
||||
return "NewQGridLayout", true
|
||||
return "NewQGridLayout"
|
||||
case "QPlainTextEdit":
|
||||
return "NewQPlainTextEdit3", true
|
||||
return "NewQPlainTextEdit3"
|
||||
case "QScrollArea":
|
||||
return "NewQScrollArea2", true
|
||||
return "NewQScrollArea2"
|
||||
case "QGroupBox":
|
||||
return "NewQGroupBox3", true
|
||||
return "NewQGroupBox3"
|
||||
|
||||
// CODEGENERATED LIST END
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
@ -5,21 +5,32 @@ import (
|
||||
)
|
||||
|
||||
type UiLayoutItem struct {
|
||||
Row *int `xml:"row,attr"`
|
||||
Column *int `xml:"column,attr"`
|
||||
Widget UiWidget `xml:"widget"`
|
||||
Row *int `xml:"row,attr"`
|
||||
Column *int `xml:"column,attr"`
|
||||
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 {
|
||||
Class string `xml:"class,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Items []UiLayoutItem `xml:"item"`
|
||||
Class string `xml:"class,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Properties []UiProperty `xml:"property"`
|
||||
Items []UiLayoutItem `xml:"item"`
|
||||
}
|
||||
|
||||
type UiPropertyContainer struct {
|
||||
Properties []UiProperty `xml:"property"`
|
||||
}
|
||||
|
||||
type UiSpacer struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Properties []UiProperty `xml:"property"`
|
||||
}
|
||||
|
||||
type UiWidget struct {
|
||||
Class string `xml:"class,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
@ -48,12 +59,30 @@ type UiString struct {
|
||||
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 {
|
||||
Name string `xml:"name,attr"`
|
||||
StringVal *UiString `xml:"string,omitempty"`
|
||||
NumberVal *string `xml:"number,omitempty"` // Preserve as string literal
|
||||
BoolVal *bool `xml:"bool,omitempty"` // "true" or "false"
|
||||
EnumVal *string `xml:"enum,omitempty"`
|
||||
RectVal *UiRect `xml:"rect,omitempty"`
|
||||
IconVal *UiIcon `xml:"iconset,omitempty"`
|
||||
SetVal *string `xml:"set,omitempty"`
|
||||
}
|
||||
|
||||
type UiActionReference struct {
|
||||
@ -71,12 +100,18 @@ type UiResources struct {
|
||||
type UiConnections struct {
|
||||
}
|
||||
|
||||
type UiLayoutDefault struct {
|
||||
Spacing *int `xml:"spacing,attr,omitempty"`
|
||||
Margin *int `xml:"margin,attr,omitempty"`
|
||||
}
|
||||
|
||||
type UiFile struct {
|
||||
XMLName xml.Name // should always be xml.Name{Local: "ui"}
|
||||
|
||||
Class string `xml:"class"`
|
||||
Version string `xml:"version,attr"` // e.g. 4.0
|
||||
Widget UiWidget `xml:"widget"` // There's only one root widget
|
||||
Resources UiResources `xml:"resources"`
|
||||
Connections UiConnections `xml:"connections"`
|
||||
Class string `xml:"class"`
|
||||
Version string `xml:"version,attr"` // e.g. 4.0
|
||||
Widget UiWidget `xml:"widget"` // There's only one root widget
|
||||
Resources UiResources `xml:"resources"`
|
||||
Connections UiConnections `xml:"connections"`
|
||||
LayoutDefault *UiLayoutDefault `xml:"layoutdefault,omitempty"`
|
||||
}
|
||||
|
@ -7,18 +7,30 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func collectClassNames_Widget(u UiWidget) []string {
|
||||
var (
|
||||
RootWindowName = ""
|
||||
DefaultGridMargin = 11
|
||||
DefaultSpacing = 6
|
||||
IconCounter = 0
|
||||
)
|
||||
|
||||
func collectClassNames_Widget(u *UiWidget) []string {
|
||||
var ret []string
|
||||
if u.Name != "" {
|
||||
ret = append(ret, u.Name+" *qt."+u.Class)
|
||||
}
|
||||
for _, w := range u.Widgets {
|
||||
ret = append(ret, collectClassNames_Widget(w)...)
|
||||
ret = append(ret, collectClassNames_Widget(&w)...)
|
||||
}
|
||||
if u.Layout != nil {
|
||||
ret = append(ret, u.Layout.Name+" *qt."+u.Layout.Class)
|
||||
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 {
|
||||
@ -45,47 +57,169 @@ func qwidgetName(name string, class string) string {
|
||||
return name + ".QWidget"
|
||||
}
|
||||
|
||||
func generateWidget(w UiWidget, parentName string, parentClass string) (string, error) {
|
||||
ret := strings.Builder{}
|
||||
|
||||
ctor, ok := constructorFunctionFor(w.Class)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("No known constructor function for %q class %q", w.Name, w.Class)
|
||||
func normalizeEnumName(s string) string {
|
||||
if strings.HasPrefix(s, `Qt::`) {
|
||||
s = s[4:]
|
||||
}
|
||||
|
||||
ret.WriteString(`
|
||||
ui.` + w.Name + ` = qt.` + ctor + `(` + qwidgetName(parentName, parentClass) + `)
|
||||
ui.` + w.Name + `.SetObjectName(` + strconv.Quote(w.Name) + `)
|
||||
`)
|
||||
return `qt.` + strings.Replace(s, `::`, `__`, -1)
|
||||
}
|
||||
|
||||
// Properties
|
||||
for _, prop := range w.Properties {
|
||||
func renderIcon(iconVal *UiIcon, ret *strings.Builder) string {
|
||||
|
||||
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:]
|
||||
|
||||
if prop.Name == "geometry" {
|
||||
if !(prop.RectVal.X == 0 && prop.RectVal.Y == 0) {
|
||||
// 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) {
|
||||
// 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 {
|
||||
// "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 {
|
||||
// "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 {
|
||||
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
|
||||
|
||||
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" {
|
||||
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 {
|
||||
ret.WriteString("/* miqt-uic: no handler for " + w.Name + " attribute '" + attr.Name + "' */\n")
|
||||
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
// w.Attributes
|
||||
|
||||
// Layout
|
||||
|
||||
if w.Layout != nil {
|
||||
ctor, ok := constructorFunctionFor(w.Layout.Class)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("No known constructor function for %q class %q", w.Layout.Name, w.Layout.Class)
|
||||
}
|
||||
ctor := constructorFunctionFor(w.Layout.Class)
|
||||
|
||||
ret.WriteString(`
|
||||
ui.` + w.Layout.Name + ` = qt.` + ctor + `(` + qwidgetName("ui."+w.Name, w.Class) + `)
|
||||
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
|
||||
// separately assigned to the layout afterwards
|
||||
err := renderProperties(w.Layout.Properties, &ret, w.Layout.Name, parentClass, true) // Always emit spacing/padding calls
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nest, err := generateWidget(child.Widget, `ui.`+w.Name, w.Class)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(w.Name+": %w", err)
|
||||
// Layout->Items
|
||||
|
||||
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 {
|
||||
case `QFormLayout`:
|
||||
// Row and Column are always populated.
|
||||
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
|
||||
nest, err := generateWidget(*child.Widget, `ui.`+w.Name, w.Class)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(w.Name+"/Layout/Item[%d]: %w", i, err)
|
||||
}
|
||||
|
||||
// For QFormLayout it's SetWidget
|
||||
ret.WriteString(`
|
||||
ui.` + w.Layout.Name + `.SetWidget(` + rowPos + `, ` + colPos + `, ` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `)
|
||||
`)
|
||||
ret.WriteString(nest)
|
||||
|
||||
case `QGridLayout`:
|
||||
// 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) + `)
|
||||
`)
|
||||
// Assign to layout
|
||||
|
||||
case "QVBoxLayout", "QHBoxLayout":
|
||||
// For box layout it's AddWidget
|
||||
ret.WriteString(`
|
||||
ui.` + w.Layout.Name + `.AddWidget(` + qwidgetName(`ui.`+child.Widget.Name, child.Widget.Class) + `)
|
||||
`)
|
||||
switch w.Layout.Class {
|
||||
case `QFormLayout`:
|
||||
// Row and Column are always populated.
|
||||
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:
|
||||
ret.WriteString("/* miqt-uic: no handler for layout '" + w.Layout.Class + "' */\n")
|
||||
// For QFormLayout it's SetWidget
|
||||
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 {
|
||||
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
|
||||
@ -195,14 +371,29 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
|
||||
// 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()
|
||||
// TODO Abstract for all SetItem{Foo} properties
|
||||
if prop, ok := propertyByName(itm.Properties, "text"); ok {
|
||||
ret.WriteString("ui." + w.Name + `.SetItemText(` + fmt.Sprintf("%d", itemNo) + `, ` + generateString(prop.StringVal, w.Class) + `)` + "\n")
|
||||
for _, prop := range itm.Properties {
|
||||
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
|
||||
// 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
|
||||
var (
|
||||
@ -211,10 +402,10 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
|
||||
setStatusBar = false
|
||||
)
|
||||
|
||||
for _, child := range w.Widgets {
|
||||
for i, child := range w.Widgets {
|
||||
nest, err := generateWidget(child, `ui.`+w.Name, w.Class)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(w.Name+": %w", err)
|
||||
return "", fmt.Errorf(w.Name+"/Widgets[%d]: %w", i, err)
|
||||
}
|
||||
|
||||
ret.WriteString(nest)
|
||||
@ -233,6 +424,15 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
|
||||
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 {
|
||||
ret.WriteString(`ui.` + w.Name + `.SetMenuBar(ui.` + child.Name + `)` + "\n")
|
||||
setMenuBar = true
|
||||
@ -244,8 +444,17 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string,
|
||||
|
||||
// QTabWidget->QTab handling
|
||||
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
|
||||
@ -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 w.Class == "QMenuBar" {
|
||||
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
|
||||
// That's convenient, but it shadows the AddAction version that takes a QAction*
|
||||
// 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
|
||||
}
|
||||
|
||||
func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, error) {
|
||||
|
||||
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
|
||||
// Qt Designer, and then run 'go generate'.
|
||||
//
|
||||
@ -289,7 +519,7 @@ import (
|
||||
)
|
||||
|
||||
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 + `.
|
||||
@ -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
|
||||
// retranslateUi() function
|
||||
var translateFunc []string
|
||||
var setCurrentIndex []string
|
||||
for _, line := range strings.Split(nest, "\n") {
|
||||
if strings.Contains(line, `_Tr(`) {
|
||||
translateFunc = append(translateFunc, line)
|
||||
} else if strings.Contains(line, `.SetCurrentIndex(`) {
|
||||
setCurrentIndex = append(setCurrentIndex, line)
|
||||
} else {
|
||||
ret.WriteString(line + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
ret.WriteString("\nui.Retranslate()\n\n")
|
||||
|
||||
for _, sci := range setCurrentIndex {
|
||||
ret.WriteString(sci + "\n")
|
||||
}
|
||||
|
||||
ret.WriteString(`
|
||||
ui.Retranslate()
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -82,3 +83,19 @@ func propertyByName(check []UiProperty, search string) (UiProperty, bool) {
|
||||
|
||||
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(!)
|
||||
}
|
||||
|
1804
doc/architecture-uic.excalidraw
Normal file
1804
doc/architecture-uic.excalidraw
Normal file
File diff suppressed because one or more lines are too long
BIN
doc/architecture-uic.png
Normal file
BIN
doc/architecture-uic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
Loading…
x
Reference in New Issue
Block a user