func TestWebView_URI(t *testing.T) { setup() defer teardown() mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {}) wantURI := server.URL + "/" var gotURI string webView.Connect("notify::uri", func() { glib.IdleAdd(func() bool { gotURI = webView.URI() if gotURI != "" { gtk.MainQuit() } return false }) }) glib.IdleAdd(func() bool { webView.LoadURI(server.URL) return false }) gtk.Main() if wantURI != gotURI { t.Errorf("want URI %q, got %q", wantURI, gotURI) } }
// EvaluateJavaScript runs the JavaScript in script in the view's context and // returns the script's result as a Go value. func (v *View) EvaluateJavaScript(script string) (result interface{}, err error) { resultChan := make(chan interface{}, 1) errChan := make(chan error, 1) glib.IdleAdd(func() bool { v.WebView.RunJavaScript(script, func(result *gojs.Value, err error) { glib.IdleAdd(func() bool { if err == nil { goval, err := result.GoValue() if err != nil { errChan <- err return false } resultChan <- goval } else { errChan <- err } return false }) }) return false }) select { case result = <-resultChan: return result, nil case err = <-errChan: return nil, err } }
func TestWebView_Title(t *testing.T) { webView := NewWebView() defer webView.Destroy() wantTitle := "foo" var gotTitle string webView.Connect("notify::title", func() { glib.IdleAdd(func() bool { gotTitle = webView.Title() if gotTitle != "" { gtk.MainQuit() } return false }) }) glib.IdleAdd(func() bool { webView.LoadHTML("<html><head><title>"+wantTitle+"</title></head><body></body></html>", "") return false }) gtk.Main() if wantTitle != gotTitle { t.Errorf("want title %q, got %q", wantTitle, gotTitle) } }
// txSenderAndReplyListener triggers btcgui to send btcwallet a JSON // request to create and send a transaction. If sending the transaction // succeeds, the recipients in the send coins notebook tab are cleared. // If the transaction fails because the wallet is not unlocked, the // unlock dialog is shown, and after a successful unlock, creating and // sending the tx is tried a second time. // // This is written to be run as a goroutine executing outside of the GTK // main event loop. func txSenderAndReplyListener(sendTo map[string]float64) { triggers.sendTx <- sendTo err := <-triggerReplies.sendTx // -13 is the error code for needing an unlocked wallet. if jsonErr, ok := err.(*btcjson.Error); ok { switch jsonErr.Code { case -13: // Wallet must be unlocked first. Show unlock dialog. glib.IdleAdd(func() { unlockSuccessful := make(chan bool) go func() { for { success, ok := <-unlockSuccessful if !ok { // A closed channel indicates // the dialog was cancelled. // Abort sending the transaction. return } if success { // Try send again. go txSenderAndReplyListener(sendTo) return } } }() d, err := createUnlockDialog(unlockForTxSend, unlockSuccessful) if err != nil { // TODO(jrick): log error to file log.Printf("[ERR] could not create unlock dialog: %v\n", err) return } d.Run() d.Destroy() }) default: // Generic case to display an error. glib.IdleAdd(func() { d := errorDialog("Unable to send transaction", fmt.Sprintf("%s\nError code: %d", jsonErr.Message, jsonErr.Code)) d.Run() d.Destroy() }) } return } // Send was successful, so clear recipient widgets. glib.IdleAdd(resetRecipients) }
func main() { gtk.Init(nil) win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) if err != nil { log.Fatal("Unable to create window:", err) } win.Connect("destroy", func() { gtk.MainQuit() }) win.Add(windowWidget()) // Native GTK is not thread safe, and thus, gotk3's GTK bindings may not // be used from other goroutines. Instead, glib.IdleAdd() must be used // to add a function to run in the GTK main loop when it is in an idle // state. // // Two examples of using glib.IdleAdd() are shown below. The first runs // a user created function, LabelSetTextIdle, and passes it two // arguments for a label and the text to set it with. The second calls // (*gtk.Label).SetText directly, passing in only the text as an // argument. // // If the function passed to glib.IdleAdd() returns one argument, and // that argument is a bool, this return value will be used in the same // manner as a native g_idle_add() call. If this return value is false, // the function will be removed from executing in the GTK main loop's // idle state. If the return value is true, the function will continue // to execute when the GTK main loop is in this state. go func() { for { time.Sleep(time.Second) s := fmt.Sprintf("Set a label %d time(s)!", nSets) _, err := glib.IdleAdd(LabelSetTextIdle, topLabel, s) if err != nil { log.Fatal("IdleAdd() failed:", err) } nSets++ s = fmt.Sprintf("Set a label %d time(s)!", nSets) _, err = glib.IdleAdd(bottomLabel.SetText, s) if err != nil { log.Fatal("IdleAdd() failed:", err) } nSets++ } }() win.ShowAll() gtk.Main() }
// updateAddresses listens for new wallet addresses, updating the GUI when // necessary. func updateAddresses() { for { addrs := <-updateChans.addrs glib.IdleAdd(func() { RecvCoins.Store.Clear() }) for i := range addrs { addr := addrs[i] glib.IdleAdd(func() { iter := RecvCoins.Store.Append() RecvCoins.Store.Set(iter, []int{1}, []interface{}{addr}) }) } } }
// cmdGetAddressesByAccount requests all addresses for an account. // // TODO(jrick): support non-default accounts. // TODO(jrick): stop throwing away errors. func cmdGetAddressesByAccount(ws *websocket.Conn) { n := <-NewJSONID msg, err := btcjson.CreateMessageWithId("getaddressesbyaccount", n, "") if err != nil { updateChans.addrs <- []string{} } replyHandlers.Lock() replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) { if r, ok := result.([]interface{}); ok { addrs := []string{} for _, v := range r { addrs = append(addrs, v.(string)) } updateChans.addrs <- addrs } else { if err.Code == btcjson.ErrWalletInvalidAccountName.Code { glib.IdleAdd(func() { if dialog, err := createNewWalletDialog(); err != nil { dialog.Run() } }) } updateChans.addrs <- []string{} } } replyHandlers.Unlock() if err = websocket.Message.Send(ws, msg); err != nil { replyHandlers.Lock() delete(replyHandlers.m, n) replyHandlers.Unlock() updateChans.addrs <- []string{} } }
func TestWebView_LoadURI_load_failed(t *testing.T) { webView := NewWebView() defer webView.Destroy() loadFailed := false loadFinished := false webView.Connect("load-failed", func() { loadFailed = true }) webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: loadFinished = true gtk.MainQuit() } }) glib.IdleAdd(func() bool { // Load a bad URL to trigger load failure. webView.LoadURI("http://127.0.0.1:99999") return false }) gtk.Main() if !loadFailed { t.Error("!loadFailed") } if !loadFinished { t.Error("!loadFinished") } }
func TestWebView_GetSnapshot(t *testing.T) { webView := NewWebView() defer webView.Destroy() webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: webView.GetSnapshot(func(img *image.RGBA, err error) { if err != nil { t.Errorf("GetSnapshot error: %q", err) } if img.Pix == nil { t.Error("!img.Pix") } if img.Stride == 0 || img.Rect.Max.X == 0 || img.Rect.Max.Y == 0 { t.Error("!img.Stride or !img.Rect.Max.X or !img.Rect.Max.Y") } gtk.MainQuit() }) } }) glib.IdleAdd(func() bool { webView.LoadHTML(`<p id=foo>abc</p>`, "") return false }) gtk.Main() }
func TestWebView_RunJavaScript_exception(t *testing.T) { webView := NewWebView() defer webView.Destroy() wantErr := errors.New("An exception was raised in JavaScript") webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: webView.RunJavaScript(`throw new Error("foo")`, func(result *gojs.Value, err error) { if result != nil { ctx := webView.JavaScriptGlobalContext() t.Errorf("want result == nil, got %q", ctx.ToStringOrDie(result)) } if !reflect.DeepEqual(wantErr, err) { t.Errorf("want error %q, got %q", wantErr, err) } gtk.MainQuit() }) } }) glib.IdleAdd(func() bool { webView.LoadHTML(`<p></p>`, "") return false }) gtk.Main() }
func TestWebView_RunJavaScript(t *testing.T) { webView := NewWebView() defer webView.Destroy() wantResultString := "abc" webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: webView.RunJavaScript(`document.getElementById("foo").innerHTML`, func(result *gojs.Value, err error) { if err != nil { t.Errorf("RunJavaScript error: %s", err) } resultString := webView.JavaScriptGlobalContext().ToStringOrDie(result) if wantResultString != resultString { t.Errorf("want result string %q, got %q", wantResultString, resultString) } gtk.MainQuit() }) } }) glib.IdleAdd(func() bool { webView.LoadHTML(`<p id=foo>abc</p>`, "") return false }) gtk.Main() }
func (self *PopupMenu) downloadFile(msgId, url, ext string) { go func() { glib.IdleAdd(func() { NewFileChooserWindow(self.parent, msgId, url, ext).window.ShowAll() }) }() }
func TestWebView_LoadHTML(t *testing.T) { webView := NewWebView() defer webView.Destroy() loadOk := false webView.Connect("load-failed", func() { t.Errorf("load failed") }) webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: loadOk = true gtk.MainQuit() } }) glib.IdleAdd(func() bool { webView.LoadHTML("<p>hello</p>", "") return false }) gtk.Main() if !loadOk { t.Error("!loadOk") } }
// NewView creates a new View in the context. func (c *Context) NewView() *View { view := make(chan *View, 1) glib.IdleAdd(func() bool { webView := webkit2.NewWebView() settings := webView.Settings() settings.SetEnableWriteConsoleMessagesToStdout(true) settings.SetUserAgentWithApplicationDetails("WebLoop", "v1") v := &View{WebView: webView} loadChangedHandler, _ := webView.Connect("load-changed", func(_ *glib.Object, loadEvent webkit2.LoadEvent) { switch loadEvent { case webkit2.LoadFinished: // If we're here, then the load must not have failed, because // otherwise we would've disconnected this handler in the // load-failed signal handler. v.load <- struct{}{} } }) webView.Connect("load-failed", func() { v.lastLoadErr = ErrLoadFailed webView.HandlerDisconnect(loadChangedHandler) }) view <- v return false }) return <-view }
// updateConnectionState listens for connection status changes to btcd // and btcwallet, updating the GUI when necessary. func updateConnectionState() { // Statusbar messages for various connection states. btcdd := "Disconnected from btcd" btcwc := "Established connection to btcwallet" btcwd := "Disconnected from btcwallet. Attempting reconnect..." for { select { case conn := <-updateChans.btcwalletConnected: if conn { glib.IdleAdd(func() { //MenuBar.Settings.New.SetSensitive(true) //MenuBar.Settings.Encrypt.SetSensitive(true) MenuBar.Settings.TxFee.SetSensitive(true) // Lock/Unlock sensitivity is set by wallet notification. RecvCoins.NewAddrBtn.SetSensitive(true) StatusElems.Lab.SetText(btcwc) StatusElems.Pb.Hide() }) } else { glib.IdleAdd(func() { //MenuBar.Settings.New.SetSensitive(false) //MenuBar.Settings.Encrypt.SetSensitive(false) MenuBar.Settings.Lock.SetSensitive(false) MenuBar.Settings.Unlock.SetSensitive(false) MenuBar.Settings.TxFee.SetSensitive(false) SendCoins.SendBtn.SetSensitive(false) RecvCoins.NewAddrBtn.SetSensitive(false) StatusElems.Lab.SetText(btcwd) StatusElems.Pb.Hide() }) } case conn := <-updateChans.btcdConnected: if conn { glib.IdleAdd(func() { SendCoins.SendBtn.SetSensitive(true) }) } else { glib.IdleAdd(func() { SendCoins.SendBtn.SetSensitive(false) StatusElems.Lab.SetText(btcdd) StatusElems.Pb.Hide() }) } } } }
// URI returns the URI of the current resource in the view. func (v *View) URI() string { uri := make(chan string, 1) glib.IdleAdd(func() bool { uri <- v.WebView.URI() return false }) return <-uri }
// Title returns the title of the current resource in the view. func (v *View) Title() string { title := make(chan string, 1) glib.IdleAdd(func() bool { title <- v.WebView.Title() return false }) return <-title }
// StartMainApplication creates and opens the main window appWindow. // It then preceeds to start all necessary goroutine to support the main // application. Currently, this starts generating the JSON ID generator // and attempts to open a connection to btcwallet. // // This is written to be called as a goroutine outside of the main GTK // loop. func StartMainApplication() { // Read CA file to verify a btcwallet TLS connection. cafile, err := ioutil.ReadFile(cfg.CAFile) if err != nil { IdlePreGUIError(fmt.Errorf("Cannot open CA file:\n%v", err)) } glib.IdleAdd(func() { w, err := CreateWindow() if err != nil { PreGUIError(fmt.Errorf("Cannot create application window:\n%v", err)) } w.ShowAll() }) // Write current application version to file. if err := version.SaveToDataDir(cfg); err != nil { log.Print(err) } // Begin generating new IDs for JSON calls. go JSONIDGenerator(NewJSONID) // Listen for updates and update GUI with new info. Attempt // reconnect if connection is lost or cannot be established. for { replies := make(chan error) done := make(chan int) go func() { ListenAndUpdate(cafile, replies) close(done) }() selectLoop: for { select { case <-done: break selectLoop case err := <-replies: switch err { case ErrConnectionRefused: updateChans.btcwalletConnected <- false time.Sleep(5 * time.Second) case ErrConnectionLost: updateChans.btcwalletConnected <- false time.Sleep(5 * time.Second) case nil: // connected updateChans.btcwalletConnected <- true log.Print("Established connection to btcwallet.") default: // TODO(jrick): present unknown error to user in the // GUI somehow. log.Printf("Unknown connect error: %v", err) } } } } }
// Open starts loading the resource at the specified URL. func (v *View) Open(url string) { v.load = make(chan struct{}, 1) v.lastLoadErr = nil glib.IdleAdd(func() bool { if !v.destroyed { v.WebView.LoadURI(url) } return false }) }
// IdlePreGUIError runs PreGUIError within the context of the GTK main // event loop. This function does not return. func IdlePreGUIError(e error) { glib.IdleAdd(func() { PreGUIError(e) }) // This function should block. However, simple adding a closure the // main event loop does not block. Use an empty select to prevent the // calling goroutine from continuing. select {} }
func (self *VerificationWindow) verifyAndLogin() { errChan := make(chan error) authTokenChan := make(chan string) go func() { authToken, err := goline.client.GetAuthTokenAfterVerify() errChan <- err authTokenChan <- authToken }() go func() { err := <-errChan if err != nil { glib.IdleAdd(func() { goline.LoggerPanicln("Failed to get authorization token:", err) }) } authToken := <-authTokenChan err = goline.client.AuthTokenLogin(authToken) if err != nil { glib.IdleAdd(func() { goline.LoggerPanicln("Failed to login with authorization token:", err) }) } glib.IdleAdd(func() { mainWindow := NewMainWindow() mainWindow.window.ShowAll() self.window.Hide() if goline.Autologin { goline.AuthToken = authToken } else { goline.AuthToken, err = encryptAuthToken(self.parent.GetText(self.parent.pwd), authToken) if err != nil { goline.LoggerPrintln(err) glib.IdleAdd(func() { RunAlertMessage(mainWindow.window, "Failed to encrypt authorization token.") }) } } err = goline.SaveSettings() if err != nil { goline.LoggerPrintln(err) glib.IdleAdd(func() { RunAlertMessage(mainWindow.window, "Failed to save authorization token.") }) } }) }() }
// updateLockState updates the application widgets due to a change in // the currently-open wallet's lock state. func updateLockState() { for { locked, ok := <-updateChans.lockState if !ok { return } if locked { glib.IdleAdd(func() { MenuBar.Settings.Lock.SetSensitive(false) MenuBar.Settings.Unlock.SetSensitive(true) }) } else { glib.IdleAdd(func() { MenuBar.Settings.Lock.SetSensitive(true) MenuBar.Settings.Unlock.SetSensitive(false) }) } } }
func (self *PopupMenu) viewFile(msgId, url, ext string) { go func() { downloadingWindow := NewDownloadingWindow(msgId) glib.IdleAdd(downloadingWindow.window.ShowAll) defer glib.IdleAdd(downloadingWindow.window.Destroy) content, err := downloadContentToTemp(msgId, url, ext) if err != nil { goline.LoggerPrintln(err) glib.IdleAdd(func() { RunAlertMessage(self.parent.window, "Failed to download file: %s", err) }) return } cmd := exec.Command("xdg-open", content) err = cmd.Start() if err != nil { goline.LoggerPrintln(err) glib.IdleAdd(func() { RunAlertMessage(self.parent.window, "Failed to open file.") }) return } }() }
// cmdGetNewAddress requests a new wallet address. // // TODO(jrick): support non-default accounts func cmdGetNewAddress(ws *websocket.Conn) { var err error defer func() { if err != nil { } }() n := <-NewJSONID msg, err := btcjson.CreateMessageWithId("getnewaddress", n, "") if err != nil { triggerReplies.newAddr <- err return } replyHandlers.Lock() replyHandlers.m[n] = func(result interface{}, err *btcjson.Error) { switch { case err == nil: if addr, ok := result.(string); ok { triggerReplies.newAddr <- addr } case err.Code == btcjson.ErrWalletKeypoolRanOut.Code: success := make(chan bool) glib.IdleAdd(func() { dialog, err := createUnlockDialog(unlockForKeypool, success) if err != nil { log.Print(err) success <- false return } dialog.Run() }) if <-success { triggers.newAddr <- 1 } default: // all other non-nil errors triggerReplies.newAddr <- errors.New(err.Message) } } replyHandlers.Unlock() if err = websocket.Message.Send(ws, msg); err != nil { replyHandlers.Lock() delete(replyHandlers.m, n) replyHandlers.Unlock() triggerReplies.newAddr <- err } }
func (self *AutologinWindow) Login() { errChan := make(chan error) go func() { err := goline.client.AuthTokenLogin(goline.AuthToken) errChan <- err }() go func() { err := <-errChan if err != nil { goline.LoggerPrintln(err) glib.IdleAdd(func() { loginWindow := NewLoginWindow() loginWindow.window.ShowAll() self.window.Hide() RunAlertMessage(loginWindow.window, "Failed to login with authorization token.") }) } else { glib.IdleAdd(func() { NewMainWindow().window.ShowAll() self.window.Hide() }) } }() }
// XXX spilt this? func updateProgress() { for { bcHeight, ok := <-updateChans.bcHeight if !ok { return } // TODO(jrick) this can go back when remote height is updated. /* bcHeightRemote, ok := <-updateChans.bcHeightRemote if !ok { return } if bcHeight >= 0 && bcHeightRemote >= 0 { percentDone := float64(bcHeight) / float64(bcHeightRemote) if percentDone < 1 { s := fmt.Sprintf("%d of ~%d blocks", bcHeight, bcHeightRemote) glib.IdleAdd(StatusElems.Lab.SetText, "Updating blockchain...") glib.IdleAdd(StatusElems.Pb.SetText, s) glib.IdleAdd(StatusElems.Pb.SetFraction, percentDone) glib.IdleAdd(StatusElems.Pb.Show) } else { s := fmt.Sprintf("%d blocks", bcHeight) glib.IdleAdd(StatusElems.Lab.SetText, s) glib.IdleAdd(StatusElems.Pb.Hide) } } else if bcHeight >= 0 && bcHeightRemote == -1 { s := fmt.Sprintf("%d blocks", bcHeight) glib.IdleAdd(StatusElems.Lab.SetText, s) glib.IdleAdd(StatusElems.Pb.Hide) } else { glib.IdleAdd(StatusElems.Lab.SetText, "Error getting blockchain height") glib.IdleAdd(StatusElems.Pb.Hide) } */ s := fmt.Sprintf("%d blocks", bcHeight) glib.IdleAdd(func() { StatusElems.Lab.SetText(s) StatusElems.Pb.Hide() }) } }
// updateBalance listens for new wallet account balances, updating the GUI // when necessary. func updateBalance() { for { balance, ok := <-updateChans.balance if !ok { return } var s string if math.IsNaN(balance) { s = "unknown" } else { s = strconv.FormatFloat(balance, 'f', 8, 64) + " BTC" } glib.IdleAdd(func() { Overview.Balance.SetMarkup("<b>" + s + "</b>") SendCoins.Balance.SetText("Balance: " + s) }) } }
// updateBalance listens for new wallet account unconfirmed balances, updating // the GUI when necessary. func updateUnconfirmed() { for { unconfirmed, ok := <-updateChans.unconfirmed if !ok { return } var s string if math.IsNaN(unconfirmed) { s = "unknown" } else { balStr := strconv.FormatFloat(unconfirmed, 'f', 8, 64) + " BTC" s = "<b>" + balStr + "</b>" } glib.IdleAdd(func() { Overview.Unconfirmed.SetMarkup(s) }) } }
func TestWebView_LoadURI(t *testing.T) { setup() defer teardown() responseOk := false mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("abc")) responseOk = true }) loadFinished := false webView.Connect("load-failed", func() { t.Errorf("load failed") }) webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) { switch loadEvent { case LoadFinished: loadFinished = true gtk.MainQuit() } }) glib.IdleAdd(func() bool { webView.LoadURI(server.URL) return false }) gtk.Main() if !responseOk { t.Error("!responseOk") } if !loadFinished { t.Error("!loadFinished") } }
func createRecvCoins() *gtk.Widget { store, err := gtk.ListStoreNew(glib.TYPE_STRING, glib.TYPE_STRING) if err != nil { log.Fatal(err) } RecvCoins.Store = store tv, err := gtk.TreeViewNewWithModel(store) if err != nil { log.Fatal(err) } RecvCoins.Treeview = tv renderer, err := gtk.CellRendererTextNew() if err != nil { log.Fatal(err) } renderer.Set("editable", true) renderer.Set("editable-set", true) renderer.Connect("edited", func(_ *glib.Object, path, text string) { iter, err := store.GetIterFromString(path) if err == nil { store.Set(iter, []int{0}, []interface{}{text}) } }) col, err := gtk.TreeViewColumnNewWithAttribute("Label", renderer, "text", 0) if err != nil { log.Fatal(err) } col.SetExpand(true) tv.AppendColumn(col) cr, err := gtk.CellRendererTextNew() if err != nil { log.Fatal(err) } col, err = gtk.TreeViewColumnNewWithAttribute("Address", cr, "text", 1) if err != nil { log.Fatal(err) } col.SetMinWidth(350) tv.AppendColumn(col) newAddr, err := gtk.ButtonNewWithLabel("New Address") if err != nil { log.Fatal(err) } newAddr.SetSizeRequest(150, -1) newAddr.Connect("clicked", func() { go func() { triggers.newAddr <- 1 reply := <-triggerReplies.newAddr if err, ok := reply.(error); ok { glib.IdleAdd(func() { mDialog := errorDialog("New address generation failed", err.Error()) mDialog.Run() mDialog.Destroy() }) } else if addr, ok := reply.(string); ok { glib.IdleAdd(func() { iter := RecvCoins.Store.Append() RecvCoins.Store.Set(iter, []int{0, 1}, []interface{}{"", addr}) }) } }() }) newAddr.SetSensitive(false) RecvCoins.NewAddrBtn = newAddr buttons, err := gtk.GridNew() if err != nil { log.Fatal(err) } buttons.Add(newAddr) cpyAddr, err := gtk.ButtonNewWithLabel("Copy Address") if err != nil { log.Fatal(err) } cpyAddr.SetSizeRequest(150, -1) cpyAddr.Connect("clicked", func() { sel, err := tv.GetSelection() if err != nil { log.Fatal(err) } var iter gtk.TreeIter if sel.GetSelected(nil, &iter) { val, err := store.GetValue(&iter, 1) if err != nil { log.Fatal(err) } display, err := gdk.DisplayGetDefault() if err != nil { log.Fatal(err) } clipboard, err := gtk.ClipboardGetForDisplay( display, gdk.SELECTION_CLIPBOARD) if err != nil { log.Fatal(err) } primary, err := gtk.ClipboardGetForDisplay( display, gdk.SELECTION_PRIMARY) if err != nil { log.Fatal(err) } s, _ := val.GetString() clipboard.SetText(s) primary.SetText(s) } }) buttons.Add(cpyAddr) sw, err := gtk.ScrolledWindowNew(nil, nil) if err != nil { log.Fatal(err) } sw.Add(tv) sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.SetHExpand(true) sw.SetVExpand(true) grid, err := gtk.GridNew() if err != nil { log.Fatal(err) } grid.SetOrientation(gtk.ORIENTATION_VERTICAL) grid.Add(sw) grid.Add(buttons) return &grid.Container.Widget }