package main import ( "fmt" "go/format" "strconv" "strings" ) 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)...) } if u.Layout != nil { ret = append(ret, u.Layout.Name+" *qt."+u.Layout.Class) for _, li := range u.Layout.Items { 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 { ret = append(ret, a.Name+" *qt.QAction") } return ret } func generateString(s *UiString, parentClass string) string { if s.Notr || parentClass == "" { return strconv.Quote(s.Value) } return `qt.` + parentClass + `_Tr(` + strconv.Quote(s.Value) + `)` } // qwidgetName creates the T.QWidget name that MIQT needs to access the base class. func qwidgetName(name string, class string) string { if name == "" { return "nil" } if class == "QWidget" { return name // It's already the right type } return name + ".QWidget" } func normalizeEnumName(s string) string { if strings.HasPrefix(s, `Qt::`) { s = s[4:] } return `qt.` + strings.Replace(s, `::`, `__`, -1) } 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.` + 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.` + 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.` + 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" // 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" // QDialogButtonBox::Cancel|QDialogButtonBox::Save 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 " + 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 := "New" + 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 { if parentClass == "QTabWidget" && attr.Name == "title" { ret.WriteString(parentName + `.SetTabText(` + parentName + ".IndexOf(ui." + w.Name + "), " + generateString(attr.StringVal, parentClass) + ")\n") } 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") } } // Layout if w.Layout != nil { ctor := "New" + 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) + `) `) // Layout->Properties err := renderProperties(w.Layout.Properties, &ret, w.Layout.Name, parentClass, true) // Always emit spacing/padding calls if err != nil { return "", 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") } if child.Widget != nil { // Layout items have the parent as the real QWidget parent and are // separately assigned to the layout afterwards nest, err := generateWidget(*child.Widget, `ui.`+w.Name, w.Class) if err != nil { return "", fmt.Errorf(w.Name+"/Layout/Item[%d]: %w", i, err) } ret.WriteString(nest) // Assign to layout 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 } // 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") } } } } // Actions for _, a := range w.Actions { ret.WriteString(` ui.` + a.Name + ` = qt.NewQAction(` + parentName + `) ui.` + a.Name + `.SetObjectName(` + strconv.Quote(a.Name) + `) `) // QActions are translated in the parent window's context if prop, ok := propertyByName(a.Properties, "text"); ok { ret.WriteString("ui." + a.Name + `.SetText(` + generateString(prop.StringVal, w.Class) + `)` + "\n") } 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 for itemNo, itm := range w.Items { ret.WriteString("ui." + w.Name + `.AddItem("")` + "\n") // 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 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 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 ( setCentralWidget = false setMenuBar = false setStatusBar = false ) for i, child := range w.Widgets { nest, err := generateWidget(child, `ui.`+w.Name, w.Class) if err != nil { return "", fmt.Errorf(w.Name+"/Widgets[%d]: %w", i, err) } ret.WriteString(nest) // QMainWindow CentralWidget handling // The first listed class can be the central widget. // TODO should it be the first child with a layout? But need to handle windows with no layout if w.Class == `QMainWindow` && !setCentralWidget { ret.WriteString(`ui.` + w.Name + `.SetCentralWidget(ui.` + child.Name + ") // Set central widget \n") setCentralWidget = true } // QDockWidget also has something like a central widget if w.Class == `QDockWidget` && !setCentralWidget { ret.WriteString(`ui.` + w.Name + `.SetWidget(ui.` + child.Name + ") // Set central widget \n") 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 } if w.Class == "QMainWindow" && child.Class == "QStatusBar" && !setStatusBar { ret.WriteString(`ui.` + w.Name + `.SetStatusBar(ui.` + child.Name + `)` + "\n") setStatusBar = true } // QTabWidget->QTab handling if w.Class == `QTabWidget` { 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 // n.b. This must be *after* all children have been constructed, in case we // are adding a direct child for _, a := range w.AddActions { if a.Name == "separator" { // TODO how does Qt Designer disambiguate a real QAction with name="separator" ? ret.WriteString("ui." + w.Name + ".AddSeparator()\n") } else { // If we are a menubar, then 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" || 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 ret.WriteString("ui." + w.Name + ".QWidget.AddAction(ui." + a.Name + ")\n") } else { ret.WriteString("ui." + w.Name + ".AddAction(ui." + a.Name + ")\n") } } } 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'. // //go:` + `generate miqt-uic ` + goGenerateArgs + ` package ` + packageName + ` import ( "github.com/mappu/miqt/qt" ) type ` + u.Class + `Ui struct { ` + strings.Join(collectClassNames_Widget(&u.Widget), "\n") + ` } // New` + u.Class + `Ui creates all Qt widget classes for ` + u.Class + `. func New` + u.Class + `Ui() *` + u.Class + `Ui { ui := &` + u.Class + `Ui{} `) nest, err := generateWidget(u.Widget, "", "") if err != nil { return nil, err } // 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(` return ui } // Retranslate reapplies all text translations. func (ui *` + u.Class + `Ui) Retranslate() { ` + strings.Join(translateFunc, "\n") + ` } `) output := ret.String() formatted, err := format.Source([]byte(output)) if err != nil { // Return unformatted so it can be fixed return []byte(output), nil } return formatted, nil }