mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-22 01:46:47 +00:00
Added Leaflet and partial Avatar support
This commit is contained in:
parent
0b155d5b07
commit
b48526aed3
2
go.mod
2
go.mod
|
@ -10,7 +10,7 @@ require (
|
|||
github.com/diamondburned/cchat v0.0.49
|
||||
github.com/diamondburned/cchat-discord v0.0.0-20200821041521-647c854d7b5e
|
||||
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b
|
||||
github.com/diamondburned/handy v0.0.0-20200827040421-5b4a15843526
|
||||
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
|
||||
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/goodsign/monday v1.0.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -91,6 +91,8 @@ github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a h1:wEldljb421/
|
|||
github.com/diamondburned/gotk3 v0.0.0-20200816224505-3cd69b83a48a/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||
github.com/diamondburned/handy v0.0.0-20200827040421-5b4a15843526 h1:GnTwaD+RKiCJiJ9yhcUb6M5o4VwFExTqmcl8Dg+EVdw=
|
||||
github.com/diamondburned/handy v0.0.0-20200827040421-5b4a15843526/go.mod h1:V0qyhW4v6KPFwtDpXdBm5aWH7zWEyrzZpcB6MPnKArQ=
|
||||
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/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/ningen v0.1.1-0.20200717072304-e483f86c08e6 h1:YN0cj0aOCa+tKmx0aD5qsbSYaIJnyrA0/+eygMKP+/w=
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
)
|
||||
|
||||
// const BreadcrumbSlash = `<span rise="-1024" size="x-large">❭</span>`
|
||||
|
@ -16,30 +17,76 @@ const BreadcrumbSlash = " 〉"
|
|||
type Header struct {
|
||||
handy.HeaderBar
|
||||
|
||||
Breadcrumb *gtk.Label
|
||||
ShowBackBtn *gtk.Revealer
|
||||
BackButton *gtk.Button
|
||||
Breadcrumb *gtk.Label
|
||||
ShowMembers *gtk.ToggleButton
|
||||
|
||||
breadcrumbs []string
|
||||
minicrumbs bool
|
||||
}
|
||||
|
||||
var backButtonCSS = primitives.PrepareClassCSS("back-button", `
|
||||
.back-button {
|
||||
margin-left: 14px;
|
||||
}
|
||||
`)
|
||||
|
||||
var rightBreadcrumbCSS = primitives.PrepareClassCSS("right-breadcrumb", `
|
||||
.right-breadcrumb {
|
||||
margin: 0 14px;
|
||||
margin-left: 14px;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewHeader() *Header {
|
||||
bk, _ := gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||
bk.SetVAlign(gtk.ALIGN_CENTER)
|
||||
bk.Show()
|
||||
backButtonCSS(bk)
|
||||
|
||||
rbk, _ := gtk.RevealerNew()
|
||||
rbk.Add(bk)
|
||||
rbk.SetRevealChild(false)
|
||||
rbk.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_LEFT)
|
||||
rbk.SetTransitionDuration(50)
|
||||
rbk.Show()
|
||||
|
||||
bc, _ := gtk.LabelNew(BreadcrumbSlash)
|
||||
bc.SetUseMarkup(true)
|
||||
bc.SetXAlign(0.0)
|
||||
bc.SetEllipsize(pango.ELLIPSIZE_MIDDLE)
|
||||
bc.SetSingleLineMode(true)
|
||||
bc.SetHExpand(true)
|
||||
bc.SetMaxWidthChars(75)
|
||||
bc.Show()
|
||||
rightBreadcrumbCSS(bc)
|
||||
|
||||
memberIcon, _ := gtk.ImageNewFromIconName("system-users-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||
memberIcon.Show()
|
||||
|
||||
mb, _ := gtk.ToggleButtonNew()
|
||||
mb.SetVAlign(gtk.ALIGN_CENTER)
|
||||
mb.SetImage(memberIcon)
|
||||
mb.SetActive(false)
|
||||
mb.SetSensitive(false)
|
||||
|
||||
header := handy.HeaderBarNew()
|
||||
header.SetShowCloseButton(true)
|
||||
header.PackStart(rbk)
|
||||
header.PackStart(bc)
|
||||
header.PackEnd(mb)
|
||||
header.Show()
|
||||
|
||||
// Hack to hide the title.
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
header.SetCustomTitle(b)
|
||||
|
||||
return &Header{
|
||||
HeaderBar: *header,
|
||||
Breadcrumb: bc,
|
||||
HeaderBar: *header,
|
||||
ShowBackBtn: rbk,
|
||||
BackButton: bk,
|
||||
Breadcrumb: bc,
|
||||
ShowMembers: mb,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,18 +94,71 @@ func (h *Header) Reset() {
|
|||
h.SetBreadcrumber(nil)
|
||||
}
|
||||
|
||||
func (h *Header) OnBackPressed(fn func()) {
|
||||
h.BackButton.Connect("clicked", fn)
|
||||
}
|
||||
|
||||
func (h *Header) OnShowMembersToggle(fn func(show bool)) {
|
||||
h.ShowMembers.Connect("toggled", func() {
|
||||
fn(h.ShowMembers.GetActive())
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Header) SetShowBackButton(show bool) {
|
||||
h.ShowBackBtn.SetRevealChild(show)
|
||||
}
|
||||
|
||||
func (h *Header) SetCanShowMembers(canShow bool) {
|
||||
if canShow {
|
||||
h.ShowMembers.Show()
|
||||
h.ShowMembers.SetSensitive(true)
|
||||
} else {
|
||||
h.ShowMembers.Hide()
|
||||
h.ShowMembers.SetSensitive(false)
|
||||
}
|
||||
}
|
||||
|
||||
// SetMiniBreadcrumb sets whether or not the breadcrumb should display the full
|
||||
// label.
|
||||
func (h *Header) SetMiniBreadcrumb(mini bool) {
|
||||
h.minicrumbs = mini
|
||||
h.updateBreadcrumb()
|
||||
}
|
||||
|
||||
// updateBreadcrumb updates the breadcrumb label from the local state.
|
||||
func (h *Header) updateBreadcrumb() {
|
||||
switch {
|
||||
case len(h.breadcrumbs) == 0:
|
||||
h.Breadcrumb.SetText("")
|
||||
|
||||
case h.minicrumbs:
|
||||
h.Breadcrumb.SetMarkup(h.breadcrumbs[len(h.breadcrumbs)-1])
|
||||
|
||||
default:
|
||||
h.Breadcrumb.SetMarkup(
|
||||
BreadcrumbSlash + " " + strings.Join(h.breadcrumbs, " "+BreadcrumbSlash+" "),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Header) SetBreadcrumber(b traverse.Breadcrumber) {
|
||||
if b == nil {
|
||||
h.Breadcrumb.SetText("")
|
||||
h.breadcrumbs = nil
|
||||
h.updateBreadcrumb()
|
||||
return
|
||||
}
|
||||
|
||||
var crumb = b.Breadcrumb()
|
||||
for i := range crumb {
|
||||
crumb[i] = html.EscapeString(crumb[i])
|
||||
h.breadcrumbs = b.Breadcrumb()
|
||||
if len(h.breadcrumbs) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
h.Breadcrumb.SetMarkup(
|
||||
BreadcrumbSlash + " " + strings.Join(crumb, " "+BreadcrumbSlash+" "),
|
||||
)
|
||||
// Skip the service name and username.
|
||||
h.breadcrumbs = h.breadcrumbs[2:]
|
||||
|
||||
for i := range h.breadcrumbs {
|
||||
h.breadcrumbs[i] = html.EscapeString(h.breadcrumbs[i])
|
||||
}
|
||||
|
||||
h.updateBreadcrumb()
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ var textCSS = primitives.PrepareCSS(`
|
|||
}
|
||||
|
||||
.message-input, .message-input * {
|
||||
background-color: transparent;
|
||||
background-color: mix(@theme_bg_color, @theme_fg_color, 0.03);
|
||||
}
|
||||
|
||||
.message-input * {
|
||||
|
@ -47,6 +47,12 @@ var textCSS = primitives.PrepareCSS(`
|
|||
}
|
||||
`)
|
||||
|
||||
var inputBoxCSS = primitives.PrepareClassCSS("input-box", `
|
||||
.input-box {
|
||||
background-color: @theme_bg_color;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewView(ctrl Controller) *InputView {
|
||||
text, _ := gtk.TextViewNew()
|
||||
text.SetSensitive(false)
|
||||
|
@ -68,8 +74,6 @@ func NewView(ctrl Controller) *InputView {
|
|||
f := NewField(text, ctrl)
|
||||
f.Show()
|
||||
|
||||
primitives.AddClass(f, "input-field")
|
||||
|
||||
return &InputView{f, c}
|
||||
}
|
||||
|
||||
|
@ -120,8 +124,17 @@ func (s *fieldState) Reset() {
|
|||
*s = fieldState{}
|
||||
}
|
||||
|
||||
var inputFieldCSS = primitives.PrepareCSS(`
|
||||
.input-field { margin: 3px 5px }
|
||||
var inputFieldCSS = primitives.PrepareClassCSS("input-field", `
|
||||
.input-field {
|
||||
margin: 3px 5px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
`)
|
||||
|
||||
var scrolledInputCSS = primitives.PrepareClassCSS("scrolled-input", `
|
||||
.scrolled-input {
|
||||
margin: 0 5px;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
||||
|
@ -133,7 +146,7 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
|
||||
field.TextScroll = scrollinput.NewV(text, 150)
|
||||
field.TextScroll.Show()
|
||||
primitives.AddClass(field.TextScroll, "scrolled-input")
|
||||
scrolledInputCSS(field.TextScroll)
|
||||
|
||||
field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
|
||||
field.attach.SetRelief(gtk.RELIEF_NONE)
|
||||
|
@ -146,15 +159,13 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
field.send.Show()
|
||||
primitives.AddClass(field.send, "send-button")
|
||||
|
||||
// Keep this number the same as size-allocate below -------v
|
||||
field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
|
||||
field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
field.FieldBox.PackStart(field.Username, false, false, 0)
|
||||
field.FieldBox.PackStart(field.attach, false, false, 0)
|
||||
field.FieldBox.PackStart(field.TextScroll, true, true, 0)
|
||||
field.FieldBox.PackStart(field.send, false, false, 0)
|
||||
field.FieldBox.Show()
|
||||
primitives.AddClass(field.FieldBox, "input-field")
|
||||
primitives.AttachCSS(field.FieldBox, inputFieldCSS)
|
||||
inputFieldCSS(field.FieldBox)
|
||||
|
||||
field.Attachments = attachment.New()
|
||||
field.Attachments.Show()
|
||||
|
@ -163,6 +174,7 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
field.Box.PackStart(field.Attachments, false, false, 0)
|
||||
field.Box.PackStart(field.FieldBox, false, false, 0)
|
||||
field.Box.Show()
|
||||
inputBoxCSS(field.Box)
|
||||
|
||||
text.SetFocusHAdjustment(field.TextScroll.GetHAdjustment())
|
||||
text.SetFocusVAdjustment(field.TextScroll.GetVAdjustment())
|
||||
|
@ -178,7 +190,7 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
field.Username.Connect("size-allocate", func(w gtk.IWidget) {
|
||||
// Calculate the left width: from the left of the message box to the
|
||||
// right of the attach button, covering the username container.
|
||||
var leftWidth = 5*2 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
|
||||
var leftWidth = 5 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
|
||||
// Set the autocompleter's left margin to be the same.
|
||||
field.Attachments.SetMarginStart(leftWidth)
|
||||
})
|
||||
|
|
|
@ -77,12 +77,12 @@ func NewContainer() *Container {
|
|||
|
||||
func (u *Container) SetRevealChild(reveal bool) {
|
||||
// Only reveal if showUser is true.
|
||||
u.Revealer.SetRevealChild(reveal && showUser)
|
||||
u.Revealer.SetRevealChild(reveal && u.shouldReveal())
|
||||
}
|
||||
|
||||
// shouldReveal returns whether or not the container should reveal.
|
||||
func (u *Container) shouldReveal() bool {
|
||||
return !u.label.GetLabel().Empty() && showUser
|
||||
return (!u.label.GetLabel().Empty() || u.avatar.URL() != "") && showUser
|
||||
}
|
||||
|
||||
func (u *Container) Reset() {
|
||||
|
@ -96,7 +96,7 @@ func (u *Container) Update(session cchat.Session, sender cchat.ServerMessageSend
|
|||
// Set the fallback username.
|
||||
u.label.SetLabelUnsafe(session.Name())
|
||||
// Reveal the name if it's not empty.
|
||||
u.SetRevealChild(u.shouldReveal())
|
||||
u.SetRevealChild(true)
|
||||
|
||||
// Does sender (aka Server) implement ServerNickname? If yes, use it.
|
||||
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
||||
|
@ -120,7 +120,7 @@ func (u *Container) SetLabel(content text.Rich) {
|
|||
u.label.SetLabelUnsafe(content)
|
||||
|
||||
// Reveal if the name is not empty.
|
||||
u.SetRevealChild(u.shouldReveal())
|
||||
u.SetRevealChild(true)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -131,9 +131,7 @@ func (u *Container) SetIcon(url string) {
|
|||
|
||||
// Reveal if the icon URL is not empty. We don't touch anything if the
|
||||
// URL is empty, as the name might not be.
|
||||
if url != "" {
|
||||
u.SetRevealChild(true)
|
||||
}
|
||||
u.SetRevealChild(true)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,21 @@ import (
|
|||
|
||||
var MemberListWidth = 250
|
||||
|
||||
type Controller interface {
|
||||
MemberListUpdated(c *Container)
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
*gtk.Revealer
|
||||
Scroll *gtk.ScrolledWindow
|
||||
Main *gtk.Box
|
||||
ctrl Controller
|
||||
|
||||
// states
|
||||
|
||||
// map id -> *Section
|
||||
Sections map[string]*Section
|
||||
|
||||
// states
|
||||
stop func()
|
||||
stop func()
|
||||
}
|
||||
|
||||
var memberListCSS = primitives.PrepareClassCSS("member-list", `
|
||||
|
@ -39,7 +44,7 @@ var memberListCSS = primitives.PrepareClassCSS("member-list", `
|
|||
}
|
||||
`)
|
||||
|
||||
func New() *Container {
|
||||
func New(ctrl Controller) *Container {
|
||||
main, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
main.SetSizeRequest(250, -1)
|
||||
main.Show()
|
||||
|
@ -52,7 +57,7 @@ func New() *Container {
|
|||
|
||||
rev, _ := gtk.RevealerNew()
|
||||
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
|
||||
rev.SetTransitionDuration(50)
|
||||
rev.SetTransitionDuration(75)
|
||||
rev.SetRevealChild(false)
|
||||
rev.Add(sw)
|
||||
|
||||
|
@ -60,10 +65,16 @@ func New() *Container {
|
|||
Revealer: rev,
|
||||
Scroll: sw,
|
||||
Main: main,
|
||||
ctrl: ctrl,
|
||||
Sections: map[string]*Section{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmpty returns whether or not the member view container is empty.
|
||||
func (c *Container) IsEmpty() bool {
|
||||
return len(c.Sections) == 0
|
||||
}
|
||||
|
||||
// Reset removes all old sections.
|
||||
func (c *Container) Reset() {
|
||||
if c.stop != nil {
|
||||
|
@ -94,10 +105,7 @@ func (c *Container) TryAsyncList(server cchat.ServerMessage) {
|
|||
return nil, errors.Wrap(err, "Failed to list members")
|
||||
}
|
||||
|
||||
return func() {
|
||||
c.stop = f
|
||||
c.Revealer.SetRevealChild(true)
|
||||
}, nil
|
||||
return func() { c.stop = f }, nil
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -138,6 +146,8 @@ func (c *Container) SetSectionsUnsafe(sections []cchat.MemberListSection) {
|
|||
c.Main.Add(section)
|
||||
c.Sections[section.ID] = section
|
||||
}
|
||||
|
||||
c.ctrl.MemberListUpdated(c)
|
||||
}
|
||||
|
||||
func (c *Container) SetMemberUnsafe(sectionID string, member cchat.ListMember) {
|
||||
|
|
|
@ -10,20 +10,15 @@ import (
|
|||
|
||||
const FaceSize = 56
|
||||
|
||||
type WidgetUnreferencer interface {
|
||||
gtk.IWidget
|
||||
Unref()
|
||||
}
|
||||
|
||||
type FaceView struct {
|
||||
gtk.Stack
|
||||
placeholder WidgetUnreferencer
|
||||
placeholder gtk.IWidget
|
||||
|
||||
Face *Container
|
||||
Loading *Spinner
|
||||
}
|
||||
|
||||
func New(parent gtk.IWidget, placeholder WidgetUnreferencer) *FaceView {
|
||||
func New(parent gtk.IWidget, placeholder gtk.IWidget) *FaceView {
|
||||
c := NewContainer()
|
||||
c.Show()
|
||||
|
||||
|
@ -55,14 +50,6 @@ func (v *FaceView) Reset() {
|
|||
v.Stack.SetVisibleChildName("empty")
|
||||
}
|
||||
|
||||
// func (v *FaceView) Disable() {
|
||||
// v.Stack.SetSensitive(false)
|
||||
// }
|
||||
|
||||
// func (v *FaceView) Enable() {
|
||||
// v.Stack.SetSensitive(true)
|
||||
// }
|
||||
|
||||
func (v *FaceView) SetMain() {
|
||||
v.ensurePlaceholderDestroyed()
|
||||
v.Loading.Spinner.Stop()
|
||||
|
@ -104,7 +91,6 @@ type Spinner struct {
|
|||
func NewSpinner() *Spinner {
|
||||
s, _ := gtk.SpinnerNew()
|
||||
s.SetSizeRequest(FaceSize, FaceSize)
|
||||
s.Start()
|
||||
s.Show()
|
||||
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -40,6 +42,8 @@ func init() {
|
|||
}
|
||||
|
||||
type Controller interface {
|
||||
// GoBack tells the main leaflet to go back to the services list.
|
||||
GoBack()
|
||||
// OnMessageBusy is called when the message buffer is busy. This happens
|
||||
// when it's loading messages.
|
||||
OnMessageBusy()
|
||||
|
@ -54,8 +58,9 @@ type View struct {
|
|||
Header *Header
|
||||
|
||||
FaceView *sadface.FaceView
|
||||
Grid *gtk.Grid
|
||||
Leaflet *handy.Leaflet
|
||||
|
||||
LeftBox *gtk.Box
|
||||
Scroller *autoscroll.ScrolledWindow
|
||||
InputView *input.InputView
|
||||
|
||||
|
@ -64,20 +69,29 @@ type View struct {
|
|||
Container container.Container
|
||||
contType int // msgIndex
|
||||
|
||||
MemberList *memberlist.Container
|
||||
MemberList *memberlist.Container // right box
|
||||
|
||||
// Inherit some useful methods.
|
||||
state
|
||||
|
||||
ctrl Controller
|
||||
ctrl Controller
|
||||
parentFolded bool // folded state
|
||||
}
|
||||
|
||||
var messageStack = primitives.PrepareClassCSS("message-stack", `
|
||||
.message-stack {
|
||||
background-color: mix(@theme_bg_color, @theme_fg_color, 0.03);
|
||||
}
|
||||
`)
|
||||
|
||||
var messageScroller = primitives.PrepareClassCSS("message-scroller", ``)
|
||||
|
||||
func NewView(c Controller) *View {
|
||||
view := &View{ctrl: c}
|
||||
view.Typing = typing.New()
|
||||
view.Typing.Show()
|
||||
|
||||
view.MemberList = memberlist.New()
|
||||
view.MemberList = memberlist.New(view)
|
||||
view.MemberList.Show()
|
||||
|
||||
view.MsgBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
|
@ -89,6 +103,7 @@ func NewView(c Controller) *View {
|
|||
view.Scroller.SetVExpand(true)
|
||||
view.Scroller.SetHExpand(true)
|
||||
view.Scroller.Show()
|
||||
messageScroller(view.Scroller)
|
||||
|
||||
view.MsgBox.SetFocusHAdjustment(view.Scroller.GetHAdjustment())
|
||||
view.MsgBox.SetFocusVAdjustment(view.Scroller.GetVAdjustment())
|
||||
|
@ -113,27 +128,51 @@ func NewView(c Controller) *View {
|
|||
view.InputView.SetHExpand(true)
|
||||
view.InputView.Show()
|
||||
|
||||
view.Grid, _ = gtk.GridNew()
|
||||
view.Grid.Attach(view.Scroller, 0, 0, 1, 1)
|
||||
view.Grid.Attach(sep, 0, 1, 1, 1)
|
||||
view.Grid.Attach(view.InputView, 0, 2, 1, 1)
|
||||
view.Grid.Attach(view.MemberList, 1, 0, 1, 3)
|
||||
view.Grid.Show()
|
||||
view.LeftBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.LeftBox.PackStart(view.Scroller, true, true, 0)
|
||||
view.LeftBox.PackStart(sep, false, false, 0)
|
||||
view.LeftBox.PackStart(view.InputView, false, false, 0)
|
||||
view.LeftBox.Show()
|
||||
|
||||
primitives.AddClass(view.Grid, "message-view")
|
||||
view.Leaflet = handy.LeafletNew()
|
||||
view.Leaflet.Add(view.LeftBox)
|
||||
view.Leaflet.Add(view.MemberList)
|
||||
view.Leaflet.SetVisibleChild(view.LeftBox)
|
||||
view.Leaflet.Show()
|
||||
primitives.AddClass(view.Leaflet, "message-view")
|
||||
|
||||
// Bind a file drag-and-drop box into the main view box.
|
||||
drag.BindFileDest(view.Grid, view.InputView.Attachments.AddFiles)
|
||||
drag.BindFileDest(view.LeftBox, view.InputView.Attachments.AddFiles)
|
||||
|
||||
// placeholder logo
|
||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
|
||||
logo.Show()
|
||||
|
||||
view.FaceView = sadface.New(view.Grid, logo)
|
||||
view.FaceView = sadface.New(view.Leaflet, logo)
|
||||
view.FaceView.Show()
|
||||
messageStack(view.FaceView)
|
||||
|
||||
view.Header = NewHeader()
|
||||
view.Header.Show()
|
||||
view.Header.OnBackPressed(view.ctrl.GoBack)
|
||||
view.Header.OnShowMembersToggle(func(show bool) {
|
||||
// If the leaflet is folded, then we should always reveal the child. Its
|
||||
// visibility should be determined by the leaflet's state.
|
||||
if view.parentFolded {
|
||||
view.MemberList.SetRevealChild(true)
|
||||
if show {
|
||||
view.Leaflet.SetVisibleChild(view.MemberList)
|
||||
} else {
|
||||
view.Leaflet.SetVisibleChild(view.LeftBox)
|
||||
}
|
||||
} else {
|
||||
// Leaflet's visible child does not matter if it's not folded,
|
||||
// though we should still set the visible child to LeftBox in case
|
||||
// that changes.
|
||||
view.MemberList.SetRevealChild(show)
|
||||
view.Leaflet.SetVisibleChild(view.LeftBox)
|
||||
}
|
||||
})
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackStart(view.Header, false, false, 0)
|
||||
|
@ -173,6 +212,9 @@ func (v *View) Reset() {
|
|||
v.MemberList.Reset() // Reset the member list.
|
||||
v.FaceView.Reset() // Switch back to the main screen.
|
||||
|
||||
// Bring the leaflet view back to the message.
|
||||
v.Leaflet.SetVisibleChild(v.LeftBox)
|
||||
|
||||
// Keep the scroller at the bottom.
|
||||
v.Scroller.Bottomed = true
|
||||
|
||||
|
@ -180,8 +222,45 @@ func (v *View) Reset() {
|
|||
v.createMessageContainer()
|
||||
}
|
||||
|
||||
func (v *View) SetFolded(folded bool) {
|
||||
v.parentFolded = folded
|
||||
|
||||
// Change to a mini breadcrumb if we're collapsed.
|
||||
v.Header.SetMiniBreadcrumb(folded)
|
||||
|
||||
// Show the right back button if we're collapsed.
|
||||
v.Header.SetShowBackButton(folded)
|
||||
|
||||
// Hide the username in the input bar if we're collapsed.
|
||||
v.InputView.Username.SetRevealChild(!folded)
|
||||
|
||||
// Hide the member list automatically on folded.
|
||||
if folded {
|
||||
v.Header.ShowMembers.SetActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MemberListUpdated is called everytime the member list is updated.
|
||||
func (v *View) MemberListUpdated(c *memberlist.Container) {
|
||||
// We can show the members list if it's not empty.
|
||||
var empty = c.IsEmpty()
|
||||
v.Header.SetCanShowMembers(!empty)
|
||||
|
||||
// If the member list is now empty, then hide the entire thing.
|
||||
if empty {
|
||||
// We can set active to false, which would trigger the above callback
|
||||
// and hide the member list.
|
||||
v.Header.ShowMembers.SetActive(false)
|
||||
} else {
|
||||
// Restore visibility.
|
||||
if !v.Leaflet.GetFolded() && v.Header.ShowMembers.GetActive() {
|
||||
c.SetRevealChild(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||
func (v *View) JoinServer(session cchat.Session, server ServerMessage) {
|
||||
func (v *View) JoinServer(session cchat.Session, server ServerMessage, bc traverse.Breadcrumber) {
|
||||
// Reset before setting.
|
||||
v.Reset()
|
||||
|
||||
|
@ -221,6 +300,9 @@ func (v *View) JoinServer(session cchat.Session, server ServerMessage) {
|
|||
// Set the cancel handler.
|
||||
v.state.setcurrent(s)
|
||||
|
||||
// Set the headerbar's breadcrumb.
|
||||
v.Header.SetBreadcrumber(bc)
|
||||
|
||||
// Try setting the typing indicator if available.
|
||||
v.Typing.TrySubscribe(server)
|
||||
|
||||
|
@ -253,7 +335,10 @@ func (v *View) FetchBacklog() {
|
|||
}
|
||||
|
||||
gts.Async(func() (func(), error) {
|
||||
err := backlogger.MessagesBefore(context.Background(), firstMsg.ID(), v.Container)
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := backlogger.MessagesBefore(ctx, firstMsg.ID(), v.Container)
|
||||
return done, errors.Wrap(err, "Failed to get messages before ID")
|
||||
})
|
||||
}
|
||||
|
@ -386,6 +471,14 @@ func (s *state) SessionID() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// ServerID returns the server ID, or an empty string if there's no server.
|
||||
func (s *state) ServerID() string {
|
||||
if s.server != nil {
|
||||
return s.server.ID()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const backloggingFreq = time.Second * 3
|
||||
|
||||
// Backlogger returns the backlogger instance if it's allowed to fetch more
|
||||
|
|
|
@ -2,9 +2,11 @@ package primitives
|
|||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/glib"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
|
@ -115,8 +117,20 @@ type ImageIconSetter interface {
|
|||
}
|
||||
|
||||
func SetImageIcon(img ImageIconSetter, icon string, sizepx int) {
|
||||
img.SetProperty("icon-name", icon)
|
||||
img.SetProperty("pixel-size", sizepx)
|
||||
// Prioritize SetSize()
|
||||
if setter, ok := img.(interface{ SetSize(int) }); ok {
|
||||
setter.SetSize(sizepx)
|
||||
} else {
|
||||
img.SetProperty("pixel-size", sizepx)
|
||||
}
|
||||
|
||||
// Prioritize SetIconName().
|
||||
if setter, ok := img.(interface{ SetIconName(string) }); ok {
|
||||
setter.SetIconName(icon)
|
||||
} else {
|
||||
img.SetProperty("icon-name", icon)
|
||||
}
|
||||
|
||||
img.SetSizeRequest(sizepx, sizepx)
|
||||
}
|
||||
|
||||
|
@ -261,3 +275,28 @@ func AttachCSS(ctx StyleContexter, prov *gtk.CssProvider) {
|
|||
func InlineCSS(ctx StyleContexter, css string) {
|
||||
AttachCSS(ctx, PrepareCSS(css))
|
||||
}
|
||||
|
||||
// LeafletOnFold binds a callback to a leaflet that would be called when the
|
||||
// leaflet's folded state changes.
|
||||
func LeafletOnFold(leaflet *handy.Leaflet, foldedFn func(folded bool)) {
|
||||
var lastFold = leaflet.GetFolded()
|
||||
foldedFn(lastFold)
|
||||
|
||||
// Give each callback a 500ms wait for animations to complete.
|
||||
const dt = 500 * time.Millisecond
|
||||
var last = time.Now()
|
||||
|
||||
leaflet.ConnectAfter("size-allocate", func() {
|
||||
// Ignore if this event is too recent.
|
||||
if now := time.Now(); now.Add(-dt).Before(last) {
|
||||
return
|
||||
} else {
|
||||
last = now
|
||||
}
|
||||
|
||||
if folded := leaflet.GetFolded(); folded != lastFold {
|
||||
lastFold = folded
|
||||
foldedFn(folded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
89
internal/ui/primitives/roundimage/avatar.go
Normal file
89
internal/ui/primitives/roundimage/avatar.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package roundimage
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/handy"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// TODO: GIF support
|
||||
|
||||
// TextSetter is an interface for setting texts.
|
||||
type TextSetter interface {
|
||||
SetText(text string)
|
||||
}
|
||||
|
||||
func TrySetText(imager Imager, text string) {
|
||||
if setter, ok := imager.(TextSetter); ok {
|
||||
setter.SetText(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar is a static HdyAvatar container.
|
||||
type Avatar struct {
|
||||
handy.Avatar
|
||||
pixbuf *gdk.Pixbuf
|
||||
}
|
||||
|
||||
var (
|
||||
_ Imager = (*Avatar)(nil)
|
||||
_ TextSetter = (*Avatar)(nil)
|
||||
_ httputil.ImageContainer = (*Avatar)(nil)
|
||||
)
|
||||
|
||||
func NewAvatar(size int) *Avatar {
|
||||
a := handy.AvatarNew(size, "", true)
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Avatar{*a, nil}
|
||||
}
|
||||
|
||||
// SetSizeRequest sets the avatar size. The actual size is min(w, h).
|
||||
func (a *Avatar) SetSizeRequest(w, h int) {
|
||||
var min = w
|
||||
if w > h {
|
||||
min = h
|
||||
}
|
||||
|
||||
a.Avatar.SetSize(min)
|
||||
a.Avatar.SetSizeRequest(w, h)
|
||||
}
|
||||
|
||||
func (a *Avatar) loadFunc(int) *gdk.Pixbuf {
|
||||
return a.pixbuf
|
||||
}
|
||||
|
||||
// SetRadius is a no-op.
|
||||
func (a *Avatar) SetRadius(float64) {}
|
||||
|
||||
// SetFromPixbuf sets the pixbuf.
|
||||
func (a *Avatar) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
a.pixbuf = pb
|
||||
a.Avatar.SetImageLoadFunc(a.loadFunc)
|
||||
}
|
||||
|
||||
func (a *Avatar) SetFromAnimation(pa *gdk.PixbufAnimation) {
|
||||
a.pixbuf = pa.GetStaticImage()
|
||||
a.Avatar.SetImageLoadFunc(a.loadFunc)
|
||||
}
|
||||
|
||||
func (a *Avatar) GetPixbuf() *gdk.Pixbuf {
|
||||
return a.pixbuf
|
||||
}
|
||||
|
||||
// GetAnimation returns nil.
|
||||
func (a *Avatar) GetAnimation() *gdk.PixbufAnimation {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetImage returns nil.
|
||||
func (a *Avatar) GetImage() *gtk.Image {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Avatar) GetStorageType() gtk.ImageType {
|
||||
return gtk.IMAGE_PIXBUF
|
||||
}
|
57
internal/ui/primitives/roundimage/button.go
Normal file
57
internal/ui/primitives/roundimage/button.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package roundimage
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// Button implements a rounded button with a rounded image. This widget only
|
||||
// supports a full circle for rounding.
|
||||
type Button struct {
|
||||
*gtk.Button
|
||||
Image Imager
|
||||
}
|
||||
|
||||
var roundButtonCSS = primitives.PrepareClassCSS("round-button", `
|
||||
.round-button {
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewButton() (*Button, error) {
|
||||
image, _ := NewImage(0)
|
||||
image.Show()
|
||||
|
||||
b, _ := NewEmptyButton()
|
||||
b.SetImage(image)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func NewEmptyButton() (*Button, error) {
|
||||
b, _ := gtk.ButtonNew()
|
||||
b.SetRelief(gtk.RELIEF_NONE)
|
||||
roundButtonCSS(b)
|
||||
|
||||
return &Button{Button: b}, nil
|
||||
}
|
||||
|
||||
// NewCustomButton creates a new rounded button with the given Imager. If the
|
||||
// given Imager implements the Connector interface (aka *StaticImage), then the
|
||||
// function will implicitly connect its handlers to the button.
|
||||
func NewCustomButton(img Imager) (*Button, error) {
|
||||
b, _ := NewEmptyButton()
|
||||
b.SetImage(img)
|
||||
|
||||
if connector, ok := img.(Connector); ok {
|
||||
connector.ConnectHandlers(b)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Button) SetImage(img Imager) {
|
||||
b.Image = img
|
||||
b.Button.SetImage(img)
|
||||
}
|
|
@ -15,57 +15,6 @@ const (
|
|||
circle = 2 * math.Pi
|
||||
)
|
||||
|
||||
// Button implements a rounded button with a rounded image. This widget only
|
||||
// supports a full circle for rounding.
|
||||
type Button struct {
|
||||
*gtk.Button
|
||||
Image Imager
|
||||
}
|
||||
|
||||
var roundButtonCSS = primitives.PrepareClassCSS("round-button", `
|
||||
.round-button {
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewButton() (*Button, error) {
|
||||
image, _ := NewImage(0)
|
||||
image.Show()
|
||||
|
||||
b, _ := NewEmptyButton()
|
||||
b.SetImage(image)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func NewEmptyButton() (*Button, error) {
|
||||
b, _ := gtk.ButtonNew()
|
||||
b.SetRelief(gtk.RELIEF_NONE)
|
||||
roundButtonCSS(b)
|
||||
|
||||
return &Button{Button: b}, nil
|
||||
}
|
||||
|
||||
// NewCustomButton creates a new rounded button with the given Imager. If the
|
||||
// given Imager implements the Connector interface (aka *StaticImage), then the
|
||||
// function will implicitly connect its handlers to the button.
|
||||
func NewCustomButton(img Imager) (*Button, error) {
|
||||
b, _ := NewEmptyButton()
|
||||
b.SetImage(img)
|
||||
|
||||
if connector, ok := img.(Connector); ok {
|
||||
connector.ConnectHandlers(b)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Button) SetImage(img Imager) {
|
||||
b.Image = img
|
||||
b.Button.SetImage(img)
|
||||
}
|
||||
|
||||
type RadiusSetter interface {
|
||||
SetRadius(float64)
|
||||
}
|
||||
|
@ -87,59 +36,6 @@ type Imager interface {
|
|||
GetImage() *gtk.Image
|
||||
}
|
||||
|
||||
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
||||
type StaticImage struct {
|
||||
*Image
|
||||
animation *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var (
|
||||
_ Imager = (*StaticImage)(nil)
|
||||
_ Connector = (*StaticImage)(nil)
|
||||
_ httputil.ImageContainer = (*StaticImage)(nil)
|
||||
)
|
||||
|
||||
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) {
|
||||
i, err := NewImage(radius)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var s = &StaticImage{i, nil}
|
||||
if parent != nil {
|
||||
s.ConnectHandlers(parent)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||
connector.Connect("enter-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromAnimation(s.animation)
|
||||
}
|
||||
})
|
||||
connector.Connect("leave-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
s.animation = nil
|
||||
s.Image.SetFromPixbuf(pb)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||
s.animation = anim
|
||||
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
||||
}
|
||||
|
||||
func (s *StaticImage) GetAnimation() *gdk.PixbufAnimation {
|
||||
return s.animation
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
*gtk.Image
|
||||
Radius float64
|
||||
|
|
60
internal/ui/primitives/roundimage/static.go
Normal file
60
internal/ui/primitives/roundimage/static.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package roundimage
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
)
|
||||
|
||||
// StaticImage is an image that only plays a GIF if it's hovered on top of.
|
||||
type StaticImage struct {
|
||||
*Image
|
||||
animation *gdk.PixbufAnimation
|
||||
}
|
||||
|
||||
var (
|
||||
_ Imager = (*StaticImage)(nil)
|
||||
_ Connector = (*StaticImage)(nil)
|
||||
_ httputil.ImageContainer = (*StaticImage)(nil)
|
||||
)
|
||||
|
||||
func NewStaticImage(parent primitives.Connector, radius float64) (*StaticImage, error) {
|
||||
i, err := NewImage(radius)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var s = &StaticImage{i, nil}
|
||||
if parent != nil {
|
||||
s.ConnectHandlers(parent)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *StaticImage) ConnectHandlers(connector primitives.Connector) {
|
||||
connector.Connect("enter-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromAnimation(s.animation)
|
||||
}
|
||||
})
|
||||
connector.Connect("leave-notify-event", func() {
|
||||
if s.animation != nil {
|
||||
s.Image.SetFromPixbuf(s.animation.GetStaticImage())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromPixbuf(pb *gdk.Pixbuf) {
|
||||
s.animation = nil
|
||||
s.Image.SetFromPixbuf(pb)
|
||||
}
|
||||
|
||||
func (s *StaticImage) SetFromAnimation(anim *gdk.PixbufAnimation) {
|
||||
s.animation = anim
|
||||
s.Image.SetFromPixbuf(anim.GetStaticImage())
|
||||
}
|
||||
|
||||
func (s *StaticImage) GetAnimation() *gdk.PixbufAnimation {
|
||||
return s.animation
|
||||
}
|
|
@ -135,6 +135,9 @@ func (i *Icon) SetIcon(url string) {
|
|||
}
|
||||
|
||||
func (i *Icon) AsyncSetIconer(iconer cchat.Icon, errwrap string) {
|
||||
// Reveal to show the placeholder.
|
||||
i.SetRevealChild(true)
|
||||
|
||||
AsyncUse(i.r, func(ctx context.Context) (interface{}, func(), error) {
|
||||
ni := &nullIcon{}
|
||||
f, err := iconer.Icon(ctx, ni)
|
||||
|
@ -161,8 +164,11 @@ type EventIcon struct {
|
|||
|
||||
func NewEventIcon(sizepx int, pp ...imgutil.Processor) *EventIcon {
|
||||
icn := NewIcon(sizepx, pp...)
|
||||
icn.Show()
|
||||
return WrapEventIcon(icn)
|
||||
}
|
||||
|
||||
func WrapEventIcon(icn *Icon) *EventIcon {
|
||||
icn.Show()
|
||||
evb, _ := gtk.EventBoxNew()
|
||||
evb.Add(icn)
|
||||
|
||||
|
@ -189,11 +195,15 @@ var (
|
|||
)
|
||||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
img, _ := roundimage.NewStaticImage(nil, 0)
|
||||
img.Show()
|
||||
return NewCustomToggleButtonImage(img, content)
|
||||
}
|
||||
|
||||
func NewCustomToggleButtonImage(img RoundIconContainer, content text.Rich) *ToggleButtonImage {
|
||||
l := NewLabel(content)
|
||||
l.Show()
|
||||
|
||||
img, _ := roundimage.NewStaticImage(nil, 0)
|
||||
img.Show()
|
||||
i := NewCustomIcon(img, 0)
|
||||
i.Show()
|
||||
|
||||
|
@ -205,7 +215,9 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
b, _ := gtk.ToggleButtonNew()
|
||||
b.Add(box)
|
||||
|
||||
img.ConnectHandlers(b)
|
||||
if connector, ok := img.(roundimage.Connector); ok {
|
||||
connector.ConnectHandlers(b)
|
||||
}
|
||||
|
||||
return &ToggleButtonImage{
|
||||
ToggleButton: *b,
|
||||
|
|
|
@ -91,7 +91,7 @@ func NewHeader() *Header {
|
|||
header.PackStart(appmenu)
|
||||
header.PackStart(sep)
|
||||
header.PackStart(svcname)
|
||||
header.PackStart(sesmenu)
|
||||
header.PackEnd(sesmenu)
|
||||
|
||||
// Hack to hide the title.
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||
"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/service/session"
|
||||
|
@ -96,14 +97,16 @@ func NewService(svc cchat.Service, svclctrl ListController) *Service {
|
|||
|
||||
// TODO: have it so the button changes to the session avatar when collapsed
|
||||
|
||||
// TODO: libhandy avatar generation?
|
||||
service.Icon = rich.NewIcon(IconSize)
|
||||
avatar := roundimage.NewAvatar(IconSize)
|
||||
avatar.SetText(svc.Name().String())
|
||||
avatar.Show()
|
||||
|
||||
service.Icon = rich.NewCustomIcon(avatar, IconSize)
|
||||
service.Icon.Show()
|
||||
// potentially nonstandard
|
||||
service.Icon.SetPlaceholderIcon("text-html-symbolic", IconSize)
|
||||
// TODO: hover for name. We use tooltip for now.
|
||||
service.Icon.SetTooltipMarkup(markup.Render(svc.Name()))
|
||||
// TODO: add a padding
|
||||
serviceIconCSS(service.Icon)
|
||||
|
||||
if iconer, ok := svc.(cchat.Icon); ok {
|
||||
|
|
|
@ -14,7 +14,7 @@ const UnreadColorDefs = `
|
|||
`
|
||||
|
||||
type ToggleButtonImage struct {
|
||||
rich.ToggleButtonImage
|
||||
*rich.ToggleButtonImage
|
||||
|
||||
extraMenu []menu.Item
|
||||
menu *menu.LazyMenu
|
||||
|
@ -57,10 +57,14 @@ var serverButtonCSS = primitives.PrepareClassCSS("server-button", `
|
|||
|
||||
func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
||||
b := rich.NewToggleButtonImage(content)
|
||||
return WrapToggleButtonImage(b)
|
||||
}
|
||||
|
||||
func WrapToggleButtonImage(b *rich.ToggleButtonImage) *ToggleButtonImage {
|
||||
b.Show()
|
||||
|
||||
tb := &ToggleButtonImage{
|
||||
ToggleButtonImage: *b,
|
||||
ToggleButtonImage: b,
|
||||
|
||||
clicked: func(bool) {},
|
||||
menu: menu.NewLazyMenu(b.ToggleButton),
|
||||
|
@ -73,7 +77,8 @@ func NewToggleButtonImage(content text.Rich) *ToggleButtonImage {
|
|||
|
||||
func (b *ToggleButtonImage) SetSelected(selected bool) {
|
||||
// Set the clickability the opposite as the boolean.
|
||||
b.SetSensitive(!selected)
|
||||
// b.SetSensitive(!selected)
|
||||
b.SetInconsistent(selected)
|
||||
|
||||
if selected {
|
||||
primitives.AddClass(b, "selected-server")
|
||||
|
|
|
@ -69,6 +69,7 @@ func (c *Children) Init() {
|
|||
if c.IsHollow() {
|
||||
c.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
c.Box.SetMarginStart(ChildrenMargin)
|
||||
c.Box.SetHExpand(true)
|
||||
childrenCSS(c.Box)
|
||||
|
||||
// Check if we're still loading. This is effectively restoring the
|
||||
|
@ -180,6 +181,24 @@ func (c *Children) LoadAll() {
|
|||
c.Box.Add(row)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have icons.
|
||||
var hasIcon bool
|
||||
|
||||
for _, row := range c.Rows {
|
||||
if row.HasIcon() {
|
||||
hasIcon = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an icon, then show all other possibly empty icons. HdyAvatar
|
||||
// will generate a placeholder.
|
||||
if hasIcon {
|
||||
for _, row := range c.Rows {
|
||||
row.UseEmptyIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveSelectedRow saves the current selected row and returns a callback that
|
||||
|
@ -198,6 +217,7 @@ func (c *Children) saveSelectedRow() (restore func()) {
|
|||
if oldID != "" {
|
||||
for _, row := range c.Rows {
|
||||
if row.Server.ID() == oldID {
|
||||
row.Init()
|
||||
row.Button.SetActive(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/button"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server/traverse"
|
||||
|
@ -109,7 +110,7 @@ func (r *ServerRow) Init() {
|
|||
|
||||
case cchat.ServerMessage:
|
||||
primitives.AddClass(r, "server-message")
|
||||
r.Button.SetClickedIfTrue(func() { r.ctrl.RowSelected(r, server) })
|
||||
r.Button.SetClicked(func(bool) { r.ctrl.RowSelected(r, server) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +153,7 @@ func (r *ServerRow) SetUnreadUnsafe(unread, mentioned bool) {
|
|||
|
||||
type Row struct {
|
||||
*gtk.Box
|
||||
Avatar *roundimage.Avatar
|
||||
Button *button.ToggleButtonImage
|
||||
|
||||
parentcrumb traverse.Breadcrumber
|
||||
|
@ -181,7 +183,14 @@ func (r *Row) Init(name text.Rich) {
|
|||
return
|
||||
}
|
||||
|
||||
r.Button = button.NewToggleButtonImage(name)
|
||||
r.Avatar = roundimage.NewAvatar(IconSize)
|
||||
r.Avatar.SetText(name.Content)
|
||||
r.Avatar.Show()
|
||||
|
||||
btn := rich.NewCustomToggleButtonImage(r.Avatar, name)
|
||||
btn.Show()
|
||||
|
||||
r.Button = button.WrapToggleButtonImage(btn)
|
||||
r.Button.Box.SetHAlign(gtk.ALIGN_START)
|
||||
r.Button.SetRelief(gtk.RELIEF_NONE)
|
||||
r.Button.Show()
|
||||
|
@ -193,10 +202,6 @@ func (r *Row) Init(name text.Rich) {
|
|||
r.childrenSetErr(r.childrenErr)
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() traverse.Breadcrumb {
|
||||
return traverse.TryBreadcrumb(r.parentcrumb, r.Button.GetText())
|
||||
}
|
||||
|
||||
// SetHollowServerList sets the row to a hollow server list (children) and
|
||||
// recursively create
|
||||
func (r *Row) SetHollowServerList(list cchat.ServerList, ctrl Controller) {
|
||||
|
@ -255,10 +260,31 @@ func (r *Row) childrenSetErr(err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// UseEmptyIcon forces the row to show a placeholder icon.
|
||||
func (r *ServerRow) UseEmptyIcon() {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.Image.SetSize(IconSize)
|
||||
r.Button.Image.SetRevealChild(true)
|
||||
}
|
||||
|
||||
// HasIcon returns true if the current row has an icon.
|
||||
func (r *ServerRow) HasIcon() bool {
|
||||
return !r.IsHollow() && r.Button.Image.GetRevealChild()
|
||||
}
|
||||
|
||||
func (r *Row) Breadcrumb() traverse.Breadcrumb {
|
||||
if r.IsHollow() {
|
||||
return nil
|
||||
}
|
||||
return traverse.TryBreadcrumb(r.parentcrumb, r.Button.GetText())
|
||||
}
|
||||
|
||||
func (r *Row) SetLabelUnsafe(name text.Rich) {
|
||||
AssertUnhollow(r)
|
||||
|
||||
r.Button.SetLabelUnsafe(name)
|
||||
r.Avatar.SetText(name.Content)
|
||||
}
|
||||
|
||||
func (r *Row) SetIconer(v interface{}) {
|
||||
|
|
|
@ -29,7 +29,10 @@ type Servers struct {
|
|||
}
|
||||
|
||||
var toplevelCSS = primitives.PrepareClassCSS("top-level", `
|
||||
.top-level { margin: 0 3px }
|
||||
.top-level {
|
||||
margin: 0 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
`)
|
||||
|
||||
func NewServers(p traverse.Breadcrumber, ctrl server.Controller) *Servers {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/actions"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/roundimage"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser/markup"
|
||||
|
@ -51,7 +52,8 @@ type Servicer interface {
|
|||
// Row represents a session row entry in the session List.
|
||||
type Row struct {
|
||||
*gtk.ListBoxRow
|
||||
icon *rich.EventIcon // nilable
|
||||
avatar *roundimage.Avatar
|
||||
icon *rich.EventIcon // nilable
|
||||
|
||||
parentcrumb traverse.Breadcrumber
|
||||
|
||||
|
@ -112,6 +114,7 @@ var rowIconCSS = primitives.PrepareClassCSS("session-icon", `
|
|||
padding: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.session-icon.failed {
|
||||
background-color: alpha(red, 0.45);
|
||||
}
|
||||
|
@ -136,7 +139,14 @@ func newRow(parent traverse.Breadcrumber, name text.Rich, ctrl Servicer) *Row {
|
|||
parentcrumb: parent,
|
||||
}
|
||||
|
||||
row.icon = rich.NewEventIcon(IconSize)
|
||||
row.avatar = roundimage.NewAvatar(IconSize)
|
||||
row.avatar.SetText(name.Content)
|
||||
row.avatar.Show()
|
||||
|
||||
icon := rich.NewCustomIcon(row.avatar, IconSize)
|
||||
icon.Show()
|
||||
|
||||
row.icon = rich.WrapEventIcon(icon)
|
||||
row.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
|
||||
row.icon.Show()
|
||||
rowIconCSS(row.icon.Icon)
|
||||
|
@ -295,6 +305,7 @@ func (r *Row) SetSession(ses cchat.Session) {
|
|||
r.sessionID = ses.ID()
|
||||
r.SetTooltipMarkup(markup.Render(ses.Name()))
|
||||
r.icon.Icon.SetPlaceholderIcon(IconName, IconSize)
|
||||
r.avatar.SetText(ses.Name().Content)
|
||||
|
||||
// If the session has an icon, then use it.
|
||||
if iconer, ok := ses.(cchat.Icon); ok {
|
||||
|
|
|
@ -2,6 +2,7 @@ package service
|
|||
|
||||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/singlestack"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session/server"
|
||||
|
@ -55,6 +56,7 @@ func NewView(ctrller Controller) *View {
|
|||
view.ServerStack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
|
||||
view.ServerStack.SetHomogeneous(true)
|
||||
view.ServerStack.Show()
|
||||
primitives.AddClass(view.ServerStack, "server-stack")
|
||||
|
||||
view.ServerView, _ = gtk.ScrolledWindowNew(nil, nil)
|
||||
view.ServerView.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config/preferences"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/session"
|
||||
|
@ -29,6 +30,9 @@ func init() {
|
|||
|
||||
/* Hack to fix the input bar being high in Adwaita */
|
||||
.input-field * { min-height: 0 }
|
||||
|
||||
/* Hide all scroll undershoots */
|
||||
undershoot { background-size: 0 }
|
||||
`)
|
||||
}
|
||||
|
||||
|
@ -71,10 +75,12 @@ func NewApplication() *App {
|
|||
app := &App{}
|
||||
|
||||
app.Services = service.NewView(app)
|
||||
app.Services.SetSizeRequest(leftMinWidth, -1)
|
||||
app.Services.SetSizeRequest(leftCurrentWidth, -1)
|
||||
app.Services.SetHExpand(false)
|
||||
app.Services.Show()
|
||||
|
||||
app.MessageView = messages.NewView(app)
|
||||
app.MessageView.SetHExpand(true)
|
||||
app.MessageView.Show()
|
||||
|
||||
app.HeaderGroup = handy.HeaderGroupNew()
|
||||
|
@ -82,6 +88,7 @@ func NewApplication() *App {
|
|||
app.HeaderGroup.AddHeaderBar(&app.MessageView.Header.HeaderBar)
|
||||
|
||||
app.Leaflet = *handy.LeafletNew()
|
||||
app.Leaflet.SetTransitionType(handy.LeafletTransitionTypeUnder)
|
||||
app.Leaflet.Add(app.Services)
|
||||
app.Leaflet.Add(app.MessageView)
|
||||
app.Leaflet.Show()
|
||||
|
@ -90,6 +97,8 @@ func NewApplication() *App {
|
|||
// The action name for this is "app.preferences".
|
||||
gts.AddAppAction("preferences", preferences.SpawnPreferenceDialog)
|
||||
|
||||
primitives.LeafletOnFold(&app.Leaflet, app.MessageView.SetFolded)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
@ -128,6 +137,14 @@ func (app *App) SessionSelected(svc *service.Service, ses *session.Row) {
|
|||
}
|
||||
|
||||
func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.ServerMessage) {
|
||||
// Change to the message view.
|
||||
app.Leaflet.SetVisibleChild(app.MessageView)
|
||||
|
||||
// Assert that the new server is not the same one.
|
||||
if app.MessageView.ServerID() == srv.Server.ID() {
|
||||
return
|
||||
}
|
||||
|
||||
// Is there an old row that we should deactivate?
|
||||
if app.lastSelector != nil {
|
||||
app.lastSelector(false)
|
||||
|
@ -137,12 +154,15 @@ func (app *App) RowSelected(ses *session.Row, srv *server.ServerRow, smsg cchat.
|
|||
app.lastSelector = srv.SetSelected
|
||||
app.lastSelector(true)
|
||||
|
||||
// Assert that server is also a list, then join the server.
|
||||
app.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage))
|
||||
app.MessageView.JoinServer(ses.Session, smsg.(messages.ServerMessage), srv)
|
||||
}
|
||||
|
||||
// MessageView methods.
|
||||
|
||||
func (app *App) GoBack() {
|
||||
app.Leaflet.Navigate(handy.NavigationDirectionBack)
|
||||
}
|
||||
|
||||
func (app *App) OnMessageBusy() {
|
||||
// Disable the server list because we don't want the user to switch around
|
||||
// while we're loading.
|
||||
|
|
Loading…
Reference in a new issue