mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-11-17 03:32:56 +00:00
Added preferences
This commit is contained in:
parent
1a9cc2626e
commit
9df0aca1fa
|
@ -21,24 +21,39 @@ var App struct {
|
|||
Header *gtk.HeaderBar
|
||||
}
|
||||
|
||||
// NewModalDialog returns a new modal dialog that's transient for the main
|
||||
// window.
|
||||
func NewModalDialog() (*gtk.Dialog, error) {
|
||||
d, err := gtk.DialogNew()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.SetModal(true)
|
||||
d.SetTransientFor(App.Window)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func AddAppAction(name string, call func()) {
|
||||
action := glib.SimpleActionNew(name, nil)
|
||||
action.Connect("activate", call)
|
||||
App.AddAction(action)
|
||||
}
|
||||
|
||||
func AddWindowAction(name string, call func()) {
|
||||
action := glib.SimpleActionNew(name, nil)
|
||||
action.Connect("activate", call)
|
||||
App.Window.AddAction(action)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gtk.Init(&Args)
|
||||
App.Application, _ = gtk.ApplicationNew(AppID, 0)
|
||||
}
|
||||
|
||||
type Windower interface {
|
||||
Window() gtk.IWidget
|
||||
}
|
||||
|
||||
type Headerer interface {
|
||||
Header() gtk.IWidget
|
||||
}
|
||||
|
||||
// Above interfaces should be kept for modularity, but since this is an internal
|
||||
// abstraction, we already know our application will implement both.
|
||||
type WindowHeaderer interface {
|
||||
Windower
|
||||
Headerer
|
||||
Window() gtk.IWidget
|
||||
Header() gtk.IWidget
|
||||
Destroy()
|
||||
}
|
||||
|
||||
func Main(wfn func() WindowHeaderer) {
|
||||
|
@ -62,8 +77,12 @@ func Main(wfn func() WindowHeaderer) {
|
|||
// Execute the function later, because we need it to run after
|
||||
// initialization.
|
||||
w := wfn()
|
||||
App.Window.Connect("destroy", w.Destroy)
|
||||
App.Window.Add(w.Window())
|
||||
App.Header.Add(w.Header())
|
||||
|
||||
// Connect extra events.
|
||||
AddAppAction("quit", App.Window.Destroy)
|
||||
})
|
||||
|
||||
// Use a special function to run the application. Exit with the appropriate
|
||||
|
|
43
internal/ui/config/config.go
Normal file
43
internal/ui/config/config.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Package config provides the repository for configuration and preferences.
|
||||
package config
|
||||
|
||||
import "sort"
|
||||
|
||||
// List of config sections.
|
||||
type Section uint8
|
||||
|
||||
const (
|
||||
Appearance Section = iota
|
||||
sectionLen
|
||||
)
|
||||
|
||||
func (s Section) String() string {
|
||||
switch s {
|
||||
case Appearance:
|
||||
return "Appearance"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
var Sections = [sectionLen][]Entry{}
|
||||
|
||||
func sortSection(section Section) {
|
||||
// TODO: remove the sorting and allow for declarative ordering
|
||||
sort.Slice(Sections[section], func(i, j int) bool {
|
||||
return Sections[section][i].Name < Sections[section][j].Name
|
||||
})
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Name string
|
||||
Value EntryValue
|
||||
}
|
||||
|
||||
func AppearanceAdd(name string, value EntryValue) {
|
||||
Sections[Appearance] = append(Sections[Appearance], Entry{
|
||||
Name: name,
|
||||
Value: value,
|
||||
})
|
||||
sortSection(Appearance)
|
||||
}
|
83
internal/ui/config/preferences/preferences.go
Normal file
83
internal/ui/config/preferences/preferences.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package preferences
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
*gtk.Dialog
|
||||
|
||||
switcher *gtk.StackSwitcher
|
||||
stack *gtk.Stack
|
||||
}
|
||||
|
||||
func NewDialog() *Dialog {
|
||||
stack, _ := gtk.StackNew()
|
||||
stack.Show()
|
||||
|
||||
switcher, _ := gtk.StackSwitcherNew()
|
||||
switcher.SetStack(stack)
|
||||
switcher.Show()
|
||||
|
||||
h, _ := gtk.HeaderBarNew()
|
||||
h.SetShowCloseButton(true)
|
||||
h.SetCustomTitle(switcher)
|
||||
h.Show()
|
||||
|
||||
d, _ := gts.NewModalDialog()
|
||||
d.SetDefaultSize(400, 300)
|
||||
d.SetTitle("Preferences")
|
||||
d.SetTitlebar(h)
|
||||
|
||||
b, _ := d.GetContentArea()
|
||||
b.SetMarginTop(8)
|
||||
b.SetMarginBottom(8)
|
||||
b.SetMarginStart(16)
|
||||
b.SetMarginEnd(16)
|
||||
b.PackStart(stack, true, true, 0)
|
||||
b.Show()
|
||||
|
||||
return &Dialog{
|
||||
Dialog: d,
|
||||
stack: stack,
|
||||
switcher: switcher,
|
||||
}
|
||||
}
|
||||
|
||||
func Section(entries []config.Entry) *gtk.Grid {
|
||||
var grid, _ = gtk.GridNew()
|
||||
|
||||
for i, entry := range entries {
|
||||
l, _ := gtk.LabelNew(entry.Name)
|
||||
l.SetHExpand(true)
|
||||
l.SetXAlign(0)
|
||||
l.Show()
|
||||
|
||||
grid.Attach(l, 0, i, 1, 1)
|
||||
grid.Attach(entry.Value.Construct(), 1, i, 1, 1)
|
||||
}
|
||||
|
||||
grid.SetRowSpacing(4)
|
||||
grid.SetColumnSpacing(4)
|
||||
grid.Show()
|
||||
return grid
|
||||
}
|
||||
|
||||
func NewPreferenceDialog() *Dialog {
|
||||
var dialog = NewDialog()
|
||||
|
||||
for i, section := range config.Sections {
|
||||
grid := Section(section)
|
||||
name := config.Section(i).String()
|
||||
|
||||
dialog.stack.AddTitled(grid, name, name)
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
func SpawnPreferenceDialog() {
|
||||
NewPreferenceDialog().Show()
|
||||
}
|
100
internal/ui/config/widgets.go
Normal file
100
internal/ui/config/widgets.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
// EntryValue with JSON serde capabilities.
|
||||
type EntryValue interface {
|
||||
json.Marshaler
|
||||
json.Unmarshaler
|
||||
Construct() gtk.IWidget
|
||||
}
|
||||
|
||||
type _combo struct {
|
||||
selected *int
|
||||
options []string
|
||||
change func(int)
|
||||
}
|
||||
|
||||
func Combo(selected *int, options []string, change func(int)) EntryValue {
|
||||
return &_combo{selected, options, change}
|
||||
}
|
||||
|
||||
func (c *_combo) Construct() gtk.IWidget {
|
||||
var combo, _ = gtk.ComboBoxTextNew()
|
||||
for _, opt := range c.options {
|
||||
combo.Append(opt, opt)
|
||||
}
|
||||
|
||||
combo.Connect("changed", func() {
|
||||
active := combo.GetActive()
|
||||
*c.selected = active
|
||||
|
||||
if c.change != nil {
|
||||
c.change(active)
|
||||
}
|
||||
})
|
||||
|
||||
combo.SetActive(*c.selected)
|
||||
combo.SetHAlign(gtk.ALIGN_END)
|
||||
combo.Show()
|
||||
|
||||
return combo
|
||||
}
|
||||
|
||||
func (c *_combo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(*c.selected)
|
||||
}
|
||||
|
||||
func (c *_combo) UnmarshalJSON(b []byte) error {
|
||||
var value int
|
||||
if err := json.Unmarshal(b, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*c.selected = value
|
||||
return nil
|
||||
}
|
||||
|
||||
type _switch struct {
|
||||
value *bool
|
||||
change func(bool)
|
||||
}
|
||||
|
||||
func Switch(value *bool, change func(bool)) EntryValue {
|
||||
return &_switch{value, change}
|
||||
}
|
||||
|
||||
func (s *_switch) Construct() gtk.IWidget {
|
||||
sw, _ := gtk.SwitchNew()
|
||||
sw.SetActive(*s.value)
|
||||
|
||||
sw.Connect("notify::active", func() {
|
||||
v := sw.GetActive()
|
||||
*s.value = v
|
||||
|
||||
if s.change != nil {
|
||||
s.change(v)
|
||||
}
|
||||
})
|
||||
|
||||
sw.SetHAlign(gtk.ALIGN_END)
|
||||
sw.Show()
|
||||
|
||||
return sw
|
||||
}
|
||||
|
||||
func (s *_switch) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(*s.value)
|
||||
}
|
||||
|
||||
func (s *_switch) UnmarshalJSON(b []byte) error {
|
||||
var value bool
|
||||
if err := json.Unmarshal(b, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*s.value = value
|
||||
return nil
|
||||
}
|
|
@ -4,19 +4,19 @@ import (
|
|||
"html"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/breadcrumb"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type header struct {
|
||||
*gtk.Box
|
||||
left *gtk.Box // TODO
|
||||
left *headerLeft // TODO
|
||||
right *headerRight
|
||||
}
|
||||
|
||||
func newHeader() *header {
|
||||
left, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
left.SetSizeRequest(leftMinWidth, -1)
|
||||
left := newHeaderLeft()
|
||||
left.Show()
|
||||
|
||||
right := newHeaderRight()
|
||||
|
@ -51,6 +51,27 @@ func (h *header) SetBreadcrumb(b breadcrumb.Breadcrumb) {
|
|||
)
|
||||
}
|
||||
|
||||
type headerLeft struct {
|
||||
*gtk.Box
|
||||
openmenu *gtk.MenuButton
|
||||
}
|
||||
|
||||
func newHeaderLeft() *headerLeft {
|
||||
openmenu := primitives.NewMenuActionButton([][2]string{
|
||||
{"Preferences", "app.preferences"},
|
||||
{"Quit", "app.quit"},
|
||||
})
|
||||
openmenu.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
box.PackStart(openmenu, false, false, 5)
|
||||
|
||||
return &headerLeft{
|
||||
Box: box,
|
||||
openmenu: openmenu,
|
||||
}
|
||||
}
|
||||
|
||||
type headerRight struct {
|
||||
*gtk.Box
|
||||
breadcrumb *gtk.Label
|
||||
|
|
|
@ -3,6 +3,7 @@ package input
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
|
@ -11,6 +12,17 @@ import (
|
|||
|
||||
const AvatarSize = 24
|
||||
|
||||
var showUser = true
|
||||
var currentRevealer = func(bool) {} // noop by default
|
||||
|
||||
func init() {
|
||||
// Bind this revealer in settings.
|
||||
config.AppearanceAdd("Show Username in Input", config.Switch(
|
||||
&showUser,
|
||||
func(b bool) { currentRevealer(b) },
|
||||
))
|
||||
}
|
||||
|
||||
type usernameContainer struct {
|
||||
*gtk.Revealer
|
||||
main *gtk.Box
|
||||
|
@ -48,6 +60,11 @@ func newUsernameContainer() *usernameContainer {
|
|||
rev.SetTransitionDuration(50)
|
||||
rev.Add(box)
|
||||
|
||||
// Bind the current global revealer to this revealer for settings. This
|
||||
// operation should be thread-safe, as everything is being done in the main
|
||||
// thread.
|
||||
currentRevealer = rev.SetRevealChild
|
||||
|
||||
return &usernameContainer{
|
||||
Revealer: rev,
|
||||
main: box,
|
||||
|
@ -56,6 +73,16 @@ func newUsernameContainer() *usernameContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func (u *usernameContainer) SetRevealChild(reveal bool) {
|
||||
// Only reveal if showUser is true.
|
||||
u.Revealer.SetRevealChild(reveal && showUser)
|
||||
}
|
||||
|
||||
// shouldReveal returns whether or not the container should reveal.
|
||||
func (u *usernameContainer) shouldReveal() bool {
|
||||
return !u.label.GetLabel().Empty() && showUser
|
||||
}
|
||||
|
||||
func (u *usernameContainer) Reset() {
|
||||
u.SetRevealChild(false)
|
||||
u.avatar.Reset()
|
||||
|
@ -67,7 +94,7 @@ func (u *usernameContainer) Update(session cchat.Session, sender cchat.ServerMes
|
|||
// Set the fallback username.
|
||||
u.label.SetLabelUnsafe(session.Name())
|
||||
// Reveal the name if it's not empty.
|
||||
u.SetRevealChild(!u.label.GetLabel().Empty())
|
||||
u.SetRevealChild(u.shouldReveal())
|
||||
|
||||
// Does sender (aka Server) implement ServerNickname? If yes, use it.
|
||||
if nicknamer, ok := sender.(cchat.ServerNickname); ok {
|
||||
|
@ -91,7 +118,7 @@ func (u *usernameContainer) SetLabel(content text.Rich) {
|
|||
u.label.SetLabelUnsafe(content)
|
||||
|
||||
// Reveal if the name is not empty.
|
||||
u.SetRevealChild(!u.label.GetLabel().Empty())
|
||||
u.SetRevealChild(u.shouldReveal())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/icons"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/compact"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/container/cozy"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/sadface"
|
||||
|
@ -16,55 +18,19 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ServerMessage combines Server and ServerMessage from cchat.
|
||||
type ServerMessage interface {
|
||||
cchat.Server
|
||||
cchat.ServerMessage
|
||||
}
|
||||
const (
|
||||
cozyMessage int = iota
|
||||
compactMessage
|
||||
)
|
||||
|
||||
type state struct {
|
||||
session cchat.Session
|
||||
server cchat.Server
|
||||
var msgIndex = cozyMessage
|
||||
|
||||
actioner cchat.ServerMessageActioner
|
||||
actions []string
|
||||
|
||||
current func() // stop callback
|
||||
author string
|
||||
}
|
||||
|
||||
func (s *state) Reset() {
|
||||
// If we still have the last server to leave, then leave it.
|
||||
if s.current != nil {
|
||||
s.current()
|
||||
}
|
||||
|
||||
// Lazy way to reset the state.
|
||||
*s = state{}
|
||||
}
|
||||
|
||||
func (s *state) hasActions() bool {
|
||||
return s.actioner != nil && len(s.actions) > 0
|
||||
}
|
||||
|
||||
// SessionID returns the session ID, or an empty string if there's no session.
|
||||
func (s *state) SessionID() string {
|
||||
if s.session != nil {
|
||||
return s.session.ID()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *state) bind(session cchat.Session, server ServerMessage) {
|
||||
s.session = session
|
||||
s.server = server
|
||||
if s.actioner, _ = server.(cchat.ServerMessageActioner); s.actioner != nil {
|
||||
s.actions = s.actioner.MessageActions()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) setcurrent(fn func()) {
|
||||
s.current = fn
|
||||
func init() {
|
||||
config.AppearanceAdd("Message Display", config.Combo(
|
||||
&msgIndex, // 0 or 1
|
||||
[]string{"Cozy", "Compact"},
|
||||
nil,
|
||||
))
|
||||
}
|
||||
|
||||
type View struct {
|
||||
|
@ -73,6 +39,7 @@ type View struct {
|
|||
|
||||
InputView *input.InputView
|
||||
Container container.Container
|
||||
contType int // msgIndex
|
||||
|
||||
// Inherit some useful methods.
|
||||
state
|
||||
|
@ -80,17 +47,16 @@ type View struct {
|
|||
|
||||
func NewView() *View {
|
||||
view := &View{}
|
||||
|
||||
// TODO: change
|
||||
view.InputView = input.NewView(view)
|
||||
// view.Container = compact.NewContainer(view)
|
||||
view.Container = cozy.NewContainer(view)
|
||||
|
||||
view.Box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
view.Box.PackStart(view.Container, true, true, 0)
|
||||
view.Box.PackStart(view.InputView, false, false, 0)
|
||||
view.Box.PackEnd(view.InputView, false, false, 0)
|
||||
view.Box.Show()
|
||||
|
||||
// Create the message container, which will use PackEnd to add the widget on
|
||||
// TOP of the input view.
|
||||
view.createMessageContainer()
|
||||
|
||||
// placeholder logo
|
||||
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256())
|
||||
logo.Show()
|
||||
|
@ -99,11 +65,34 @@ func NewView() *View {
|
|||
return view
|
||||
}
|
||||
|
||||
func (v *View) createMessageContainer() {
|
||||
// Remove the old message container.
|
||||
if v.Container != nil {
|
||||
v.Box.Remove(v.Container)
|
||||
}
|
||||
|
||||
// Update the container type.
|
||||
switch v.contType = msgIndex; msgIndex {
|
||||
case cozyMessage:
|
||||
v.Container = cozy.NewContainer(v)
|
||||
case compactMessage:
|
||||
v.Container = compact.NewContainer(v)
|
||||
}
|
||||
|
||||
// Add the new message container.
|
||||
v.Box.PackEnd(v.Container, true, true, 0)
|
||||
}
|
||||
|
||||
func (v *View) Reset() {
|
||||
v.state.Reset() // Reset the state variables.
|
||||
v.FaceView.Reset() // Switch back to the main screen.
|
||||
v.Container.Reset() // Clean all messages.
|
||||
v.InputView.Reset() // Reset the input.
|
||||
v.Container.Reset() // Clean all messages.
|
||||
|
||||
// Recreate the message container if the type is different.
|
||||
if v.contType != msgIndex {
|
||||
v.createMessageContainer()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinServer is not thread-safe, but it calls backend functions asynchronously.
|
||||
|
@ -217,3 +206,54 @@ func (v *View) makeActionItem(action, msgID string) menu.Item {
|
|||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// ServerMessage combines Server and ServerMessage from cchat.
|
||||
type ServerMessage interface {
|
||||
cchat.Server
|
||||
cchat.ServerMessage
|
||||
}
|
||||
|
||||
type state struct {
|
||||
session cchat.Session
|
||||
server cchat.Server
|
||||
|
||||
actioner cchat.ServerMessageActioner
|
||||
actions []string
|
||||
|
||||
current func() // stop callback
|
||||
author string
|
||||
}
|
||||
|
||||
func (s *state) Reset() {
|
||||
// If we still have the last server to leave, then leave it.
|
||||
if s.current != nil {
|
||||
s.current()
|
||||
}
|
||||
|
||||
// Lazy way to reset the state.
|
||||
*s = state{}
|
||||
}
|
||||
|
||||
func (s *state) hasActions() bool {
|
||||
return s.actioner != nil && len(s.actions) > 0
|
||||
}
|
||||
|
||||
// SessionID returns the session ID, or an empty string if there's no session.
|
||||
func (s *state) SessionID() string {
|
||||
if s.session != nil {
|
||||
return s.session.ID()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *state) bind(session cchat.Session, server ServerMessage) {
|
||||
s.session = session
|
||||
s.server = server
|
||||
if s.actioner, _ = server.(cchat.ServerMessageActioner); s.actioner != nil {
|
||||
s.actions = s.actioner.MessageActions()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) setcurrent(fn func()) {
|
||||
s.current = fn
|
||||
}
|
||||
|
|
|
@ -168,4 +168,60 @@ func NewTargetEntry(target string) gtk.TargetEntry {
|
|||
return *e
|
||||
}
|
||||
|
||||
// func
|
||||
// NewMenuActionButton is the same as NewActionButton, but it uses the
|
||||
// open-menu-symbolic icon.
|
||||
func NewMenuActionButton(actions [][2]string) *gtk.MenuButton {
|
||||
return NewActionButton("open-menu-symbolic", actions)
|
||||
}
|
||||
|
||||
// NewActionButton creates a new menu button that spawns a popover with the
|
||||
// listed actions.
|
||||
func NewActionButton(iconName string, actions [][2]string) *gtk.MenuButton {
|
||||
p, _ := gtk.PopoverNew(nil)
|
||||
p.SetSizeRequest(200, -1) // wide enough width
|
||||
ActionPopover(p, actions)
|
||||
|
||||
i, _ := gtk.ImageNew()
|
||||
i.SetProperty("icon-name", iconName)
|
||||
i.SetProperty("icon-size", gtk.ICON_SIZE_SMALL_TOOLBAR)
|
||||
i.Show()
|
||||
|
||||
b, _ := gtk.MenuButtonNew()
|
||||
b.SetHAlign(gtk.ALIGN_CENTER)
|
||||
b.SetPopover(p)
|
||||
b.Add(i)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// LabelTweaker is used for ActionPopover and other functions that may need to
|
||||
// change the alignment of children widgets.
|
||||
type LabelTweaker interface {
|
||||
SetUseMarkup(bool)
|
||||
SetHAlign(gtk.Align)
|
||||
SetXAlign(float64)
|
||||
}
|
||||
|
||||
var _ LabelTweaker = (*gtk.Label)(nil)
|
||||
|
||||
func ActionPopover(p *gtk.Popover, actions [][2]string) {
|
||||
var box, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
||||
|
||||
for _, action := range actions {
|
||||
b, _ := gtk.ModelButtonNew()
|
||||
b.SetLabel(action[0])
|
||||
b.SetActionName(action[1])
|
||||
b.Show()
|
||||
|
||||
// Set the label's alignment in a hacky way.
|
||||
c, _ := b.GetChild()
|
||||
l := c.(LabelTweaker)
|
||||
l.SetUseMarkup(true)
|
||||
l.SetHAlign(gtk.ALIGN_START)
|
||||
|
||||
box.PackStart(b, false, true, 0)
|
||||
}
|
||||
|
||||
box.Show()
|
||||
p.Add(box)
|
||||
}
|
||||
|
|
|
@ -127,6 +127,10 @@ func NewContainer(svc cchat.Service, ctrl Controller) *Container {
|
|||
return container
|
||||
}
|
||||
|
||||
func (c *Container) Sessions() []*session.Row {
|
||||
return c.children.Sessions()
|
||||
}
|
||||
|
||||
func (c *Container) AddSession(ses cchat.Session) *session.Row {
|
||||
srow := session.New(c, ses, c)
|
||||
c.children.AddSessionRow(ses.ID(), srow)
|
||||
|
|
|
@ -96,8 +96,14 @@ func (r *Row) ReconnectSession() {
|
|||
r.ctrl.RestoreSession(r, r.sessionID)
|
||||
}
|
||||
|
||||
// DisconnectSession disconnects the current session.
|
||||
// DisconnectSession disconnects the current session. It does nothing if the row
|
||||
// does not have a session active.
|
||||
func (r *Row) DisconnectSession() {
|
||||
// No-op if no session.
|
||||
if r.Session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Call the disconnect function from the controller first.
|
||||
r.ctrl.OnSessionDisconnect(r)
|
||||
|
||||
|
|
|
@ -7,3 +7,7 @@ headerbar { padding: 0; }
|
|||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
popover > box {
|
||||
margin: 6px;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package ui
|
|||
import (
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/config/preferences"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/service/auth"
|
||||
|
@ -44,8 +45,7 @@ type App struct {
|
|||
}
|
||||
|
||||
var (
|
||||
_ gts.Windower = (*App)(nil)
|
||||
_ gts.Headerer = (*App)(nil)
|
||||
_ gts.WindowHeaderer = (*App)(nil)
|
||||
_ service.Controller = (*App)(nil)
|
||||
)
|
||||
|
||||
|
@ -63,6 +63,10 @@ func NewApplication() *App {
|
|||
app.header.left.SetSizeRequest(width, -1)
|
||||
})
|
||||
|
||||
// Bind the preferences action for our GAction button in the header popover.
|
||||
// The action name for this is "app.preferences".
|
||||
gts.AddAppAction("preferences", preferences.SpawnPreferenceDialog)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
@ -114,6 +118,16 @@ func (app *App) AuthenticateSession(container *service.Container, svc cchat.Serv
|
|||
})
|
||||
}
|
||||
|
||||
// Destroy is called when the main window is destroyed or closed.
|
||||
func (app *App) Destroy() {
|
||||
// Disconnect everything.
|
||||
for _, service := range app.window.Services.Services {
|
||||
for _, session := range service.Sessions() {
|
||||
session.DisconnectSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) Header() gtk.IWidget {
|
||||
return app.header
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue