Search code examples
gogtkgtk4

Can you add a GTK4 applicationWindow from a ui file to a GTK4 application?


I'm only a couple of days into using GTK as I needed a GUI for my go program so this is a newbie question. Being new I took the simplest way I could find to get started and used Cambalache to generate a .UI file and then called that in my go app.

I put everything under an ApplicationWindow in the .UI file and got it to load and work fine with gkt4-builder-tool but in my go app I got no events in the GUI. Looking at the sample code it occurred to me that I need to attach the ApplicationWindow to the GTK app that is instantiated in my go program and I verified this by changing the ApplicationWindow to just a window and it worked just fine as I could add a normal window to the GTK app.

The problem is I can't find a way to attach an ApplicationWindow to an Application after extracting it from the UI file. The only method for attaching windows to apps is app.AddWindow() but that only adds GtkWindow, not GtkApplicationWindow and in a strongly typed language like go that's a "no go" (no pun intended).

You can export a GTK App right into the .UI file along with the ApplicationWindow and in theory if you could do this successfully you could extract them both and perhaps have them joined that way however, I have not successfully exported a GTK Application, ApplicationWindow and Menu as gkt4-builder-tool always comes up with a validation error (after trying many configurations) so either Cambalache doesn't know how to export that combination; or I don't know how to make it do so; or it's not really a legal combination in a .UI file.

So my question is, should I just abandon trying to store an ApplicationWindow in a .UI file and just build the ApplicationWindow widget in code or are there other options I just haven't yet learned?

If it's not really necessary I could abandoned having an ApplicationWindow and just go with Windows as another path.

Your knowledge and experience appreciated.

Thanks!

UPDATE:

To be clearer based on Kripto's comments.

Here is the code. This won't run directly as I extracted the relevant snippet in main() from a much larger program and env, log := boot.Initialize() doesn't exist in this snippet but it shouldn't matter to understand the problem.

I am aware that there is only one Application Window in a GTK GUI. The concept is akin, if not identical, to an Application Window from my old Visual Basic days.

Below is the go code and a cut down version with the relevant elements from the .UI file:

If I change line 8 in the .UI file from this:

<object class="GtkApplicationWindow" id="appWin">

to this:

<object class="GtkWindow" id="appWin">

The the Go code will open the .UI file and it will work but now I don't have an Application window.

If I leave line 8 in the .UI file as is then it will still open the file but the resulting GUI has no interaction and it only closes when I stop the Go program which I'm running in debug mode in GoLand. This is because to run it I have to make the two changes to the go program.:

  1. Change this line:

appWindow := builder.GetObject(appWin).Cast().(*gtk.Window)

to

appWindow := builder.GetObject(appWin).Cast().(*gtk.ApplicationWindow)

  1. Comment out this line:

app.AddWindow(appWindow)

And 2nd one is the problem because now the Window is not attached to the application which I am sure is why it's non-interactive.

package main

import (
    "os"

    "github.com/diamondburned/gotk4/pkg/gio/v2"
    "github.com/diamondburned/gotk4/pkg/gtk/v4"

    "gitlab.com/trading5124936/core.git/loggers"
)

func main() {
    env, log := boot.Initialize()
    var app *gtk.Application
    app = gtk.NewApplication(`site.TradingAnalyzer`, gio.ApplicationFlagsNone)
    app.ConnectActivate(func() { activate(app, log, env.Paths.GUIFile) })

    if code := app.Run(os.Args); code > 0 {
        os.Exit(code)
    }

}

func activate(app *gtk.Application, log *loggers.Logger, guiPath string) {
    // You can build UIs using Cambalache (https://flathub.org/apps/details/ar.xjuan.Cambalache)

    b, err := os.ReadFile(guiPath)
    if err != nil {
        log.Critical(err)
        return
    }
    uiXML := string(b)

    builder := gtk.NewBuilderFromString(uiXML, len(uiXML))

    // MainWindow and Button are object IDs from the UI file
    appWindow := builder.GetObject(`appWin`).Cast().(*gtk.Window)

    entry := builder.GetObject(`GeneralSetup.Timezone`).Cast().(*gtk.Entry)
    entry.Connect("changed", func() {
        println(`Changed`)
    })

    app.AddWindow(appWindow)
    appWindow.Show()

}

And here is the .UI file:

<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.16.0 -->
<interface domain="ta.site">
  <!-- interface-name TradingAnalyzer.ui -->
  <!-- interface-authors Reg Proctor -->
  <requires lib="gtk" version="4.6"/>
  <object class="GtkApplication" id="app"/>
  <object class="GtkApplicationWindow" id="appWin">
    <property name="default-height">925</property>
    <property name="default-width">1200</property>
    <child>
      <object class="GtkPaned">
        <child>
          <object class="GtkFrame">
            <property name="label">Settings</property>
            <child>
              <object class="GtkStackSidebar">
                <property name="halign">start</property>
                <property name="height-request">900</property>
                <property name="stack">pages</property>
                <property name="valign">start</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkFrame">
            <child>
              <object class="GtkStack" id="pages">
                <property name="name">Timezone</property>
                <child>
                  <object class="GtkStackPage" id="GeneralSetup">
                    <property name="child">
                      <object class="GtkFlowBox">
                        <property name="margin-bottom">20</property>
                        <property name="margin-end">50</property>
                        <property name="margin-start">50</property>
                        <property name="margin-top">20</property>
                        <property name="name">Timezone</property>
                        <child>
                          <object class="GtkEntry" id="GeneralSetup.Timezone">
                            <property name="activates-default">True</property>
                            <property name="halign">start</property>
                            <property name="height-request">10</property>
                            <property name="input-purpose">alpha</property>
                            <property name="placeholder-text">Timezone</property>
                            <property name="text">America/Phoenix</property>
                            <property name="tooltip-text">Enter your timezone</property>
                            <property name="valign">center</property>
                            <property name="width-request">50</property>
                          </object>
                        </child>
                      </object>
                    </property>
                    <property name="name">General Setup</property>
                    <property name="title">General Setup</property>
                  </object>
                </child>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>

UPDATE 2

I found a partial answer. You can do it but you need to add the App to the ApplicationWindow, not the other way around so it's a different function. Here's the way it works (It's also a little shorter as I learned there's a function to load from a file):

func activate(app *gtk.Application, log *loggers.Logger, guiPath string, appName string) {

    const ext = `.ui`

    guiPath += `/`
    builder := gtk.NewBuilderFromFile(guiPath + appName + ext)
    // builder := gtk.NewBuilderFromFile(guiPath + `Another template File.ui`)

    appWindow := builder.GetObject(`appWin`).Cast().(*gtk.ApplicationWindow)

    entry := builder.GetObject(`GeneralSetup.Timezone`).Cast().(*gtk.Entry)
    entry.Connect("changed", func() {
        println(`Changed`)
    })

    // ***** THE LINE THAT MAKES THE DIFFERENCE ***
    appWindow.SetApplication(app)
    appWindow.Show()

}

I'm still not sure if there's ever a case where you would export the application object into one of these .UI files.

I haven't found an example of anyone doing so and I'm inclined to believe that's not what you are supposed to do but I'm still learning so could easily be wrong.


Solution

  • Update 2 has most of the answer to this question.

    Additionally, so far I don't think there is any reason to add an application into the UI file. It's tends to throw errors anyway.