aerc/widgets/msglist.go
Jeffas f6216bb621 Add Mouseable
This adds the Mouseable interface. When this is implemented for a
component that item can accept and process mouseevents.

At the top level when a mouse event is received it is passed to the
grid's handler and then it trickles down until it reaches a component
that can actually handle it, such as the tablist, dirlist or msglist.

A mouse event is passed so that components can handle other things such
as scrolling with the mousewheel. The components themselves then perform
the necessary actions.

Clicking emails in the messagelist opens them in a new tab.

Textinputs can be clicked to position the cursor inside them.

Mouseevents are not forwarded to the terminal at the moment.

Elements which do not handle mouse events are not required to implement
the Mouseable interface.
2019-09-11 11:41:34 -04:00

301 lines
6.3 KiB
Go

package widgets
import (
"fmt"
"log"
"sort"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib"
"git.sr.ht/~sircmpwn/aerc/lib/format"
"git.sr.ht/~sircmpwn/aerc/lib/ui"
"git.sr.ht/~sircmpwn/aerc/models"
)
type MessageList struct {
ui.Invalidatable
conf *config.AercConfig
logger *log.Logger
height int
scroll int
nmsgs int
spinner *Spinner
store *lib.MessageStore
isInitalizing bool
aerc *Aerc
}
type msgSorter struct {
uids []uint32
store *lib.MessageStore
}
func (s *msgSorter) Len() int {
return len(s.uids)
}
func (s *msgSorter) Less(i, j int) bool {
msgI := s.store.Messages[s.uids[i]]
msgJ := s.store.Messages[s.uids[j]]
if msgI == nil && msgJ == nil {
return false // doesn't matter which order among nulls
} else if msgI == nil && msgJ != nil {
return true // say i is before j so we sort i to bottom
} else if msgI != nil && msgJ == nil {
return false // say i is after j so we sort j to bottom
}
return msgI.InternalDate.Before(msgJ.InternalDate)
}
func (s *msgSorter) Swap(i, j int) {
tmp := s.uids[i]
s.uids[i] = s.uids[j]
s.uids[j] = tmp
}
func NewMessageList(conf *config.AercConfig, logger *log.Logger, aerc *Aerc) *MessageList {
ml := &MessageList{
conf: conf,
logger: logger,
spinner: NewSpinner(&conf.Ui),
isInitalizing: true,
aerc: aerc,
}
ml.spinner.OnInvalidate(func(_ ui.Drawable) {
ml.Invalidate()
})
// TODO: stop spinner, probably
ml.spinner.Start()
return ml
}
func (ml *MessageList) Invalidate() {
ml.DoInvalidate(ml)
}
func (ml *MessageList) Draw(ctx *ui.Context) {
ml.height = ctx.Height()
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
store := ml.Store()
if store == nil {
if ml.isInitalizing {
ml.spinner.Draw(ctx)
return
} else {
ml.spinner.Stop()
ml.drawEmptyMessage(ctx)
return
}
}
var (
needsHeaders []uint32
row int = 0
)
uids := store.Uids()
sorter := msgSorter{uids: uids, store: store}
sort.Stable(&sorter)
for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
uid := uids[i]
msg := store.Messages[uid]
if row >= ctx.Height() {
break
}
if msg == nil {
needsHeaders = append(needsHeaders, uid)
ml.spinner.Draw(ctx.Subcontext(0, row, ctx.Width(), 1))
row += 1
continue
}
style := tcell.StyleDefault
// current row
if row == ml.store.SelectedIndex()-ml.scroll {
style = style.Reverse(true)
}
// deleted message
if _, ok := store.Deleted[msg.Uid]; ok {
style = style.Foreground(tcell.ColorGray)
}
// unread message
seen := false
for _, flag := range msg.Flags {
if flag == models.SeenFlag {
seen = true
}
}
if !seen {
style = style.Bold(true)
}
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
fmtStr, args, err := format.ParseMessageFormat(
ml.conf.Ui.IndexFormat,
ml.conf.Ui.TimestampFormat, "", i, msg)
if err != nil {
ctx.Printf(0, row, style, "%v", err)
} else {
line := fmt.Sprintf(fmtStr, args...)
line = runewidth.Truncate(line, ctx.Width(), "…")
ctx.Printf(0, row, style, "%s", line)
}
row += 1
}
if len(uids) == 0 {
ml.drawEmptyMessage(ctx)
}
if len(needsHeaders) != 0 {
store.FetchHeaders(needsHeaders, nil)
ml.spinner.Start()
} else {
ml.spinner.Stop()
}
}
func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
switch event := event.(type) {
case *tcell.EventMouse:
switch event.Buttons() {
case tcell.Button1:
if ml.aerc == nil {
return
}
selectedMsg, ok := ml.Clicked(localX, localY)
if ok {
ml.Select(selectedMsg)
acct := ml.aerc.SelectedAccount()
if acct.Messages().Empty() {
return
}
store := acct.Messages().Store()
msg := acct.Messages().Selected()
if msg == nil {
return
}
viewer := NewMessageViewer(acct, ml.aerc.Config(), store, msg)
ml.aerc.NewTab(viewer, msg.Envelope.Subject)
}
case tcell.WheelDown:
ml.store.Next()
ml.Scroll()
case tcell.WheelUp:
ml.store.Prev()
ml.Scroll()
}
}
}
func (ml *MessageList) Clicked(x, y int) (int, bool) {
store := ml.Store()
if store == nil || ml.nmsgs == 0 || y >= ml.nmsgs {
return 0, false
}
return y + ml.scroll, true
}
func (ml *MessageList) Height() int {
return ml.height
}
func (ml *MessageList) storeUpdate(store *lib.MessageStore) {
if ml.Store() != store {
return
}
uids := store.Uids()
if len(uids) > 0 {
// When new messages come in, advance the cursor accordingly
// Note that this assumes new messages are appended to the top, which
// isn't necessarily true once we implement SORT... ideally we'd look
// for the previously selected UID.
if len(uids) > ml.nmsgs && ml.nmsgs != 0 {
for i := 0; i < len(uids)-ml.nmsgs; i++ {
ml.Store().Next()
}
}
if len(uids) < ml.nmsgs && ml.nmsgs != 0 {
for i := 0; i < ml.nmsgs-len(uids); i++ {
ml.Store().Prev()
}
}
ml.nmsgs = len(uids)
}
ml.Scroll()
ml.Invalidate()
}
func (ml *MessageList) SetStore(store *lib.MessageStore) {
if ml.Store() != store {
ml.scroll = 0
}
ml.store = store
if store != nil {
ml.spinner.Stop()
ml.nmsgs = len(store.Uids())
store.OnUpdate(ml.storeUpdate)
} else {
ml.spinner.Start()
}
ml.Invalidate()
}
func (ml *MessageList) SetInitDone() {
ml.isInitalizing = false
}
func (ml *MessageList) Store() *lib.MessageStore {
return ml.store
}
func (ml *MessageList) Empty() bool {
store := ml.Store()
return store == nil || len(store.Uids()) == 0
}
func (ml *MessageList) Selected() *models.MessageInfo {
store := ml.Store()
uids := store.Uids()
return store.Messages[uids[len(uids)-ml.store.SelectedIndex()-1]]
}
func (ml *MessageList) Select(index int) {
store := ml.Store()
store.Select(index)
ml.Scroll()
}
func (ml *MessageList) Scroll() {
store := ml.Store()
if store == nil || len(store.Uids()) == 0 {
return
}
if ml.Height() != 0 {
// I'm too lazy to do the math right now
for store.SelectedIndex()-ml.scroll >= ml.Height() {
ml.scroll += 1
}
for store.SelectedIndex()-ml.scroll < 0 {
ml.scroll -= 1
}
}
ml.Invalidate()
}
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
msg := ml.conf.Ui.EmptyMessage
ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
tcell.StyleDefault, "%s", msg)
}