update cchat-gtk to latest gotk3 to fix leaks
This commit is contained in:
parent
e159b0d611
commit
50376cb2b0
17
go.mod
17
go.mod
|
@ -2,22 +2,21 @@ module github.com/diamondburned/cchat-gtk
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4
|
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385
|
||||||
|
|
||||||
//replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
// replace github.com/diamondburned/gotk3-tcmalloc => ../../gotk3-tcmalloc
|
||||||
|
// replace github.com/diamondburned/cchat-discord => ../cchat-discord
|
||||||
//replace github.com/diamondburned/ningen/v2 => ../../ningen
|
// replace github.com/diamondburned/ningen/v2 => ../../ningen
|
||||||
|
// replace github.com/diamondburned/arikawa/v2 => ../../arikawa
|
||||||
//replace github.com/diamondburned/arikawa/v2 => ../../arikawa
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Xuanwo/go-locale v1.0.0
|
github.com/Xuanwo/go-locale v1.0.0
|
||||||
github.com/alecthomas/chroma v0.7.3
|
github.com/alecthomas/chroma v0.7.3
|
||||||
github.com/diamondburned/cchat v0.3.15
|
github.com/diamondburned/cchat v0.3.15
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af
|
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828
|
||||||
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
|
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/goodsign/monday v1.0.0
|
github.com/goodsign/monday v1.0.0
|
||||||
|
|
26
go.sum
26
go.sum
|
@ -52,6 +52,8 @@ github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd h1:HCaw0Y
|
||||||
github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd/go.mod h1:/vapSS3yfYRAt5hhgI6JiPkca+wKhgi0MdanT1dBBQY=
|
github.com/diamondburned/arikawa/v2 v2.0.0-20201219075756-36c2f166becd/go.mod h1:/vapSS3yfYRAt5hhgI6JiPkca+wKhgi0MdanT1dBBQY=
|
||||||
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377 h1:71BLnECSl0/Ns7iZmEm7MpE5+qSuWw/BQBQY2XCUmVc=
|
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377 h1:71BLnECSl0/Ns7iZmEm7MpE5+qSuWw/BQBQY2XCUmVc=
|
||||||
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
|
github.com/diamondburned/arikawa/v2 v2.0.0-20201220032235-088b30430377/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
|
||||||
|
github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44 h1:i6Jec7bvVY8NhwW3L0SlpfWM6r2p2i67XuhiOEzkfwI=
|
||||||
|
github.com/diamondburned/arikawa/v2 v2.0.0-20201227001310-f3f075b27f44/go.mod h1:e+lhS20ni2luFEU06Pc8paCxgZL99/RZb77dOC82CF0=
|
||||||
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
|
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
|
||||||
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||||
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
|
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
|
||||||
|
@ -60,6 +62,10 @@ github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d h1:n61
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220054426-918719599f2d/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af h1:pTdxsrVSYCdraGormbu1t8uQJMe/OD/ZIz9KljDWAvc=
|
||||||
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
github.com/diamondburned/cchat-discord v0.0.0-20201220081640-288591a535af/go.mod h1:pvp1TOHK7NUM+GDRPixQGsKyCSbGYhiseK2jM+1I+ms=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8 h1:eyK9GRaHg1KcWZx4hBPHWG16+EwgZi5groEW5I/FRq0=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201227023505-c4e360010fb8/go.mod h1:i3y8dyAFrtigpGOwunBdoJK/phwt9Gp/wfpVJb4imV0=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092 h1:oxY7APUclLgaWjaTK++7kHBdl0GdVyqOvHQv68TcpHw=
|
||||||
|
github.com/diamondburned/cchat-discord v0.0.0-20201227035212-6beff5225092/go.mod h1:rFBGZYLq0g6Pb/WGN/K0++kXrhCYlQQ1nc2FX4r8CO0=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db h1:VQI2PdbsdsRJ7d669kp35GbCUO44KZ0Xfqdu4o/oqVg=
|
||||||
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
github.com/diamondburned/cchat-mock v0.0.0-20201115033644-df8d1b10f9db/go.mod h1:M87kjNzWVPlkZycFNzpGPKQXzkHNnZphuwMf3E9ckgc=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
github.com/diamondburned/gotk3 v0.0.0-20201209182406-e7291341a091 h1:lQpSWzbi3rQf66aMSip/rIypasIFwqCqF0Wfn5og6gw=
|
||||||
|
@ -70,16 +76,36 @@ github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f h1:Lnrq+vXBgzb
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20201221091325-c5152a10909f/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4 h1:KvlmpqxLoXKg+j5uiJWZXhacfgPg4fi/8wLecWX+XlE=
|
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4 h1:KvlmpqxLoXKg+j5uiJWZXhacfgPg4fi/8wLecWX+XlE=
|
||||||
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
github.com/diamondburned/gotk3 v0.0.0-20201225074909-7bf1378bcba4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da h1:ovty7leKv+E6PqocAeK+toLYnaMqbhZX69oPFNQK5Fk=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201225090124-444dc90054da/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2 h1:ExJxwhfSnIRJxL+CdcRZM33xoJ8WycoXGw6z3LaBFVM=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201226041445-1e6de5f7c2b2/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03 h1:/azfkq4zFt8JPo8p2c/7kJJcf4cgTH1OaigR3fZHvIA=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201226085844-7ac29c072f03/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20 h1:/jSna2cSHrNXSzR9rNp9kUQRd/VOC/FL7vhRBor4zOc=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229054305-848200601f20/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59 h1:9nYQE9MZDcu++bFyyxKUQdin4ML+0PRoRm0bIUhjuBs=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229092333-f5c9db5d1d59/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385 h1:nmlMCeEWmT6z9GAslxkTBZd9mH0Yt2QtWFD1yxGCJ98=
|
||||||
|
github.com/diamondburned/gotk3 v0.0.0-20201229104206-9bea3709a385/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g=
|
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894 h1:QgI21deaQbCUMnxKkQQUXzQolnAe1dMIXAWwqAyOp2g=
|
||||||
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA=
|
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894/go.mod h1:IoyMxPKSJOMoP0BiBuFwf2RDMeA4Uqx0HPKN5BzqTtA=
|
||||||
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828 h1:Lm1F+GwrDdAaaMzrR7AYl4GGd/T+FE2OgOz25QWwsIg=
|
||||||
|
github.com/diamondburned/gspell v0.0.0-20201229064336-e43698fd5828/go.mod h1:ODW0Ai5dTVVp/HNUSSDTl5qE6K642CoOSZ8isIhupNg=
|
||||||
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 h1:qF5VHC35+GyCjUmKz+1O94xpFc0JQd4Ui3h+I955pJw=
|
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4 h1:qF5VHC35+GyCjUmKz+1O94xpFc0JQd4Ui3h+I955pJw=
|
||||||
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ=
|
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ=
|
||||||
|
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374 h1:1KLPz5mbYF7t3ajrK55alkDcbjWc+7aQFjKzV9qJRN4=
|
||||||
|
github.com/diamondburned/handy v0.0.0-20201229063418-ec23c1370374/go.mod h1:9EiMOAKWhEFoVnFLTco2v0v0rJn855YAHDDa2bL8urU=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo=
|
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972 h1:OWxllHbUptXzDias6YI4MM0R3o50q8MfhkkwVIlfiNo=
|
||||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ=
|
||||||
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y=
|
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a h1:w8CWPYiwH9p2XGlHHeTqRWx7e8CJJLN8i4orAkOa27Y=
|
||||||
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a/go.mod h1:Pw4ZPQmZUonCytlKhHgan98CZeCQ4AWh0DWqvnhsuNE=
|
github.com/diamondburned/ningen/v2 v2.0.0-20201219070301-15610044db9a/go.mod h1:Pw4ZPQmZUonCytlKhHgan98CZeCQ4AWh0DWqvnhsuNE=
|
||||||
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4 h1:qzh5ghfgvUllilOhkrGP29IGQT6DGfcc3lhk9uSA6nU=
|
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4 h1:qzh5ghfgvUllilOhkrGP29IGQT6DGfcc3lhk9uSA6nU=
|
||||||
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4/go.mod h1:2ZjyeHqO9jCdlfmJhhVhk8eCumx418n39uVaC/LgEgY=
|
github.com/diamondburned/ningen/v2 v2.0.0-20201220054153-c69c4f7057b4/go.mod h1:2ZjyeHqO9jCdlfmJhhVhk8eCumx418n39uVaC/LgEgY=
|
||||||
|
github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c h1:IWn/N54JkJz7PVgmAt7cWXLky9wY0UjXzznNoWLIFgA=
|
||||||
|
github.com/diamondburned/ningen/v2 v2.0.0-20201227020621-a4e33db11d3c/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo=
|
||||||
|
github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4 h1:ptIpcyB/FIBM5viKLVXuBiE1utqBcH4S3l5kUQrsL9Q=
|
||||||
|
github.com/diamondburned/ningen/v2 v2.0.0-20201227034843-dc1d22fc28e4/go.mod h1:zQkAo1RT4ru4HW6B5T4IRO2pee8ITzTUA2Y7XNpgjqo=
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
|
||||||
|
|
|
@ -3,28 +3,43 @@ package icons
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/cairo"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// static assets
|
// static assets
|
||||||
var assets = map[string]*gdk.Pixbuf{}
|
// var assets = map[string]*gdk.Pixbuf{}
|
||||||
|
|
||||||
func Logo256Variant2(sz int) *gdk.Pixbuf {
|
func Logo256Variant2(sz, scale int) *cairo.Surface {
|
||||||
return loadPixbuf(__cchat_variant2_256, sz)
|
return mustSurface(loadPixbuf(__cchat_variant2_256, sz, scale), scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Logo256(sz int) *gdk.Pixbuf {
|
func Logo256(sz, scale int) *cairo.Surface {
|
||||||
return loadPixbuf(__cchat_256, sz)
|
return mustSurface(loadPixbuf(__cchat_256, sz, scale), scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPixbuf(data []byte, sz int) *gdk.Pixbuf {
|
func Logo256Pixbuf() *gdk.Pixbuf {
|
||||||
|
return loadPixbuf(__cchat_256, 256, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSurface(p *gdk.Pixbuf, scale int) *cairo.Surface {
|
||||||
|
surface, err := gdk.CairoSurfaceCreateFromPixbuf(p, scale, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to create surface from pixbuf:", err)
|
||||||
|
}
|
||||||
|
return surface
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPixbuf(data []byte, sz, scale int) *gdk.Pixbuf {
|
||||||
l, err := gdk.PixbufLoaderNew()
|
l, err := gdk.PixbufLoaderNew()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to create a pixbuf loader for icons:", err)
|
log.Fatalln("Failed to create a pixbuf loader for icons:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sz > 0 {
|
if sz > 0 {
|
||||||
l.Connect("size-prepared", func() { l.SetSize(sz, sz) })
|
l.Connect("size-prepared", func(l *gdk.PixbufLoader) {
|
||||||
|
l.SetSize(sz*scale, sz*scale)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := l.WriteAndReturnPixbuf(data)
|
p, err := l.WriteAndReturnPixbuf(data)
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
package gts
|
package gts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/throttler"
|
"github.com/diamondburned/cchat-gtk/internal/gts/throttler"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/handy"
|
"github.com/diamondburned/handy"
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/glib"
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
@ -66,15 +63,14 @@ func NewEmptyModalDialog() (*gtk.Dialog, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to get content area")
|
return nil, errors.Wrap(err, "Failed to get content area")
|
||||||
}
|
}
|
||||||
|
b.Destroy()
|
||||||
d.Remove(b)
|
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddAppAction(name string, call func()) {
|
func AddAppAction(name string, call func()) {
|
||||||
action := glib.SimpleActionNew(name, nil)
|
action := glib.SimpleActionNew(name, nil)
|
||||||
action.Connect("activate", call)
|
action.Connect("activate", func(*glib.SimpleAction) { call() })
|
||||||
App.AddAction(action)
|
App.AddAction(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,12 +86,12 @@ func init() {
|
||||||
type MainApplication interface {
|
type MainApplication interface {
|
||||||
gtk.IWidget
|
gtk.IWidget
|
||||||
Menu() *glib.MenuModel
|
Menu() *glib.MenuModel
|
||||||
Icon() *gdk.Pixbuf
|
Icon() *gdk.Pixbuf // assume scale 1
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Main(wfn func() MainApplication) {
|
func Main(wfn func() MainApplication) {
|
||||||
App.Application.Connect("activate", func() {
|
App.Application.Connect("activate", func(*gtk.Application) {
|
||||||
handy.Init()
|
handy.Init()
|
||||||
|
|
||||||
// Load all CSS onto the default screen.
|
// Load all CSS onto the default screen.
|
||||||
|
@ -119,17 +115,16 @@ func Main(wfn func() MainApplication) {
|
||||||
w := wfn()
|
w := wfn()
|
||||||
App.Window.Add(w)
|
App.Window.Add(w)
|
||||||
App.Window.SetIcon(w.Icon())
|
App.Window.SetIcon(w.Icon())
|
||||||
// App.Application.SetAppMenu(w.Menu())
|
|
||||||
|
|
||||||
// Connect the destructor.
|
// Connect the destructor.
|
||||||
App.Window.Window.Connect("destroy", func() {
|
App.Window.Window.Connect("destroy", func(window *handy.ApplicationWindow) {
|
||||||
// Hide the application window.
|
// Hide the application window.
|
||||||
App.Window.Hide()
|
window.Hide()
|
||||||
|
|
||||||
// Let the main loop run once by queueing the stop loop afterwards.
|
// Let the main loop run once by queueing the stop loop afterwards.
|
||||||
// This is to allow the main loop to properly hide the Gtk window
|
// This is to allow the main loop to properly hide the Gtk window
|
||||||
// before trying to disconnect.
|
// before trying to disconnect.
|
||||||
ExecAsync(func() {
|
ExecLater(func() {
|
||||||
// Stop the application loop.
|
// Stop the application loop.
|
||||||
App.Application.Quit()
|
App.Application.Quit()
|
||||||
// Finalize the application by running the closer.
|
// Finalize the application by running the closer.
|
||||||
|
@ -164,9 +159,14 @@ func Async(fn func() (func(), error)) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecLater executes the function asynchronously with a low priority.
|
||||||
|
func ExecLater(fn func()) {
|
||||||
|
glib.IdleAddPriority(glib.PRIORITY_LOW, fn)
|
||||||
|
}
|
||||||
|
|
||||||
// ExecAsync executes function asynchronously in the Gtk main thread.
|
// ExecAsync executes function asynchronously in the Gtk main thread.
|
||||||
func ExecAsync(fn func()) {
|
func ExecAsync(fn func()) {
|
||||||
glib.IdleAdd(fn)
|
glib.IdleAddPriority(glib.PRIORITY_HIGH, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecSync executes the function asynchronously, but returns a channel that
|
// ExecSync executes the function asynchronously, but returns a channel that
|
||||||
|
@ -174,7 +174,7 @@ func ExecAsync(fn func()) {
|
||||||
func ExecSync(fn func()) <-chan struct{} {
|
func ExecSync(fn func()) <-chan struct{} {
|
||||||
var ch = make(chan struct{})
|
var ch = make(chan struct{})
|
||||||
|
|
||||||
glib.IdleAdd(func() {
|
glib.IdleAddPriority(glib.PRIORITY_HIGH, func() {
|
||||||
fn()
|
fn()
|
||||||
close(ch)
|
close(ch)
|
||||||
})
|
})
|
||||||
|
@ -189,10 +189,7 @@ func DoAfter(d time.Duration, f func()) {
|
||||||
|
|
||||||
// DoAfterMs calls f after the given ms in the Gtk main loop.
|
// DoAfterMs calls f after the given ms in the Gtk main loop.
|
||||||
func DoAfterMs(ms uint, f func()) {
|
func DoAfterMs(ms uint, f func()) {
|
||||||
_, err := glib.TimeoutAdd(ms, f)
|
glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, f)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
// AfterFunc mimics time.AfterFunc's API but runs the callback inside the Gtk
|
||||||
|
@ -203,11 +200,7 @@ func AfterFunc(d time.Duration, f func()) (stop func()) {
|
||||||
|
|
||||||
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
|
// AfterMsFunc is similar to AfterFunc but takes in milliseconds instead.
|
||||||
func AfterMsFunc(ms uint, f func()) (stop func()) {
|
func AfterMsFunc(ms uint, f func()) (stop func()) {
|
||||||
h, err := glib.TimeoutAdd(ms, func() bool { f(); return true })
|
h := glib.TimeoutAddPriority(ms, glib.PRIORITY_HIGH_IDLE, func() bool { f(); return true })
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() { glib.SourceRemove(h) }
|
return func() { glib.SourceRemove(h) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,30 +209,6 @@ func EventIsRightClick(ev *gdk.Event) bool {
|
||||||
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
return keyev.Type() == gdk.EVENT_BUTTON_PRESS && keyev.Button() == gdk.BUTTON_SECONDARY
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPixbuf(img image.Image) *gdk.Pixbuf {
|
|
||||||
var nrgba *image.NRGBA
|
|
||||||
if n, ok := img.(*image.NRGBA); ok {
|
|
||||||
nrgba = n
|
|
||||||
} else {
|
|
||||||
nrgba = imaging.Clone(img)
|
|
||||||
}
|
|
||||||
|
|
||||||
pix, err := gdk.PixbufNewFromData(
|
|
||||||
nrgba.Pix, gdk.COLORSPACE_RGB,
|
|
||||||
true, // NRGBA has alpha.
|
|
||||||
8, // 8-bit aka 1-byte per sample.
|
|
||||||
nrgba.Rect.Dx(),
|
|
||||||
nrgba.Rect.Dy(), // We already know the image size.
|
|
||||||
nrgba.Stride,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create pixbuf from *NRGBA: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return pix
|
|
||||||
}
|
|
||||||
|
|
||||||
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
|
func SpawnUploader(dirpath string, callback func(absolutePaths []string)) {
|
||||||
dialog, _ := gtk.FileChooserNativeDialogNew(
|
dialog, _ := gtk.FileChooserNativeDialogNew(
|
||||||
"Upload File", App.Window,
|
"Upload File", App.Window,
|
||||||
|
@ -276,7 +245,7 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
|
||||||
|
|
||||||
fc.SetPreviewWidget(img)
|
fc.SetPreviewWidget(img)
|
||||||
fc.Connect("update-preview",
|
fc.Connect("update-preview",
|
||||||
func(_ interface{}, img *gtk.Image) {
|
func(fc *gtk.FileChooserNativeDialog) {
|
||||||
file := fc.GetPreviewFilename()
|
file := fc.GetPreviewFilename()
|
||||||
|
|
||||||
b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true)
|
b, err := gdk.PixbufNewFromFileAtScale(file, 256, 256, true)
|
||||||
|
@ -288,6 +257,5 @@ func BindPreviewer(fc *gtk.FileChooserNativeDialog) {
|
||||||
img.SetFromPixbuf(b)
|
img.SetFromPixbuf(b)
|
||||||
fc.SetPreviewWidgetActive(true)
|
fc.SetPreviewWidgetActive(true)
|
||||||
},
|
},
|
||||||
img,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,18 @@ package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
|
||||||
"github.com/gregjones/httpcache"
|
"github.com/gregjones/httpcache"
|
||||||
"github.com/gregjones/httpcache/diskcache"
|
"github.com/gregjones/httpcache/diskcache"
|
||||||
"github.com/peterbourgon/diskv"
|
"github.com/peterbourgon/diskv"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-sabotaging-the-desktop-experience")
|
var basePath = filepath.Join(os.TempDir(), "cchat-gtk-totally-not-node-modules")
|
||||||
|
|
||||||
var dskcached = http.Client{
|
var dskcached = http.Client{
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 15 * time.Second,
|
||||||
|
@ -25,40 +23,12 @@ var dskcached = http.Client{
|
||||||
TempDir: filepath.Join(basePath, "tmp"),
|
TempDir: filepath.Join(basePath, "tmp"),
|
||||||
PathPerm: 0750,
|
PathPerm: 0750,
|
||||||
FilePerm: 0750,
|
FilePerm: 0750,
|
||||||
Compression: diskv.NewZlibCompressionLevel(2),
|
Compression: diskv.NewZlibCompressionLevel(5),
|
||||||
CacheSizeMax: 25 * 1024 * 1024, // 25 MiB in memory
|
CacheSizeMax: 0, // 25 MiB in memory
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
func AsyncStreamUncached(url string, fn func(r io.Reader)) {
|
|
||||||
gts.Async(func() (func(), error) {
|
|
||||||
r, err := get(context.Background(), url, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
fn(r.Body)
|
|
||||||
r.Body.Close()
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func AsyncStream(url string, fn func(r io.Reader)) {
|
|
||||||
gts.Async(func() (func(), error) {
|
|
||||||
r, err := get(context.Background(), url, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
fn(r.Body)
|
|
||||||
r.Body.Close()
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
|
func get(ctx context.Context, url string, cached bool) (r *http.Response, err error) {
|
||||||
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
q, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,10 @@ package httputil
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
|
@ -41,7 +45,7 @@ type surfaceWrapper struct {
|
||||||
scale int
|
scale int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
|
func (wrapper *surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||||
surface, _ := gdk.CairoSurfaceCreateFromPixbuf(pb, wrapper.scale, nil)
|
surface, _ := gdk.CairoSurfaceCreateFromPixbuf(pb, wrapper.scale, nil)
|
||||||
wrapper.SetFromSurface(surface)
|
wrapper.SetFromSurface(surface)
|
||||||
}
|
}
|
||||||
|
@ -49,98 +53,149 @@ func (wrapper surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||||
// AsyncImage loads an image. This method uses the cache. It prefers loading
|
// AsyncImage loads an image. This method uses the cache. It prefers loading
|
||||||
// SetFromSurface over SetFromPixbuf, but will fallback if needed be.
|
// SetFromSurface over SetFromPixbuf, but will fallback if needed be.
|
||||||
func AsyncImage(ctx context.Context,
|
func AsyncImage(ctx context.Context,
|
||||||
img ImageContainer, url string, procs ...imgutil.Processor) {
|
img ImageContainer, imageURL string, procs ...imgutil.Processor) {
|
||||||
|
|
||||||
if url == "" {
|
if imageURL == "" {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gif := strings.Contains(url, ".gif")
|
|
||||||
scale := 1
|
|
||||||
|
|
||||||
surfaceContainer, canSurface := img.(SurfaceContainer)
|
|
||||||
|
|
||||||
if canSurface = canSurface && !gif; canSurface {
|
|
||||||
// Only bother with this API if we even have HiDPI.
|
|
||||||
if scale = surfaceContainer.GetScaleFactor(); scale > 1 {
|
|
||||||
img = surfaceWrapper{surfaceContainer, scale}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = primitives.HandleDestroyCtx(ctx, img)
|
|
||||||
|
|
||||||
l, err := gdk.PixbufLoaderNew()
|
|
||||||
if err != nil {
|
|
||||||
log.Error(errors.Wrap(err, "Failed to make pixbuf loader"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w, h := img.GetSizeRequest()
|
w, h := img.GetSizeRequest()
|
||||||
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
scale := 1
|
||||||
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
|
||||||
if w != imgW || h != imgH || scale > 1 {
|
surfaceContainer, canSurface := img.(SurfaceContainer)
|
||||||
l.SetSize(w*scale, h*scale)
|
if canSurface {
|
||||||
|
scale = surfaceContainer.GetScaleFactor()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ctx := primitives.HandleDestroyCtx(ctx, img)
|
||||||
|
|
||||||
|
// Try and guess the MIME type from the URL.
|
||||||
|
mimeType := mime.TypeByExtension(urlExt(imageURL))
|
||||||
|
|
||||||
|
r, err := get(ctx, imageURL, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "failed to GET"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
defer r.Body.Close()
|
||||||
|
|
||||||
l.Connect("area-prepared", areaPreparedFn(ctx, img, gif))
|
// Try and use the image type from the MIME header over the type from
|
||||||
|
// the URL, as it is more reliable.
|
||||||
|
if mime := mimeFromHeaders(r.Header); mime != "" {
|
||||||
|
mimeType = mime
|
||||||
|
}
|
||||||
|
|
||||||
go downloadImage(ctx, l, url, procs, gif)
|
_, fileType := path.Split(mimeType) // abuse split "a/b" to get b
|
||||||
|
|
||||||
|
isGIF := fileType == "gif"
|
||||||
|
if isGIF {
|
||||||
|
canSurface = false
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only bother with this if we even have HiDPI. We also can't use a
|
||||||
|
// Surface for a GIF.
|
||||||
|
if canSurface && scale > 1 {
|
||||||
|
img = &surfaceWrapper{surfaceContainer, scale}
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := gdk.PixbufLoaderNewWithType(fileType)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(errors.Wrap(err, "failed to make pixbuf loader"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
||||||
|
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
||||||
|
if w != imgW || h != imgH || scale > 1 {
|
||||||
|
l.SetSize(w*scale, h*scale)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
load := loadFn(ctx, img, isGIF)
|
||||||
|
l.Connect("area-prepared", load)
|
||||||
|
l.Connect("area-updated", load)
|
||||||
|
|
||||||
|
if err := downloadImage(r.Body, l, procs, isGIF); err != nil {
|
||||||
|
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
|
||||||
|
// Force close after downloading.
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.Close(); err != nil {
|
||||||
|
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func areaPreparedFn(ctx context.Context, img ImageContainer, gif bool) func(l *gdk.PixbufLoader) {
|
func urlExt(anyURL string) string {
|
||||||
|
u, err := url.Parse(anyURL)
|
||||||
|
if err != nil {
|
||||||
|
return path.Ext(strings.SplitN(anyURL, "?", 1)[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Ext(u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mimeFromHeaders(headers http.Header) string {
|
||||||
|
cType := headers.Get("Content-Type")
|
||||||
|
if cType == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
media, _, err := mime.ParseMediaType(cType)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFn(ctx context.Context, img ImageContainer, isGIF bool) func(l *gdk.PixbufLoader) {
|
||||||
|
var pixbuf interface{}
|
||||||
|
|
||||||
return func(l *gdk.PixbufLoader) {
|
return func(l *gdk.PixbufLoader) {
|
||||||
if !gif {
|
if pixbuf == nil {
|
||||||
p, err := l.GetPixbuf()
|
if !isGIF {
|
||||||
if err != nil {
|
pixbuf, _ = l.GetPixbuf()
|
||||||
log.Error(errors.Wrap(err, "Failed to get pixbuf"))
|
} else {
|
||||||
return
|
pixbuf, _ = l.GetAnimation()
|
||||||
}
|
}
|
||||||
execIfCtx(ctx, func() { img.SetFromPixbuf(p) })
|
}
|
||||||
} else {
|
|
||||||
p, err := l.GetAnimation()
|
switch pixbuf := pixbuf.(type) {
|
||||||
if err != nil {
|
case *gdk.Pixbuf:
|
||||||
log.Error(errors.Wrap(err, "Failed to get animation"))
|
execIfCtx(ctx, func() { img.SetFromPixbuf(pixbuf) })
|
||||||
return
|
case *gdk.PixbufAnimation:
|
||||||
}
|
execIfCtx(ctx, func() { img.SetFromAnimation(pixbuf) })
|
||||||
execIfCtx(ctx, func() { img.SetFromAnimation(p) })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func execIfCtx(ctx context.Context, fn func()) {
|
func execIfCtx(ctx context.Context, fn func()) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecLater(func() {
|
||||||
if ctx.Err() == nil {
|
if ctx.Err() == nil {
|
||||||
fn()
|
fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadImage(ctx context.Context, dst io.WriteCloser, url string, p []imgutil.Processor, gif bool) {
|
func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error {
|
||||||
// Close at the end when done.
|
var err error
|
||||||
defer dst.Close()
|
|
||||||
|
|
||||||
r, err := get(ctx, url, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// If we have processors, then write directly in there.
|
// If we have processors, then write directly in there.
|
||||||
if len(p) > 0 {
|
if len(p) > 0 {
|
||||||
if !gif {
|
if !isGIF {
|
||||||
err = imgutil.ProcessStream(dst, r.Body, p)
|
err = imgutil.ProcessStream(dst, src, p)
|
||||||
} else {
|
} else {
|
||||||
err = imgutil.ProcessAnimationStream(dst, r.Body, p)
|
err = imgutil.ProcessAnimationStream(dst, src, p)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Else, directly copy the body over.
|
// Else, directly copy the body over.
|
||||||
_, err = io.Copy(dst, r.Body)
|
_, err = io.Copy(dst, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(errors.Wrap(err, "Error processing image"))
|
return errors.Wrap(err, "failed to process image")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ type State struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
Connect(string, interface{}) glib.SignalHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
func Bind(app *gtk.Application) *State {
|
func Bind(app *gtk.Application) *State {
|
||||||
|
@ -34,8 +34,8 @@ func Bind(app *gtk.Application) *State {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) Connect(c Connector) {
|
func (s *State) Connect(c Connector) {
|
||||||
c.Connect("focus-out-event", s.Start)
|
c.Connect("focus-out-event", func(interface{}) { s.Start() })
|
||||||
c.Connect("focus-in-event", s.Stop)
|
c.Connect("focus-in-event", func(interface{}) { s.Stop() })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) Start() {
|
func (s *State) Start() {
|
||||||
|
|
|
@ -81,7 +81,7 @@ func NewPreferenceDialog() *Dialog {
|
||||||
|
|
||||||
func SpawnPreferenceDialog() {
|
func SpawnPreferenceDialog() {
|
||||||
p := NewPreferenceDialog()
|
p := NewPreferenceDialog()
|
||||||
p.Connect("destroy", func() {
|
p.Connect("destroy", func(interface{}) {
|
||||||
// On close, save the settings.
|
// On close, save the settings.
|
||||||
if err := config.Save(); err != nil {
|
if err := config.Save(); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to save settings"))
|
log.Error(errors.Wrap(err, "Failed to save settings"))
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (c *_combo) Construct() gtk.IWidget {
|
||||||
combo.Append(opt, opt)
|
combo.Append(opt, opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
combo.Connect("changed", func() { c.set(combo.GetActive()) })
|
combo.Connect("changed", func(combo *gtk.ComboBoxText) { c.set(combo.GetActive()) })
|
||||||
combo.SetActive(*c.selected)
|
combo.SetActive(*c.selected)
|
||||||
combo.SetHAlign(gtk.ALIGN_END)
|
combo.SetHAlign(gtk.ALIGN_END)
|
||||||
combo.Show()
|
combo.Show()
|
||||||
|
@ -76,7 +76,7 @@ func (s *_switch) set(v bool) {
|
||||||
func (s *_switch) Construct() gtk.IWidget {
|
func (s *_switch) Construct() gtk.IWidget {
|
||||||
sw, _ := gtk.SwitchNew()
|
sw, _ := gtk.SwitchNew()
|
||||||
sw.SetActive(*s.value)
|
sw.SetActive(*s.value)
|
||||||
sw.Connect("notify::active", func() { s.set(sw.GetActive()) })
|
sw.Connect("notify::active", func(sw *gtk.Switch) { s.set(sw.GetActive()) })
|
||||||
sw.SetHAlign(gtk.ALIGN_END)
|
sw.SetHAlign(gtk.ALIGN_END)
|
||||||
sw.Show()
|
sw.Show()
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ func (e *_inputentry) Construct() gtk.IWidget {
|
||||||
entry.SetHExpand(true)
|
entry.SetHExpand(true)
|
||||||
entry.SetText(*e.value)
|
entry.SetText(*e.value)
|
||||||
|
|
||||||
entry.Connect("changed", func() {
|
entry.Connect("changed", func(entry *gtk.Entry) {
|
||||||
v, err := entry.GetText()
|
v, err := entry.GetText()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -53,8 +53,8 @@ func NewModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) *M
|
||||||
header,
|
header,
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel.Connect("clicked", dialog.Destroy)
|
cancel.Connect("clicked", func(interface{}) { dialog.Destroy() })
|
||||||
action.Connect("clicked", func() { clicked(modald) })
|
action.Connect("clicked", func(interface{}) { clicked(modald) })
|
||||||
|
|
||||||
return modald
|
return modald
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func newCSD(body, header gtk.IWidget) *gtk.Dialog {
|
||||||
dialog.Add(body)
|
dialog.Add(body)
|
||||||
|
|
||||||
if oldh, _ := dialog.GetHeaderBar(); oldh != nil {
|
if oldh, _ := dialog.GetHeaderBar(); oldh != nil {
|
||||||
dialog.Remove(oldh)
|
oldh.ToWidget().Destroy()
|
||||||
}
|
}
|
||||||
dialog.SetTitlebar(header)
|
dialog.SetTitlebar(header)
|
||||||
|
|
||||||
|
|
|
@ -66,9 +66,9 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
||||||
avatar := NewAvatar()
|
avatar := NewAvatar()
|
||||||
avatar.SetMarginTop(TopFullMargin)
|
avatar.SetMarginTop(TopFullMargin)
|
||||||
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
avatar.SetMarginStart(container.ColumnSpacing * 2)
|
||||||
avatar.Connect("clicked", func() {
|
avatar.Connect("clicked", func(w gtk.IWidget) {
|
||||||
if output := gc.Username.Output(); len(output.Mentions) > 0 {
|
if output := gc.Username.Output(); len(output.Mentions) > 0 {
|
||||||
labeluri.PopoverMentioner(avatar, output.Input, output.Mentions[0])
|
labeluri.PopoverMentioner(w, output.Input, output.Mentions[0])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// We don't call avatar.Show(). That's called in Attach.
|
// We don't call avatar.Show(). That's called in Attach.
|
||||||
|
|
|
@ -95,12 +95,12 @@ func (h *Header) Reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Header) OnBackPressed(fn func()) {
|
func (h *Header) OnBackPressed(fn func()) {
|
||||||
h.BackButton.Connect("clicked", fn)
|
h.BackButton.Connect("clicked", func(*gtk.Button) { fn() })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Header) OnShowMembersToggle(fn func(show bool)) {
|
func (h *Header) OnShowMembersToggle(fn func(show bool)) {
|
||||||
h.ShowMembers.Connect("toggled", func() {
|
h.ShowMembers.Connect("toggled", func(showMembers *gtk.ToggleButton) {
|
||||||
fn(h.ShowMembers.GetActive())
|
fn(showMembers.GetActive())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
package attachment
|
package attachment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
"mime"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/gotk3/gotk3/cairo"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pngEncoder = png.Encoder{
|
|
||||||
CompressionLevel: png.BestCompression,
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ThumbSize = 72
|
ThumbSize = 72
|
||||||
IconSize = 56
|
IconSize = 56
|
||||||
|
@ -37,7 +28,7 @@ const (
|
||||||
type File struct {
|
type File struct {
|
||||||
Prog *Progress
|
Prog *Progress
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
Size int64 // -1 = stream
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFile creates a new attachment file with a progress state.
|
// NewFile creates a new attachment file with a progress state.
|
||||||
|
@ -70,7 +61,7 @@ type Container struct {
|
||||||
|
|
||||||
// states
|
// states
|
||||||
files []File
|
files []File
|
||||||
items map[string]gtk.IWidget
|
items map[string]primitives.WidgetDestroyer
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachmentsCSS = primitives.PrepareCSS(`
|
var attachmentsCSS = primitives.PrepareCSS(`
|
||||||
|
@ -120,7 +111,7 @@ func New() *Container {
|
||||||
Revealer: rev,
|
Revealer: rev,
|
||||||
Scroll: scr,
|
Scroll: scr,
|
||||||
Box: box,
|
Box: box,
|
||||||
items: map[string]gtk.IWidget{},
|
items: map[string]primitives.WidgetDestroyer{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +146,11 @@ func (c *Container) Reset() {
|
||||||
|
|
||||||
// Clear all items.
|
// Clear all items.
|
||||||
for _, item := range c.items {
|
for _, item := range c.items {
|
||||||
c.Box.Remove(item)
|
item.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the map.
|
// Reset the map.
|
||||||
c.items = map[string]gtk.IWidget{}
|
c.items = map[string]primitives.WidgetDestroyer{}
|
||||||
|
|
||||||
// Hide the window.
|
// Hide the window.
|
||||||
c.SetRevealChild(false)
|
c.SetRevealChild(false)
|
||||||
|
@ -187,61 +178,31 @@ func (c *Container) AddFile(path string) error {
|
||||||
func() (io.ReadCloser, error) { return os.Open(path) },
|
func() (io.ReadCloser, error) { return os.Open(path) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
scale := c.GetScaleFactor()
|
||||||
|
|
||||||
// Maybe try making a preview. A nil image is fine, so we can skip the error
|
// Maybe try making a preview. A nil image is fine, so we can skip the error
|
||||||
// check.
|
// check.
|
||||||
// TODO: add a filesize check
|
// TODO: add a filesize check
|
||||||
i, _ := imaging.Open(path, imaging.AutoOrientation(true))
|
pixbuf, _ := gdk.PixbufNewFromFileAtScale(path, ThumbSize*scale, ThumbSize*scale, true)
|
||||||
c.addPreview(filename, i)
|
c.addPreview(filename, thumbnailPixbuf(pixbuf, scale))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPixbuf is used for adding pixbufs from the clipboard.
|
// AddPixbuf is used for adding pixbufs from the clipboard.
|
||||||
func (c *Container) AddPixbuf(pb *gdk.Pixbuf) error {
|
func (c *Container) AddPixbuf(pb *gdk.Pixbuf) {
|
||||||
// Pixbuf's colorspace is only RGB. This is indicated with
|
|
||||||
// GDK_COLORSPACE_RGB.
|
|
||||||
if pb.GetColorspace() != gdk.COLORSPACE_RGB {
|
|
||||||
return errors.New("Pixbuf has unsupported colorspace")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that the pixbuf has alpha, as we're using RGBA.
|
|
||||||
if !pb.GetHasAlpha() {
|
|
||||||
return errors.New("Pixbuf has no alpha channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that there are 4 channels: red, green, blue and alpha.
|
|
||||||
if pb.GetNChannels() != 4 {
|
|
||||||
return errors.New("Pixbuf has unexpected channel count")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that there are 8 bits in a channel/sample.
|
|
||||||
if pb.GetBitsPerSample() != 8 {
|
|
||||||
return errors.New("Pixbuf has unexpected bits per sample")
|
|
||||||
}
|
|
||||||
|
|
||||||
var img = &image.NRGBA{
|
|
||||||
Pix: pb.GetPixels(),
|
|
||||||
Stride: pb.GetRowstride(),
|
|
||||||
Rect: image.Rect(0, 0, pb.GetWidth(), pb.GetHeight()),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the image in memory.
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
if err := pngEncoder.Encode(&buf, img); err != nil {
|
|
||||||
return errors.Wrap(err, "Failed to encode PNG")
|
|
||||||
}
|
|
||||||
|
|
||||||
var filename = c.append(
|
var filename = c.append(
|
||||||
fmt.Sprintf("clipboard_%d.png", len(c.files)+1), int64(buf.Len()),
|
fmt.Sprintf("clipboard_%d.png", len(c.files)+1), -1,
|
||||||
func() (io.ReadCloser, error) {
|
func() (io.ReadCloser, error) {
|
||||||
return ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
|
r, w := io.Pipe()
|
||||||
|
go func() { w.CloseWithError(pb.WritePNG(w, 9)) }()
|
||||||
|
return r, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
c.addPreview(filename, img)
|
scale := c.GetScaleFactor()
|
||||||
|
|
||||||
return nil
|
c.addPreview(filename, thumbnailPixbuf(pb, scale))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- internal methods --
|
// -- internal methods --
|
||||||
|
@ -273,7 +234,7 @@ func (c *Container) remove(name string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if w, ok := c.items[name]; ok {
|
if w, ok := c.items[name]; ok {
|
||||||
c.Box.Remove(w)
|
w.Destroy()
|
||||||
delete(c.items, name)
|
delete(c.items, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,7 +270,7 @@ var deleteAttBtnCSS = primitives.PrepareCSS(`
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func (c *Container) addPreview(name string, src image.Image) {
|
func (c *Container) addPreview(name string, thumbnail *cairo.Surface) {
|
||||||
// Make a fallback image first.
|
// Make a fallback image first.
|
||||||
gimg, _ := roundimage.NewImage(4) // border-radius: 4px
|
gimg, _ := roundimage.NewImage(4) // border-radius: 4px
|
||||||
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize)
|
primitives.SetImageIcon(gimg.Image, iconFromName(name), IconSize)
|
||||||
|
@ -322,19 +283,8 @@ func (c *Container) addPreview(name string, src image.Image) {
|
||||||
primitives.AttachCSS(gimg, previewCSS)
|
primitives.AttachCSS(gimg, previewCSS)
|
||||||
|
|
||||||
// Determine if we could generate an image preview.
|
// Determine if we could generate an image preview.
|
||||||
if src != nil {
|
if thumbnail != nil {
|
||||||
// Get the minimum dimension.
|
gimg.SetFromSurface(thumbnail)
|
||||||
var w, h = minsize(src.Bounds().Dx(), src.Bounds().Dy(), ThumbSize)
|
|
||||||
|
|
||||||
var img *image.NRGBA
|
|
||||||
// Downscale the image.
|
|
||||||
img = imaging.Resize(src, w, h, imaging.Lanczos)
|
|
||||||
|
|
||||||
// Crop to a square.
|
|
||||||
img = imaging.CropCenter(img, ThumbSize, ThumbSize)
|
|
||||||
|
|
||||||
// Copy the image to a pixbuf.
|
|
||||||
gimg.SetFromPixbuf(gts.RenderPixbuf(img))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BLOAT!!! Make an overlay of an event box that, when hovered, will show
|
// BLOAT!!! Make an overlay of an event box that, when hovered, will show
|
||||||
|
@ -343,7 +293,7 @@ func (c *Container) addPreview(name string, src image.Image) {
|
||||||
del.SetVAlign(gtk.ALIGN_CENTER)
|
del.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
del.SetHAlign(gtk.ALIGN_CENTER)
|
del.SetHAlign(gtk.ALIGN_CENTER)
|
||||||
del.SetTooltipText("Remove " + name)
|
del.SetTooltipText("Remove " + name)
|
||||||
del.Connect("clicked", func() { c.remove(name) })
|
del.Connect("clicked", func(del *gtk.Button) { c.remove(name) })
|
||||||
del.Show()
|
del.Show()
|
||||||
primitives.AddClass(del, "delete-attachment")
|
primitives.AddClass(del, "delete-attachment")
|
||||||
primitives.AttachCSS(del, deleteAttBtnCSS)
|
primitives.AttachCSS(del, deleteAttBtnCSS)
|
||||||
|
@ -358,6 +308,58 @@ func (c *Container) addPreview(name string, src image.Image) {
|
||||||
c.Box.PackStart(ovl, false, false, 0)
|
c.Box.PackStart(ovl, false, false, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func thumbnailPixbuf(pixbuf *gdk.Pixbuf, scale int) *cairo.Surface {
|
||||||
|
if pixbuf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
originalWidth = pixbuf.GetWidth()
|
||||||
|
originalHeight = pixbuf.GetHeight()
|
||||||
|
|
||||||
|
scaledThumbSize = ThumbSize * scale
|
||||||
|
scaledWidth, scaledHeight = minsize(originalWidth, originalHeight, scaledThumbSize)
|
||||||
|
|
||||||
|
// offset of src on thumbnail; one of those will be 0
|
||||||
|
offsetX = float64(scaledThumbSize-scaledWidth) / 2
|
||||||
|
offsetY = float64(scaledThumbSize-scaledHeight) / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
thumbnail, err := gdk.PixbufNew(
|
||||||
|
pixbuf.GetColorspace(),
|
||||||
|
true, 8, // always have alpha, 8bpc
|
||||||
|
scaledThumbSize, scaledThumbSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to allocate upload thumbnail pixbuf: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill with transparent pixels.
|
||||||
|
thumbnail.Fill(0x0)
|
||||||
|
|
||||||
|
pixbuf.Scale(
|
||||||
|
thumbnail,
|
||||||
|
int(offsetX), int(offsetY),
|
||||||
|
// size of src on thumbnail
|
||||||
|
scaledWidth, scaledHeight,
|
||||||
|
// no offset on source image
|
||||||
|
offsetX, offsetY,
|
||||||
|
// scale ratio for both sides
|
||||||
|
float64(scaledWidth)/float64(originalWidth),
|
||||||
|
float64(scaledHeight)/float64(originalHeight),
|
||||||
|
// expensive rescale algorithm
|
||||||
|
gdk.INTERP_HYPER,
|
||||||
|
)
|
||||||
|
|
||||||
|
surface, err := gdk.CairoSurfaceCreateFromPixbuf(thumbnail, scale, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to create thumbnail cairo surface: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return surface
|
||||||
|
}
|
||||||
|
|
||||||
func iconFromName(filename string) string {
|
func iconFromName(filename string) string {
|
||||||
switch t := mime.TypeByExtension(filepath.Ext(filename)); {
|
switch t := mime.TypeByExtension(filepath.Ext(filename)); {
|
||||||
case strings.HasPrefix(t, "image"):
|
case strings.HasPrefix(t, "image"):
|
||||||
|
@ -376,8 +378,9 @@ func iconFromName(filename string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// minsize returns the scaled size so that the largest edge is maxsz.
|
||||||
func minsize(w, h, maxsz int) (int, int) {
|
func minsize(w, h, maxsz int) (int, int) {
|
||||||
if w < h {
|
if w > h {
|
||||||
// return the scaled width as max
|
// return the scaled width as max
|
||||||
// h*max/w is the same as h/w*max but with more accuracy
|
// h*max/w is the same as h/w*max but with more accuracy
|
||||||
return maxsz, h * maxsz / w
|
return maxsz, h * maxsz / w
|
||||||
|
@ -385,3 +388,17 @@ func minsize(w, h, maxsz int) (int, int) {
|
||||||
|
|
||||||
return w * maxsz / h, maxsz
|
return w * maxsz / h, maxsz
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(w, h int) int {
|
||||||
|
if w > h {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(w, h int) int {
|
||||||
|
if w > h {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
"github.com/gotk3/gotk3/pango"
|
"github.com/gotk3/gotk3/pango"
|
||||||
)
|
)
|
||||||
|
@ -47,7 +48,12 @@ func NewProgressBar(file File) *ProgressBar {
|
||||||
bar.SetVAlign(gtk.ALIGN_CENTER)
|
bar.SetVAlign(gtk.ALIGN_CENTER)
|
||||||
bar.Show()
|
bar.Show()
|
||||||
|
|
||||||
name, _ := gtk.LabelNew(file.Name)
|
var label = file.Name
|
||||||
|
if file.Size > 0 {
|
||||||
|
label += " - " + glib.FormatSize(uint64(file.Size))
|
||||||
|
}
|
||||||
|
|
||||||
|
name, _ := gtk.LabelNew(label)
|
||||||
name.SetMaxWidthChars(45)
|
name.SetMaxWidthChars(45)
|
||||||
name.SetSingleLineMode(true)
|
name.SetSingleLineMode(true)
|
||||||
name.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
name.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||||
|
|
|
@ -196,16 +196,23 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||||
// Bind text events.
|
// Bind text events.
|
||||||
text.Connect("key-press-event", field.keyDown)
|
text.Connect("key-press-event", field.keyDown)
|
||||||
// Bind the send button.
|
// Bind the send button.
|
||||||
field.send.Connect("clicked", field.sendInput)
|
field.send.Connect("clicked", func(*gtk.Button) { field.sendInput() })
|
||||||
// Bind the attach button.
|
// Bind the attach button.
|
||||||
field.attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) })
|
field.attach.Connect("clicked", func(attach *gtk.Button) {
|
||||||
|
gts.SpawnUploader("", field.Attachments.AddFiles)
|
||||||
|
})
|
||||||
|
|
||||||
|
// allocatedWidthGetter is used below.
|
||||||
|
type allocatedWidthGetter interface {
|
||||||
|
GetAllocatedWidth() int
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to the field's revealer. On resize, we want the attachments
|
// Connect to the field's revealer. On resize, we want the attachments
|
||||||
// carousel to have the same padding too.
|
// carousel to have the same padding too.
|
||||||
field.Username.Connect("size-allocate", func(w gtk.IWidget) {
|
field.Username.Connect("size-allocate", func(w allocatedWidthGetter) {
|
||||||
// Calculate the left width: from the left of the message box to the
|
// Calculate the left width: from the left of the message box to the
|
||||||
// right of the attach button, covering the username container.
|
// right of the attach button, covering the username container.
|
||||||
var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
|
var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.GetAllocatedWidth()
|
||||||
// Set the autocompleter's left margin to be the same.
|
// Set the autocompleter's left margin to be the same.
|
||||||
field.Attachments.SetMarginStart(leftWidth)
|
field.Attachments.SetMarginStart(leftWidth)
|
||||||
})
|
})
|
||||||
|
|
|
@ -84,23 +84,23 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make this asynchronous.
|
||||||
|
|
||||||
// Is there an image in the clipboard?
|
// Is there an image in the clipboard?
|
||||||
if !gts.Clipboard.WaitIsImageAvailable() {
|
if !gts.Clipboard.WaitIsImageAvailable() {
|
||||||
// No.
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Yes.
|
|
||||||
|
|
||||||
p, err := gts.Clipboard.WaitForImage()
|
gts.Async(func() (func(), error) {
|
||||||
if err != nil {
|
p, err := gts.Clipboard.WaitForImage()
|
||||||
log.Error(errors.Wrap(err, "Failed to get image from clipboard"))
|
if err != nil {
|
||||||
return true // interrupt as technically valid
|
return nil, errors.Wrap(err, "Failed to get image from clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Attachments.AddPixbuf(p); err != nil {
|
return func() { f.Attachments.AddPixbuf(p) }, nil
|
||||||
log.Error(errors.Wrap(err, "Failed to add image to attachment list"))
|
})
|
||||||
return true
|
|
||||||
}
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the server supports typing indication, then announce that we are
|
// If the server supports typing indication, then announce that we are
|
||||||
|
|
|
@ -87,7 +87,7 @@ func (c *Container) Reset() {
|
||||||
c.Revealer.SetRevealChild(false)
|
c.Revealer.SetRevealChild(false)
|
||||||
|
|
||||||
for _, section := range c.Sections {
|
for _, section := range c.Sections {
|
||||||
c.Main.Remove(section)
|
section.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Sections = map[string]*Section{}
|
c.Sections = map[string]*Section{}
|
||||||
|
@ -276,7 +276,7 @@ func (s *Section) RemoveMember(id string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listSortNameAsc(r1, r2 *gtk.ListBoxRow, _ ...interface{}) int {
|
func listSortNameAsc(r1, r2 *gtk.ListBoxRow) int {
|
||||||
n1, _ := r1.GetName()
|
n1, _ := r1.GetName()
|
||||||
n2, _ := r2.GetName()
|
n2, _ := r2.GetName()
|
||||||
|
|
||||||
|
@ -400,7 +400,7 @@ func (m *Member) Popup(evq EventQueuer) {
|
||||||
// Unbounded concurrency is kind of bad. We should deal with
|
// Unbounded concurrency is kind of bad. We should deal with
|
||||||
// this in the future.
|
// this in the future.
|
||||||
evq.Activate()
|
evq.Activate()
|
||||||
p.Connect("closed", evq.Deactivate)
|
p.Connect("closed", func(interface{}) { evq.Deactivate() })
|
||||||
|
|
||||||
p.SetPosition(gtk.POS_LEFT)
|
p.SetPosition(gtk.POS_LEFT)
|
||||||
p.Popup()
|
p.Popup()
|
||||||
|
|
|
@ -58,7 +58,7 @@ func New() *Container {
|
||||||
})
|
})
|
||||||
|
|
||||||
// On label destroy, stop the state loop as well.
|
// On label destroy, stop the state loop as well.
|
||||||
l.Connect("destroy", state.stopper)
|
l.Connect("destroy", func(interface{}) { state.stopper() })
|
||||||
|
|
||||||
return &Container{
|
return &Container{
|
||||||
Revealer: r,
|
Revealer: r,
|
||||||
|
|
|
@ -155,7 +155,8 @@ func NewView(c Controller) *View {
|
||||||
drag.BindFileDest(view.LeftBox, view.InputView.Attachments.AddFiles)
|
drag.BindFileDest(view.LeftBox, view.InputView.Attachments.AddFiles)
|
||||||
|
|
||||||
// placeholder logo
|
// placeholder logo
|
||||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
|
logo, _ := gtk.ImageNew()
|
||||||
|
logo.SetFromSurface(icons.Logo256Variant2(128, logo.GetScaleFactor()))
|
||||||
logo.Show()
|
logo.Show()
|
||||||
|
|
||||||
view.FaceView = sadface.New(view.Leaflet, logo)
|
view.FaceView = sadface.New(view.Leaflet, logo)
|
||||||
|
@ -201,6 +202,7 @@ func (v *View) createMessageContainer() {
|
||||||
|
|
||||||
// Remove the old message container.
|
// Remove the old message container.
|
||||||
if v.Container != nil {
|
if v.Container != nil {
|
||||||
|
v.Container.Reset()
|
||||||
v.MsgBox.Remove(v.Container)
|
v.MsgBox.Remove(v.Container)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,13 +223,19 @@ func (v *View) createMessageContainer() {
|
||||||
|
|
||||||
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
|
func (v *View) Bottomed() bool { return v.Scroller.Bottomed }
|
||||||
|
|
||||||
|
// Reset resets the message view.
|
||||||
func (v *View) Reset() {
|
func (v *View) Reset() {
|
||||||
|
v.FaceView.Reset() // Switch back to the main screen.
|
||||||
|
v.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets the message view, but does not change visible containers.
|
||||||
|
func (v *View) reset() {
|
||||||
v.Header.Reset() // Reset the header.
|
v.Header.Reset() // Reset the header.
|
||||||
v.state.Reset() // Reset the state variables.
|
v.state.Reset() // Reset the state variables.
|
||||||
v.Typing.Reset() // Reset the typing state.
|
v.Typing.Reset() // Reset the typing state.
|
||||||
v.InputView.Reset() // Reset the input.
|
v.InputView.Reset() // Reset the input.
|
||||||
v.MemberList.Reset() // Reset the member list.
|
v.MemberList.Reset() // Reset the member list.
|
||||||
v.FaceView.Reset() // Switch back to the main screen.
|
|
||||||
|
|
||||||
// Bring the leaflet view back to the message.
|
// Bring the leaflet view back to the message.
|
||||||
v.Leaflet.SetVisibleChild(v.LeftBox)
|
v.Leaflet.SetVisibleChild(v.LeftBox)
|
||||||
|
@ -282,16 +290,8 @@ func (v *View) JoinServer(session cchat.Session, server cchat.Server, bc travers
|
||||||
v.FaceView.SetLoading()
|
v.FaceView.SetLoading()
|
||||||
v.ctrl.OnMessageBusy()
|
v.ctrl.OnMessageBusy()
|
||||||
|
|
||||||
// We can be dumb. Reset afterwards so the animation goes smoother.
|
|
||||||
gts.DoAfterMs(
|
|
||||||
v.FaceView.GetTransitionDuration(),
|
|
||||||
func() { v.joinServer(session, server, bc) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *View) joinServer(session cchat.Session, server cchat.Server, bc traverse.Breadcrumber) {
|
|
||||||
// Reset before setting.
|
// Reset before setting.
|
||||||
v.Reset()
|
v.reset()
|
||||||
|
|
||||||
// Get the messenger once.
|
// Get the messenger once.
|
||||||
var messenger = server.AsMessenger()
|
var messenger = server.AsMessenger()
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (m *MenuButton) Bind(menu *Menu) {
|
||||||
// menu items.
|
// menu items.
|
||||||
m.SetSensitive(model.GetNItems() > 0)
|
m.SetSensitive(model.GetNItems() > 0)
|
||||||
// Subscribe the button to menu update events.
|
// Subscribe the button to menu update events.
|
||||||
m.lastsig, _ = model.Connect("items-changed", func() {
|
m.lastsig = model.Connect("items-changed", func(model *glib.MenuModel) {
|
||||||
m.SetSensitive(model.GetNItems() > 0)
|
m.SetSensitive(model.GetNItems() > 0)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -46,10 +46,6 @@ func Take(b, smallbutton Button, size int) {
|
||||||
childv, _ := b.GetChild()
|
childv, _ := b.GetChild()
|
||||||
widget := childv.ToWidget()
|
widget := childv.ToWidget()
|
||||||
|
|
||||||
// As GetChild doesn't reference, we'll want our own reference.
|
|
||||||
widget.Ref()
|
|
||||||
defer widget.Unref()
|
|
||||||
|
|
||||||
// This will unreference.
|
// This will unreference.
|
||||||
b.Remove(widget)
|
b.Remove(widget)
|
||||||
// Wrap will reference.
|
// Wrap will reference.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||||
|
@ -68,9 +69,9 @@ func NewCompleter(input *gtk.TextView) *Completer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This one is for buffer modification.
|
// This one is for buffer modification.
|
||||||
ibuf.Connect("end-user-action", c.onChange)
|
ibuf.Connect("end-user-action", func(interface{}) { c.onChange() })
|
||||||
// This one is for when the cursor moves.
|
// This one is for when the cursor moves.
|
||||||
input.Connect("move-cursor", c.onChange)
|
input.Connect("move-cursor", func(interface{}) { c.onChange() })
|
||||||
|
|
||||||
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor)
|
SwapWord(ibuf, c.entries[r.GetIndex()].Raw, c.cursor)
|
||||||
|
@ -115,9 +116,7 @@ func (c *Completer) Clear() {
|
||||||
}
|
}
|
||||||
|
|
||||||
children.Foreach(func(i interface{}) {
|
children.Foreach(func(i interface{}) {
|
||||||
w := i.(gtk.IWidget).ToWidget()
|
i.(primitives.WidgetDestroyer).Destroy()
|
||||||
c.List.Remove(w)
|
|
||||||
w.Destroy()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,21 +104,21 @@ func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Dragga
|
||||||
dragger.DragSourceSet(gdk.BUTTON1_MASK, dragEntries, gdk.ACTION_MOVE)
|
dragger.DragSourceSet(gdk.BUTTON1_MASK, dragEntries, gdk.ACTION_MOVE)
|
||||||
|
|
||||||
dragger.Connect("drag-data-get",
|
dragger.Connect("drag-data-get",
|
||||||
func(_ gtk.IWidget, ctx *gdk.DragContext, data *gtk.SelectionData) {
|
func(_ interface{}, ctx *gdk.DragContext, data *gtk.SelectionData) {
|
||||||
// Set the index-in-bytes.
|
// Set the index-in-bytes.
|
||||||
data.SetData(dragAtom, []byte(dg.ID()))
|
data.SetData(dragAtom, []byte(dg.ID()))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
dragger.Connect("drag-begin",
|
dragger.Connect("drag-begin",
|
||||||
func(_ gtk.IWidget, ctx *gdk.DragContext) {
|
func(_ interface{}, ctx *gdk.DragContext) {
|
||||||
gtk.DragSetIconName(ctx, icon, 0, 0)
|
gtk.DragSetIconName(ctx, icon, 0, 0)
|
||||||
dg.SetSensitive(false)
|
dg.SetSensitive(false)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
dragger.Connect("drag-end",
|
dragger.Connect("drag-end",
|
||||||
func() {
|
func(interface{}) {
|
||||||
dg.SetSensitive(true)
|
dg.SetSensitive(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -117,7 +117,7 @@ func SimpleItem(name string, fn func()) Item {
|
||||||
|
|
||||||
func (item Item) ToMenuItem() *gtk.MenuItem {
|
func (item Item) ToMenuItem() *gtk.MenuItem {
|
||||||
mb, _ := gtk.MenuItemNewWithLabel(item.Name)
|
mb, _ := gtk.MenuItemNewWithLabel(item.Name)
|
||||||
mb.Connect("activate", item.Func)
|
mb.Connect("activate", func(interface{}) { item.Func() })
|
||||||
mb.Show()
|
mb.Show()
|
||||||
|
|
||||||
if item.Extra != nil {
|
if item.Extra != nil {
|
||||||
|
@ -129,7 +129,7 @@ func (item Item) ToMenuItem() *gtk.MenuItem {
|
||||||
|
|
||||||
func (item Item) ToToolButton() *gtk.ToolButton {
|
func (item Item) ToToolButton() *gtk.ToolButton {
|
||||||
tb, _ := gtk.ToolButtonNew(nil, item.Name)
|
tb, _ := gtk.ToolButtonNew(nil, item.Name)
|
||||||
tb.Connect("clicked", item.Func)
|
tb.Connect("clicked", func(interface{}) { item.Func() })
|
||||||
tb.Show()
|
tb.Show()
|
||||||
|
|
||||||
return tb
|
return tb
|
||||||
|
|
|
@ -13,6 +13,11 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WidgetDestroyer interface {
|
||||||
|
gtk.IWidget
|
||||||
|
Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
type Container interface {
|
type Container interface {
|
||||||
Remove(gtk.IWidget)
|
Remove(gtk.IWidget)
|
||||||
GetChildren() *glib.List
|
GetChildren() *glib.List
|
||||||
|
@ -21,8 +26,12 @@ type Container interface {
|
||||||
var _ Container = (*gtk.Container)(nil)
|
var _ Container = (*gtk.Container)(nil)
|
||||||
|
|
||||||
func RemoveChildren(w Container) {
|
func RemoveChildren(w Container) {
|
||||||
|
type destroyer interface {
|
||||||
|
Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
w.GetChildren().Foreach(func(child interface{}) {
|
w.GetChildren().Foreach(func(child interface{}) {
|
||||||
w.Remove(child.(gtk.IWidget))
|
child.(destroyer).Destroy()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,18 +175,18 @@ func MenuItem(label string, fn interface{}) *gtk.MenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
Connect(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
Connect(string, interface{}) glib.SignalHandle
|
||||||
ConnectAfter(string, interface{}, ...interface{}) (glib.SignalHandle, error)
|
ConnectAfter(string, interface{}) glib.SignalHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context {
|
func HandleDestroyCtx(ctx context.Context, connector Connector) context.Context {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
connector.Connect("destroy", cancel)
|
connector.Connect("destroy", func(c Connector) { cancel() })
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func BindMenu(connector Connector, menu *gtk.Menu) {
|
func BindMenu(connector Connector, menu *gtk.Menu) {
|
||||||
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
menu.PopupAtPointer(ev)
|
menu.PopupAtPointer(ev)
|
||||||
}
|
}
|
||||||
|
@ -185,7 +194,7 @@ func BindMenu(connector Connector, menu *gtk.Menu) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) {
|
func BindDynamicMenu(connector Connector, constr func(menu *gtk.Menu)) {
|
||||||
connector.Connect("button-press-event", func(_ *gtk.ToggleButton, ev *gdk.Event) {
|
connector.Connect("button-press-event", func(c Connector, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
menu, _ := gtk.MenuNew()
|
menu, _ := gtk.MenuNew()
|
||||||
constr(menu)
|
constr(menu)
|
||||||
|
@ -287,7 +296,7 @@ func InlineCSS(ctx StyleContexter, css string) {
|
||||||
// LeafletOnFold binds a callback to a leaflet that would be called when the
|
// LeafletOnFold binds a callback to a leaflet that would be called when the
|
||||||
// leaflet's folded state changes.
|
// leaflet's folded state changes.
|
||||||
func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) {
|
func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) {
|
||||||
leaflet.ConnectAfter("notify::folded", func() {
|
leaflet.ConnectAfter("notify::folded", func(leaflet *handy.Leaflet) {
|
||||||
foldedFn(leaflet.GetFolded())
|
foldedFn(leaflet.GetFolded())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,13 +85,13 @@ func (a *Avatar) loadFunc(size int) *gdk.Pixbuf {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily resize for now.
|
// // Temporarily resize for now.
|
||||||
p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
|
// p, err := a.pixbuf.ScaleSimple(size, size, gdk.INTERP_HYPER)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
p = a.pixbuf
|
// p = a.pixbuf
|
||||||
}
|
// }
|
||||||
|
|
||||||
return p
|
return a.pixbuf
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRadius is a no-op.
|
// SetRadius is a no-op.
|
||||||
|
|
|
@ -63,7 +63,7 @@ func NewImage(radius float64) (*Image, error) {
|
||||||
// Backup plan if Cairo's Surface is weird.
|
// Backup plan if Cairo's Surface is weird.
|
||||||
|
|
||||||
// var width, height int
|
// var width, height int
|
||||||
// i.Connect("size-allocate", func() {
|
// i.Connect("size-allocate", func(i *gtk.Image) {
|
||||||
// w := i.GetAllocatedWidth()
|
// w := i.GetAllocatedWidth()
|
||||||
// h := i.GetAllocatedHeight()
|
// h := i.GetAllocatedHeight()
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ func (i *Image) SetRadius(r float64) {
|
||||||
i.Radius = r
|
i.Radius = r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Image) drawer(widget gtk.IWidget, cc *cairo.Context) bool {
|
func (i *Image) drawer(_ interface{}, cc *cairo.Context) bool {
|
||||||
var w = float64(i.GetAllocatedWidth())
|
var w = float64(i.GetAllocatedWidth())
|
||||||
var h = float64(i.GetAllocatedHeight())
|
var h = float64(i.GetAllocatedHeight())
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
|
"github.com/gotk3/gotk3/cairo"
|
||||||
"github.com/gotk3/gotk3/gdk"
|
"github.com/gotk3/gotk3/gdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,13 +37,13 @@ func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||||
connector.Connect("enter-notify-event", func() {
|
connector.Connect("enter-notify-event", func(interface{}) {
|
||||||
if s.animation != nil && !s.animating {
|
if s.animation != nil && !s.animating {
|
||||||
s.animating = true
|
s.animating = true
|
||||||
s.Image.SetFromAnimation(s.animation)
|
s.Image.SetFromAnimation(s.animation)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
connector.Connect("leave-notify-event", func() {
|
connector.Connect("leave-notify-event", func(interface{}) {
|
||||||
if s.animation != nil && s.animating {
|
if s.animation != nil && s.animating {
|
||||||
s.animating = false
|
s.animating = false
|
||||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||||
|
@ -60,6 +61,11 @@ func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||||
s.Image.SetFromPixbuf(pb)
|
s.Image.SetFromPixbuf(pb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StaticImage) SetFromSurface(sf *cairo.Surface) {
|
||||||
|
s.animation = nil
|
||||||
|
s.Image.SetFromSurface(sf)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||||
s.animation = anim
|
s.animation = anim
|
||||||
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
||||||
|
|
|
@ -132,7 +132,7 @@ func (i *Icon) AsyncSetIconer(iconer cchat.Iconer, errwrap string) {
|
||||||
return nil, errors.Wrap(err, "failed to load iconer")
|
return nil, errors.Wrap(err, "failed to load iconer")
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() { i.Connect("destroy", f) }, nil
|
return func() { i.Connect("destroy", func(interface{}) { f() }) }, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (l *Label) AsyncSetLabel(fn LabelerFn, info string) {
|
||||||
return nil, errors.Wrap(err, "failed to load iconer")
|
return nil, errors.Wrap(err, "failed to load iconer")
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() { l.Connect("destroy", f) }, nil
|
return func() { l.Connect("destroy", func(interface{}) { f() }) }, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,7 +179,6 @@ func NewPopoverMentioner(rel gtk.IWidget, input string, segment text.Segment) *g
|
||||||
p, _ := gtk.PopoverNew(rel)
|
p, _ := gtk.PopoverNew(rel)
|
||||||
p.Add(box)
|
p.Add(box)
|
||||||
p.SetSizeRequest(PopoverWidth, -1)
|
p.SetSizeRequest(PopoverWidth, -1)
|
||||||
p.Connect("destroy", box.Destroy)
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +215,7 @@ func popoverImg(url string, round bool) gtk.IWidget {
|
||||||
|
|
||||||
btn.SetHAlign(gtk.ALIGN_CENTER)
|
btn.SetHAlign(gtk.ALIGN_CENTER)
|
||||||
btn.SetRelief(gtk.RELIEF_NONE)
|
btn.SetRelief(gtk.RELIEF_NONE)
|
||||||
btn.Connect("clicked", func() { PromptOpen(url) })
|
btn.Connect("clicked", func(*gtk.Button) { PromptOpen(url) })
|
||||||
btn.Show()
|
btn.Show()
|
||||||
|
|
||||||
return btn
|
return btn
|
||||||
|
@ -236,7 +235,7 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
|
||||||
// message, but we're also keeping alive the widget.
|
// message, but we're also keeping alive the widget.
|
||||||
|
|
||||||
var x, y float64
|
var x, y float64
|
||||||
connector.Connect("motion-notify-event", func(w gtk.IWidget, ev *gdk.Event) {
|
connector.Connect("motion-notify-event", func(_ interface{}, ev *gdk.Event) {
|
||||||
x, y = gdk.EventMotionNewFromEvent(ev).MotionVal()
|
x, y = gdk.EventMotionNewFromEvent(ev).MotionVal()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -274,12 +273,11 @@ func bind(connector WidgetConnector, activator func(uri string, r gdk.Rectangle)
|
||||||
btn, _ := gtk.ButtonNew()
|
btn, _ := gtk.ButtonNew()
|
||||||
btn.Add(img)
|
btn.Add(img)
|
||||||
btn.SetRelief(gtk.RELIEF_NONE)
|
btn.SetRelief(gtk.RELIEF_NONE)
|
||||||
btn.Connect("clicked", func() { PromptOpen(uri) })
|
btn.Connect("clicked", func(*gtk.Button) { PromptOpen(uri) })
|
||||||
btn.Show()
|
btn.Show()
|
||||||
|
|
||||||
p, _ := gtk.PopoverNew(c)
|
p, _ := gtk.PopoverNew(c)
|
||||||
p.SetPointingTo(r)
|
p.SetPointingTo(r)
|
||||||
p.Connect("closed", img.Destroy) // on close, destroy image
|
|
||||||
p.Add(btn)
|
p.Add(btn)
|
||||||
p.Popup()
|
p.Popup()
|
||||||
|
|
||||||
|
|
|
@ -40,14 +40,14 @@ func (a *AppendMap) Anchor(start, end int, href string) {
|
||||||
|
|
||||||
// AnchorNU makes a new <a> tag without underlines and colors.
|
// AnchorNU makes a new <a> tag without underlines and colors.
|
||||||
func (a *AppendMap) AnchorNU(start, end int, href string) {
|
func (a *AppendMap) AnchorNU(start, end int, href string) {
|
||||||
a.Openf(start, `<a href="%s">`, html.EscapeString(href))
|
a.Openf(start, `<a href="`+html.EscapeString(href)+`%s">`)
|
||||||
a.Close(end, "</a>")
|
a.Close(end, "</a>")
|
||||||
// a.Anchor(start, end, href)
|
// a.Anchor(start, end, href)
|
||||||
a.Span(start, end, `underline="none"`)
|
a.Span(start, end, `underline="none"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppendMap) Span(start, end int, attrs ...string) {
|
func (a *AppendMap) Span(start, end int, attrs ...string) {
|
||||||
a.Openf(start, "<span %s>", strings.Join(attrs, " "))
|
a.Open(start, "<span "+strings.Join(attrs, " ")+">")
|
||||||
a.Close(end, "</span>")
|
a.Close(end, "</span>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"html"
|
"html"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/attrmap"
|
||||||
|
@ -205,29 +206,37 @@ func colorAttrs(c uint32, bg bool) []string {
|
||||||
rgb, a := splitRGBA(c)
|
rgb, a := splitRGBA(c)
|
||||||
|
|
||||||
// Render the hex representation beforehand.
|
// Render the hex representation beforehand.
|
||||||
hex := fmt.Sprintf("#%06X", rgb)
|
hex := "#" + strconv.FormatUint(uint64(rgb), 16)
|
||||||
|
|
||||||
attrs := make([]string, 1, 4)
|
attrs := make([]string, 1, 4)
|
||||||
attrs[0] = fmt.Sprintf(`color="%s"`, hex)
|
attrs[0] = wrapKeyValue("color", hex)
|
||||||
|
|
||||||
// If we have an alpha that isn't solid (100%), then write it.
|
// If we have an alpha that isn't solid (100%), then write it.
|
||||||
if a < 0xFF {
|
if a < 0xFF {
|
||||||
// Calculate alpha percentage.
|
// Calculate alpha percentage.
|
||||||
perc := a * 100 / 255
|
perc := a * 100 / 255
|
||||||
attrs = append(attrs, fmt.Sprintf(`fgalpha="%d%%"`, perc))
|
attrs = append(attrs, wrapKeyValue("fgalpha", strconv.Itoa(int(perc))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a faded background if we explicitly requested for one.
|
// Draw a faded background if we explicitly requested for one.
|
||||||
if bg {
|
if bg {
|
||||||
// Calculate how faded the background should be for visual purposes.
|
// Calculate how faded the background should be for visual purposes.
|
||||||
perc := a * 10 / 255 // always 10% or less.
|
perc := a * 10 / 255 // always 10% or less.
|
||||||
attrs = append(attrs, fmt.Sprintf(`bgalpha="%d%%"`, perc))
|
attrs = append(attrs, wrapKeyValue("bgalpha", strconv.Itoa(int(perc))))
|
||||||
attrs = append(attrs, fmt.Sprintf(`bgcolor="%s"`, hex))
|
attrs = append(attrs, wrapKeyValue("bgcolor", hex))
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hexPad(c uint32) string {
|
||||||
|
hex := strconv.FormatUint(uint64(c), 16)
|
||||||
|
if len(hex) >= 6 {
|
||||||
|
return hex
|
||||||
|
}
|
||||||
|
return strings.Repeat("0", 6-len(hex)) + hex
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// string constant for formatting width and height in URL fragments
|
// string constant for formatting width and height in URL fragments
|
||||||
f_FragmentSize = "w=%d;h=%d"
|
f_FragmentSize = "w=%d;h=%d"
|
||||||
|
@ -296,6 +305,17 @@ func span(key, value string) string {
|
||||||
return "<span key=\"" + value + "\">"
|
return "<span key=\"" + value + "\">"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wrapKeyValue(key, value string) string {
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.Grow(len(key) + len(value) + 3)
|
||||||
|
buf.WriteString(key)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
buf.WriteByte('"')
|
||||||
|
buf.WriteString(value)
|
||||||
|
buf.WriteByte('"')
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func markupAttr(attr text.Attribute) string {
|
func markupAttr(attr text.Attribute) string {
|
||||||
// meme fast path
|
// meme fast path
|
||||||
if attr == 0 {
|
if attr == 0 {
|
||||||
|
|
|
@ -65,7 +65,7 @@ func NewDialog(name text.Rich, authers []cchat.Authenticator, auth func(cchat.Se
|
||||||
|
|
||||||
d.back, _ = gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON)
|
d.back, _ = gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||||
d.back.Show()
|
d.back.Show()
|
||||||
d.back.Connect("clicked", func() {
|
d.back.Connect("clicked", func(back *gtk.Button) {
|
||||||
// If check just in case.
|
// If check just in case.
|
||||||
if d.stageList != nil {
|
if d.stageList != nil {
|
||||||
d.leaflet.SetVisibleChild(d.stageList)
|
d.leaflet.SetVisibleChild(d.stageList)
|
||||||
|
|
|
@ -110,7 +110,7 @@ func newEntry(k string, conf map[string]string, change func()) *entry {
|
||||||
e, _ := gtk.EntryNew()
|
e, _ := gtk.EntryNew()
|
||||||
e.SetText(conf[k])
|
e.SetText(conf[k])
|
||||||
e.SetHExpand(true)
|
e.SetHExpand(true)
|
||||||
e.Connect("changed", func() {
|
e.Connect("changed", func(e *gtk.Entry) {
|
||||||
conf[k], _ = e.GetText()
|
conf[k], _ = e.GetText()
|
||||||
change()
|
change()
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,7 +18,7 @@ type AppMenu struct {
|
||||||
|
|
||||||
func NewAppMenu() *AppMenu {
|
func NewAppMenu() *AppMenu {
|
||||||
img, _ := gtk.ImageNew()
|
img, _ := gtk.ImageNew()
|
||||||
img.SetFromPixbuf(icons.Logo256(24))
|
img.SetFromSurface(icons.Logo256(24, img.GetScaleFactor()))
|
||||||
img.Show()
|
img.Show()
|
||||||
|
|
||||||
appmenu, _ := gtk.MenuButtonNew()
|
appmenu, _ := gtk.MenuButtonNew()
|
||||||
|
@ -131,7 +131,7 @@ type sizeBinder interface {
|
||||||
var _ sizeBinder = (*List)(nil)
|
var _ sizeBinder = (*List)(nil)
|
||||||
|
|
||||||
func (h *Header) AppMenuBindSize(c sizeBinder) {
|
func (h *Header) AppMenuBindSize(c sizeBinder) {
|
||||||
c.Connect("size-allocate", func() {
|
c.Connect("size-allocate", func(c sizeBinder) {
|
||||||
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
|
h.AppMenu.SetSizeRequest(c.GetAllocatedWidth(), -1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewController interface {
|
type ViewController interface {
|
||||||
|
ClearMessenger(*session.Row)
|
||||||
MessengerSelected(*session.Row, *server.ServerRow)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
SessionSelected(*Service, *session.Row)
|
SessionSelected(*Service, *session.Row)
|
||||||
AuthenticateSession(*List, *Service)
|
AuthenticateSession(*List, *Service)
|
||||||
|
|
|
@ -20,6 +20,8 @@ import (
|
||||||
const IconSize = 48
|
const IconSize = 48
|
||||||
|
|
||||||
type ListController interface {
|
type ListController interface {
|
||||||
|
// ClearMessenger is called when a nil slice of servers is set.
|
||||||
|
ClearMessenger(*session.Row)
|
||||||
// MessengerSelected is called when a server message row is clicked.
|
// MessengerSelected is called when a server message row is clicked.
|
||||||
MessengerSelected(*session.Row, *server.ServerRow)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
// SessionSelected tells the view to change the session view.
|
// SessionSelected tells the view to change the session view.
|
||||||
|
@ -35,6 +37,8 @@ type ListController interface {
|
||||||
|
|
||||||
// Service holds everything that a single service has.
|
// Service holds everything that a single service has.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
ListController
|
||||||
|
|
||||||
*gtk.Box
|
*gtk.Box
|
||||||
Button *gtk.ToggleButton
|
Button *gtk.ToggleButton
|
||||||
Icon *rich.Icon
|
Icon *rich.Icon
|
||||||
|
@ -42,8 +46,7 @@ type Service struct {
|
||||||
BodyRev *gtk.Revealer // revealed
|
BodyRev *gtk.Revealer // revealed
|
||||||
BodyList *session.List // not really supposed to be here
|
BodyList *session.List // not really supposed to be here
|
||||||
|
|
||||||
svclctrl ListController
|
service cchat.Service // state
|
||||||
service cchat.Service // state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceCSS = primitives.PrepareClassCSS("service", `
|
var serviceCSS = primitives.PrepareClassCSS("service", `
|
||||||
|
@ -81,8 +84,8 @@ var serviceIconCSS = primitives.PrepareClassCSS("service-icon", `
|
||||||
|
|
||||||
func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
||||||
service := &Service{
|
service := &Service{
|
||||||
service: svc,
|
service: svc,
|
||||||
svclctrl: svclctrl,
|
ListController: svclctrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
service.BodyList = session.NewList(service)
|
service.BodyList = session.NewList(service)
|
||||||
|
@ -148,11 +151,11 @@ func (s *Service) GetRevealChild() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SessionSelected(srow *session.Row) {
|
func (s *Service) SessionSelected(srow *session.Row) {
|
||||||
s.svclctrl.SessionSelected(s, srow)
|
s.ListController.SessionSelected(s, srow)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AuthenticateSession() {
|
func (s *Service) AuthenticateSession() {
|
||||||
s.svclctrl.AuthenticateSession(s)
|
s.ListController.AuthenticateSession(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddLoadingSession(id, name string) *session.Row {
|
func (s *Service) AddLoadingSession(id, name string) *session.Row {
|
||||||
|
@ -196,15 +199,11 @@ func (s *Service) OnSessionDisconnect(row *session.Row) {
|
||||||
s.BodyList.UnselectAll()
|
s.BodyList.UnselectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.svclctrl.OnSessionDisconnect(s, row)
|
s.ListController.OnSessionDisconnect(s, row)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) MessengerSelected(r *session.Row, sv *server.ServerRow) {
|
|
||||||
s.svclctrl.MessengerSelected(r, sv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) RemoveSession(row *session.Row) {
|
func (s *Service) RemoveSession(row *session.Row) {
|
||||||
s.svclctrl.OnSessionRemove(s, row)
|
s.ListController.OnSessionRemove(s, row)
|
||||||
s.BodyList.RemoveSessionRow(row.ID())
|
s.BodyList.RemoveSessionRow(row.ID())
|
||||||
s.SaveAllSessions()
|
s.SaveAllSessions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
const UnreadColorDefs = `
|
const UnreadColorDefs = `
|
||||||
|
@ -61,10 +62,14 @@ func WrapToggleButtonImage(b *rich.ToggleButtonImage) *ToggleButtonImage {
|
||||||
|
|
||||||
tb := &ToggleButtonImage{
|
tb := &ToggleButtonImage{
|
||||||
ToggleButtonImage: b,
|
ToggleButtonImage: b,
|
||||||
|
clicked: func(bool) {},
|
||||||
clicked: func(bool) {},
|
|
||||||
}
|
}
|
||||||
tb.Connect("clicked", func() { tb.clicked(tb.GetActive()) })
|
|
||||||
|
type activeGetter interface {
|
||||||
|
GetActive() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.Connect("clicked", func(w *gtk.ToggleButton) { tb.clicked(w.GetActive()) })
|
||||||
serverButtonCSS(tb)
|
serverButtonCSS(tb)
|
||||||
|
|
||||||
return tb
|
return tb
|
||||||
|
|
|
@ -96,7 +96,7 @@ func (c *Children) Reset() {
|
||||||
if row.IsHollow() {
|
if row.IsHollow() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.Box.Remove(row)
|
row.Destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,38 +141,40 @@ func (c *Children) setNotLoading() {
|
||||||
// Do we have the spinning circle button? If yes, remove it.
|
// Do we have the spinning circle button? If yes, remove it.
|
||||||
if c.load != nil {
|
if c.load != nil {
|
||||||
// Stop the loading mode. The reset function should do everything for us.
|
// Stop the loading mode. The reset function should do everything for us.
|
||||||
c.Box.Remove(c.load)
|
c.load.Destroy()
|
||||||
c.load = nil
|
c.load = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetServers is reserved for cchat.ServersContainer.
|
// SetServers is reserved for cchat.ServersContainer.
|
||||||
func (c *Children) SetServers(servers []cchat.Server) {
|
func (c *Children) SetServers(servers []cchat.Server) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() { c.SetServersUnsafe(servers) })
|
||||||
// Save the current state (if any) if the children container is not
|
}
|
||||||
// hollow.
|
|
||||||
if !c.IsHollow() {
|
func (c *Children) SetServersUnsafe(servers []cchat.Server) {
|
||||||
restore := c.saveSelectedRow()
|
// Save the current state (if any) if the children container is not
|
||||||
defer restore()
|
// hollow.
|
||||||
|
if !c.IsHollow() {
|
||||||
|
restore := c.saveSelectedRow()
|
||||||
|
defer restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset before inserting new servers.
|
||||||
|
c.Reset()
|
||||||
|
|
||||||
|
// Insert hollow servers.
|
||||||
|
c.Rows = make([]*ServerRow, len(servers))
|
||||||
|
for i, server := range servers {
|
||||||
|
if server == nil {
|
||||||
|
log.Panicln("one of given servers in SetServers is nil at ", i)
|
||||||
}
|
}
|
||||||
|
c.Rows[i] = NewHollowServer(c, server, c.rowctrl)
|
||||||
|
}
|
||||||
|
|
||||||
// Reset before inserting new servers.
|
// We should not unhollow everything here, but rather on uncollapse.
|
||||||
c.Reset()
|
// Since the root node is always unhollow, calls to this function will
|
||||||
|
// pass the hollow test and unhollow its children nodes. That should not
|
||||||
// Insert hollow servers.
|
// happen.
|
||||||
c.Rows = make([]*ServerRow, len(servers))
|
|
||||||
for i, server := range servers {
|
|
||||||
if server == nil {
|
|
||||||
log.Panicln("one of given servers in SetServers is nil at ", i)
|
|
||||||
}
|
|
||||||
c.Rows[i] = NewHollowServer(c, server, c.rowctrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not unhollow everything here, but rather on uncollapse.
|
|
||||||
// Since the root node is always unhollow, calls to this function will
|
|
||||||
// pass the hollow test and unhollow its children nodes. That should not
|
|
||||||
// happen.
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Children) findID(id cchat.ID) (int, *ServerRow) {
|
func (c *Children) findID(id cchat.ID) (int, *ServerRow) {
|
||||||
|
@ -196,35 +198,37 @@ func (c *Children) insertAt(row *ServerRow, i int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Children) UpdateServer(update cchat.ServerUpdate) {
|
func (c *Children) UpdateServer(update cchat.ServerUpdate) {
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() { c.UpdateServerUnsafe(update) })
|
||||||
prevID, replace := update.PreviousID()
|
}
|
||||||
|
|
||||||
// TODO: I don't think this code unhollows a new server.
|
func (c *Children) UpdateServerUnsafe(update cchat.ServerUpdate) {
|
||||||
var newServer = NewHollowServer(c, update, c.rowctrl)
|
prevID, replace := update.PreviousID()
|
||||||
var i, oldRow = c.findID(prevID)
|
|
||||||
|
|
||||||
// If we're appending a new row, then replace is false.
|
// TODO: I don't think this code unhollows a new server.
|
||||||
if !replace {
|
var newServer = NewHollowServer(c, update, c.rowctrl)
|
||||||
// Increment the old row's index so we know where to insert.
|
var i, oldRow = c.findID(prevID)
|
||||||
c.insertAt(newServer, i+1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only update the server if the old row was found.
|
// If we're appending a new row, then replace is false.
|
||||||
if oldRow == nil {
|
if !replace {
|
||||||
return
|
// Increment the old row's index so we know where to insert.
|
||||||
}
|
c.insertAt(newServer, i+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Rows[i] = newServer
|
// Only update the server if the old row was found.
|
||||||
|
if oldRow == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !c.IsHollow() {
|
c.Rows[i] = newServer
|
||||||
// Update the UI as well.
|
|
||||||
// TODO: check if this reorder is correct.
|
if !c.IsHollow() {
|
||||||
c.Box.Remove(oldRow)
|
// Update the UI as well.
|
||||||
c.Box.Add(newServer)
|
// TODO: check if this reorder is correct.
|
||||||
c.Box.ReorderChild(newServer, i)
|
oldRow.Destroy()
|
||||||
}
|
c.Box.Add(newServer)
|
||||||
})
|
c.Box.ReorderChild(newServer, i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAll forces all children rows to be unhollowed (initialized). It does
|
// LoadAll forces all children rows to be unhollowed (initialized). It does
|
||||||
|
|
|
@ -135,7 +135,7 @@ func (r *ServerRow) Init() {
|
||||||
r.SetIconer(r.Server)
|
r.SetIconer(r.Server)
|
||||||
|
|
||||||
// Connect the destroyer, if any.
|
// Connect the destroyer, if any.
|
||||||
r.Connect("destroy", r.cancelUnread)
|
r.Connect("destroy", func(interface{}) { r.cancelUnread() })
|
||||||
|
|
||||||
// Restore the read state.
|
// Restore the read state.
|
||||||
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
r.Button.SetUnreadUnsafe(r.unread, r.mentioned) // update with state
|
||||||
|
@ -269,11 +269,8 @@ func (r *ServerRow) load(finish func(error)) {
|
||||||
// Reset clears off all children servers. It's a no-op if there are none.
|
// Reset clears off all children servers. It's a no-op if there are none.
|
||||||
func (r *ServerRow) Reset() {
|
func (r *ServerRow) Reset() {
|
||||||
if r.children != nil {
|
if r.children != nil {
|
||||||
// Remove everything from the children container.
|
|
||||||
r.children.Reset()
|
r.children.Reset()
|
||||||
|
r.children.Destroy()
|
||||||
// Remove the children container itself.
|
|
||||||
r.Box.Remove(r.children)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the state.
|
// Reset the state.
|
||||||
|
|
|
@ -17,6 +17,13 @@ import (
|
||||||
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
|
const FaceSize = 48 // gtk.ICON_SIZE_DIALOG
|
||||||
const ListWidth = 200
|
const ListWidth = 200
|
||||||
|
|
||||||
|
// SessionController extends server.Controller to add needed methods that the
|
||||||
|
// specific top-level servers container needs.
|
||||||
|
type SessionController interface {
|
||||||
|
server.Controller
|
||||||
|
ClearMessenger()
|
||||||
|
}
|
||||||
|
|
||||||
// Servers wraps around a list of servers inherited from Children. It's the
|
// Servers wraps around a list of servers inherited from Children. It's the
|
||||||
// container that's displayed on the right of the service sidebar.
|
// container that's displayed on the right of the service sidebar.
|
||||||
type Servers struct {
|
type Servers struct {
|
||||||
|
@ -24,6 +31,8 @@ type Servers struct {
|
||||||
Children *server.Children
|
Children *server.Children
|
||||||
spinner *spinner.Boxed // non-nil if loading.
|
spinner *spinner.Boxed // non-nil if loading.
|
||||||
|
|
||||||
|
ctrl SessionController
|
||||||
|
|
||||||
// state
|
// state
|
||||||
ServerList cchat.Lister
|
ServerList cchat.Lister
|
||||||
}
|
}
|
||||||
|
@ -35,7 +44,7 @@ var toplevelCSS = primitives.PrepareClassCSS("top-level", `
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
func NewServers(p traverse.Breadcrumber, ctrl server.Controller) *Servers {
|
func NewServers(p traverse.Breadcrumber, ctrl SessionController) *Servers {
|
||||||
c := server.NewChildren(p, ctrl)
|
c := server.NewChildren(p, ctrl)
|
||||||
c.SetMarginStart(0) // children is top level; there is no main row
|
c.SetMarginStart(0) // children is top level; there is no main row
|
||||||
c.SetVExpand(true)
|
c.SetVExpand(true)
|
||||||
|
@ -47,6 +56,7 @@ func NewServers(p traverse.Breadcrumber, ctrl server.Controller) *Servers {
|
||||||
return &Servers{
|
return &Servers{
|
||||||
Box: b,
|
Box: b,
|
||||||
Children: c,
|
Children: c,
|
||||||
|
ctrl: ctrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +98,7 @@ func (s *Servers) load() {
|
||||||
s.setLoading()
|
s.setLoading()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := s.ServerList.Servers(s.Children)
|
err := s.ServerList.Servers(s)
|
||||||
gts.ExecAsync(func() {
|
gts.ExecAsync(func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.setFailed(err)
|
s.setFailed(err)
|
||||||
|
@ -99,6 +109,22 @@ func (s *Servers) load() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetServers is reserved for cchat.ServersContainer.
|
||||||
|
func (s *Servers) SetServers(servers []cchat.Server) {
|
||||||
|
gts.ExecAsync(func() {
|
||||||
|
s.Children.SetServersUnsafe(servers)
|
||||||
|
|
||||||
|
if servers == nil {
|
||||||
|
s.ctrl.ClearMessenger()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetServers is reserved for cchat.ServersContainer.
|
||||||
|
func (s *Servers) UpdateServer(update cchat.ServerUpdate) {
|
||||||
|
gts.ExecAsync(func() { s.Children.UpdateServerUnsafe(update) })
|
||||||
|
}
|
||||||
|
|
||||||
// setDone changes the view to show the servers.
|
// setDone changes the view to show the servers.
|
||||||
func (s *Servers) setDone() {
|
func (s *Servers) setDone() {
|
||||||
primitives.RemoveChildren(s)
|
primitives.RemoveChildren(s)
|
||||||
|
@ -139,7 +165,7 @@ func (s *Servers) setFailed(err error) {
|
||||||
// Create a retry button.
|
// Create a retry button.
|
||||||
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_DIALOG)
|
btn, _ := gtk.ButtonNewFromIconName("view-refresh-symbolic", gtk.ICON_SIZE_DIALOG)
|
||||||
btn.Show()
|
btn.Show()
|
||||||
btn.Connect("clicked", s.load)
|
btn.Connect("clicked", func(interface{}) { s.load() })
|
||||||
|
|
||||||
// Create a bottom label for the error itself.
|
// Create a bottom label for the error itself.
|
||||||
lerr, _ := gtk.LabelNew("")
|
lerr, _ := gtk.LabelNew("")
|
||||||
|
|
|
@ -22,16 +22,23 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Servicer extends server.RowController to add session.
|
|
||||||
type Servicer interface {
|
type Servicer interface {
|
||||||
// Service asks the controller for its service.
|
// Service asks the controller for its service.
|
||||||
Service() cchat.Service
|
Service() cchat.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller extends server.Controller to add session parameters.
|
||||||
|
type Controller interface {
|
||||||
|
Servicer
|
||||||
|
|
||||||
// OnSessionDisconnect is called before a session is disconnected. This
|
// OnSessionDisconnect is called before a session is disconnected. This
|
||||||
// function is used for cleanups.
|
// function is used for cleanups.
|
||||||
OnSessionDisconnect(*Row)
|
OnSessionDisconnect(*Row)
|
||||||
// SessionSelected is called when the row is clicked. The parent container
|
// SessionSelected is called when the row is clicked. The parent container
|
||||||
// should change the views to show this session's *Servers.
|
// should change the views to show this session's *Servers.
|
||||||
SessionSelected(*Row)
|
SessionSelected(*Row)
|
||||||
|
// ClearMessenger is called when a nil slice of servers is set.
|
||||||
|
ClearMessenger(*Row)
|
||||||
// MessengerSelected is called when a server that can display messages (aka
|
// MessengerSelected is called when a server that can display messages (aka
|
||||||
// implements Messenger) is called.
|
// implements Messenger) is called.
|
||||||
MessengerSelected(*Row, *server.ServerRow)
|
MessengerSelected(*Row, *server.ServerRow)
|
||||||
|
@ -53,13 +60,13 @@ type Row struct {
|
||||||
iconBox *gtk.EventBox
|
iconBox *gtk.EventBox
|
||||||
icon *rich.Icon // nillable
|
icon *rich.Icon // nillable
|
||||||
|
|
||||||
|
ctrl Controller
|
||||||
parentcrumb traverse.Breadcrumber
|
parentcrumb traverse.Breadcrumber
|
||||||
|
|
||||||
Session cchat.Session // state; nilable
|
Session cchat.Session // state; nilable
|
||||||
sessionID string
|
sessionID string
|
||||||
|
|
||||||
Servers *Servers // accessed by View for the right view
|
Servers *Servers // accessed by View for the right view
|
||||||
svcctrl Servicer
|
|
||||||
|
|
||||||
ActionsMenu *actions.Menu // session.*
|
ActionsMenu *actions.Menu // session.*
|
||||||
|
|
||||||
|
@ -129,22 +136,22 @@ func newIcon(img rich.RoundIconContainer) *rich.Icon {
|
||||||
return icon
|
return icon
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Servicer) *Row {
|
func New(parent traverse.Breadcrumber, ses cchat.Session, ctrl Controller) *Row {
|
||||||
row := newRow(parent, text.Rich{}, ctrl)
|
row := newRow(parent, text.Rich{}, ctrl)
|
||||||
row.SetSession(ses)
|
row.SetSession(ses)
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Servicer) *Row {
|
func NewLoading(parent traverse.Breadcrumber, id, name string, ctrl Controller) *Row {
|
||||||
row := newRow(parent, text.Rich{Content: name}, ctrl)
|
row := newRow(parent, text.Rich{Content: name}, ctrl)
|
||||||
row.sessionID = id
|
row.sessionID = id
|
||||||
row.SetLoading()
|
row.SetLoading()
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
|
func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Controller) *Row {
|
||||||
row := &Row{
|
row := &Row{
|
||||||
svcctrl: ctrl,
|
ctrl: ctrl,
|
||||||
parentcrumb: parent,
|
parentcrumb: parent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +176,7 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
|
||||||
row.ActionsMenu.InsertActionGroup(row)
|
row.ActionsMenu.InsertActionGroup(row)
|
||||||
|
|
||||||
// Bind right clicks and show a popover menu on such event.
|
// Bind right clicks and show a popover menu on such event.
|
||||||
row.iconBox.Connect("button-press-event", func(_ gtk.IWidget, ev *gdk.Event) {
|
row.iconBox.Connect("button-press-event", func(_ interface{}, ev *gdk.Event) {
|
||||||
if gts.EventIsRightClick(ev) {
|
if gts.EventIsRightClick(ev) {
|
||||||
row.ActionsMenu.Popup(row)
|
row.ActionsMenu.Popup(row)
|
||||||
}
|
}
|
||||||
|
@ -242,6 +249,10 @@ func (r *Row) Breadcrumb() string {
|
||||||
return r.Session.Name().Content
|
return r.Session.Name().Content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Row) ClearMessenger() {
|
||||||
|
r.ctrl.ClearMessenger(r)
|
||||||
|
}
|
||||||
|
|
||||||
// Activate executes whatever needs to be done. If the row has failed, then this
|
// Activate executes whatever needs to be done. If the row has failed, then this
|
||||||
// method will reconnect. If the row is already loaded, then SessionSelected
|
// method will reconnect. If the row is already loaded, then SessionSelected
|
||||||
// will be called.
|
// will be called.
|
||||||
|
@ -257,7 +268,7 @@ func (r *Row) Activate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display the empty server list first, then try and reconnect.
|
// Display the empty server list first, then try and reconnect.
|
||||||
r.svcctrl.SessionSelected(r)
|
r.ctrl.SessionSelected(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLoading sets the session button to have a spinner circle. DO NOT CONFUSE
|
// SetLoading sets the session button to have a spinner circle. DO NOT CONFUSE
|
||||||
|
@ -371,13 +382,13 @@ func (r *Row) SetSession(ses cchat.Session) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Row) MessengerSelected(sr *server.ServerRow) {
|
func (r *Row) MessengerSelected(sr *server.ServerRow) {
|
||||||
r.svcctrl.MessengerSelected(r, sr)
|
r.ctrl.MessengerSelected(r, sr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSession removes itself from the session list.
|
// RemoveSession removes itself from the session list.
|
||||||
func (r *Row) RemoveSession() {
|
func (r *Row) RemoveSession() {
|
||||||
// Remove the session off the list.
|
// Remove the session off the list.
|
||||||
r.svcctrl.RemoveSession(r)
|
r.ctrl.RemoveSession(r)
|
||||||
|
|
||||||
var session = r.Session
|
var session = r.Session
|
||||||
if session == nil {
|
if session == nil {
|
||||||
|
@ -404,7 +415,7 @@ func (r *Row) ReconnectSession() {
|
||||||
// Set the row as loading.
|
// Set the row as loading.
|
||||||
r.SetLoading()
|
r.SetLoading()
|
||||||
// Try to restore the session.
|
// Try to restore the session.
|
||||||
r.svcctrl.RestoreSession(r, r.sessionID)
|
r.ctrl.RestoreSession(r, r.sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisconnectSession disconnects the current session. It does nothing if the row
|
// DisconnectSession disconnects the current session. It does nothing if the row
|
||||||
|
@ -416,7 +427,7 @@ func (r *Row) DisconnectSession() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the disconnect function from the controller first.
|
// Call the disconnect function from the controller first.
|
||||||
r.svcctrl.OnSessionDisconnect(r)
|
r.ctrl.OnSessionDisconnect(r)
|
||||||
|
|
||||||
// Copy the session to avoid data race and allow us to reset.
|
// Copy the session to avoid data race and allow us to reset.
|
||||||
session := r.Session
|
session := r.Session
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
type Controller interface {
|
type Controller interface {
|
||||||
// SessionSelected is called when
|
// SessionSelected is called when
|
||||||
SessionSelected(svc *Service, srow *session.Row)
|
SessionSelected(svc *Service, srow *session.Row)
|
||||||
|
// ClearMessenger is called when a nil slice of servers is set.
|
||||||
|
ClearMessenger(*session.Row)
|
||||||
// MessengerSelected is wrapped around session's MessengerSelected.
|
// MessengerSelected is wrapped around session's MessengerSelected.
|
||||||
MessengerSelected(*session.Row, *server.ServerRow)
|
MessengerSelected(*session.Row, *server.ServerRow)
|
||||||
// AuthenticateSession is called to spawn the authentication dialog.
|
// AuthenticateSession is called to spawn the authentication dialog.
|
||||||
|
|
|
@ -142,12 +142,20 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
|
||||||
app.MessageView.Reset()
|
app.MessageView.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *App) ClearMessenger(ses *session.Row) {
|
||||||
|
if app.MessageView.SessionID() == ses.Session.ID() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) {
|
func (app *App) MessengerSelected(ses *session.Row, srv *server.ServerRow) {
|
||||||
// Change to the message view.
|
// Change to the message view.
|
||||||
app.Leaflet.SetVisibleChild(app.MessageView)
|
app.Leaflet.SetVisibleChild(app.MessageView)
|
||||||
|
|
||||||
// Assert that the new server is not the same one.
|
// Assert that the new server is not the same one.
|
||||||
if app.MessageView.ServerID() == srv.Server.ID() {
|
if app.MessageView.SessionID() == ses.Session.ID() &&
|
||||||
|
app.MessageView.ServerID() == srv.Server.ID() {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +218,7 @@ func (app *App) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) Icon() *gdk.Pixbuf {
|
func (app *App) Icon() *gdk.Pixbuf {
|
||||||
return icons.Logo256(0)
|
return icons.Logo256Pixbuf()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) Menu() *glib.MenuModel {
|
func (app *App) Menu() *glib.MenuModel {
|
||||||
|
|
|
@ -6,10 +6,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Inject madvdontneed=1 as soon as possible.
|
// Inject madvdontneed=1 as soon as possible.
|
||||||
|
@ -39,16 +36,3 @@ var _ = func() struct{} {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return struct{}{}
|
return struct{}{}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Aggressive memory freeing you asked, so aggressive memory freeing we will
|
|
||||||
// deliver.
|
|
||||||
if strings.Contains(os.Getenv("GODEBUG"), "madvdontneed=1") {
|
|
||||||
go func() {
|
|
||||||
log.Println("Now attempting to free memory every 5s... (madvdontneed=1)")
|
|
||||||
for range time.Tick(5 * time.Second) {
|
|
||||||
debug.FreeOSMemory()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
18
main.go
18
main.go
|
@ -1,16 +1,31 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui"
|
"github.com/diamondburned/cchat-gtk/internal/ui"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||||
"github.com/diamondburned/cchat/services"
|
"github.com/diamondburned/cchat/services"
|
||||||
|
|
||||||
|
// _ "github.com/diamondburned/gotk3-tcmalloc"
|
||||||
|
// "github.com/diamondburned/gotk3-tcmalloc/heapprofiler"
|
||||||
|
|
||||||
_ "github.com/diamondburned/cchat-discord"
|
_ "github.com/diamondburned/cchat-discord"
|
||||||
_ "github.com/diamondburned/cchat-mock"
|
_ "github.com/diamondburned/cchat-mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go func() {
|
||||||
|
// If you GC more, you have shorter STWs. Easy.
|
||||||
|
for range time.Tick(time.Second) {
|
||||||
|
runtime.GC()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
gts.Main(func() gts.MainApplication {
|
gts.Main(func() gts.MainApplication {
|
||||||
var app = ui.NewApplication()
|
var app = ui.NewApplication()
|
||||||
|
@ -31,6 +46,9 @@ func main() {
|
||||||
// Restore the configs.
|
// Restore the configs.
|
||||||
config.Restore()
|
config.Restore()
|
||||||
|
|
||||||
|
// heapprofiler.Start("/tmp/cchat-gtk")
|
||||||
|
// gts.App.Window.Window.Connect("destroy", heapprofiler.Stop)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
44
shell.nix
44
shell.nix
|
@ -1,28 +1,42 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }:
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
let libhandy = pkgs.libhandy.overrideAttrs(old: {
|
let nostrip = pkg: pkgs.enableDebugging (pkg.overrideAttrs(old: {
|
||||||
name = "libhandy-1.0.1";
|
dontStrip = true;
|
||||||
src = builtins.fetchGit {
|
doCheck = false;
|
||||||
url = "https://gitlab.gnome.org/GNOME/libhandy.git";
|
NIX_CFLAGS_COMPILE = (old.NIX_CFLAGS_COMPILE or "") + " -g";
|
||||||
rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6";
|
}));
|
||||||
};
|
|
||||||
patches = [];
|
|
||||||
|
|
||||||
buildInputs = old.buildInputs ++ (with pkgs; [
|
libhandy = pkgs.libhandy.overrideAttrs(old: {
|
||||||
gnome3.librsvg
|
name = "libhandy-1.0.1";
|
||||||
gdk-pixbuf
|
src = builtins.fetchGit {
|
||||||
]);
|
url = "https://gitlab.gnome.org/GNOME/libhandy.git";
|
||||||
});
|
rev = "5cee0927b8b39dea1b2a62ec6d19169f73ba06c6";
|
||||||
|
};
|
||||||
|
patches = [];
|
||||||
|
|
||||||
|
buildInputs = old.buildInputs ++ (with pkgs; [
|
||||||
|
(nostrip gnome3.librsvg)
|
||||||
|
(nostrip gdk-pixbuf)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
in pkgs.stdenv.mkDerivation rec {
|
in pkgs.stdenv.mkDerivation rec {
|
||||||
name = "cchat-gtk";
|
name = "cchat-gtk";
|
||||||
version = "0.0.2";
|
version = "0.0.2";
|
||||||
|
|
||||||
buildInputs = [ libhandy ] ++ (with pkgs; [
|
buildInputs = [
|
||||||
gnome3.gspell gnome3.glib gnome3.gtk
|
(nostrip libhandy)
|
||||||
]);
|
(nostrip pkgs.gnome3.gspell)
|
||||||
|
(nostrip pkgs.gnome3.glib)
|
||||||
|
(nostrip pkgs.gnome3.gtk)
|
||||||
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
pkgconfig go
|
pkgconfig go
|
||||||
|
gperftools
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Debug flags.
|
||||||
|
CGO_CFLAGS = "-g";
|
||||||
|
CGO_CXXFLAGS = "-g";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue