Start moving to mauview
This commit is contained in:
parent
ae36b9cddd
commit
8aa134b8b2
3
go.mod
3
go.mod
@ -11,9 +11,8 @@ require (
|
||||
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d
|
||||
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
maunium.net/go/mautrix v0.1.0-alpha.3
|
||||
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686
|
||||
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009
|
||||
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b
|
||||
)
|
||||
|
18
go.sum
18
go.sum
@ -10,6 +10,8 @@ github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC1
|
||||
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs=
|
||||
github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340/go.mod h1:SOLvOL4ybwgLJ6TYoX/rtaJ8EGOulH4XU7E9/TLrTCE=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
@ -18,29 +20,17 @@ github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8
|
||||
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
|
||||
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
|
||||
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
|
||||
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
maunium.net/go/mautrix v0.1.0-alpha.2 h1:NsLc5tyrp5tyrKTvFSmqcLi+FISQ+FsuWC/ycL08PzI=
|
||||
maunium.net/go/mautrix v0.1.0-alpha.2/go.mod h1:C8akEpHpmmO8gQhLvmInr3HujhUXyKvCoCAzFsxHjGE=
|
||||
maunium.net/go/mautrix v0.1.0-alpha.3 h1:kBz7M63hRetQnAnYK+gVmuSxsmZesX6xERphVgEn324=
|
||||
maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI=
|
||||
maunium.net/go/tcell v0.0.0-20190111210611-542340841245 h1:JIekRWZ4na6cZJa5VMwLFpuiGzmeX+5Xx+WRVOHQHuk=
|
||||
maunium.net/go/tcell v0.0.0-20190111210611-542340841245/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
|
||||
maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9 h1:aSZPhcBmGu8MIddPxWNksfjbXHmlRL0yUddwB9CP4s0=
|
||||
maunium.net/go/tcell v0.0.0-20190111212645-703b3f6ecec9/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
|
||||
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686 h1:kFgijToFPbMQGIMElizZGPQsffu+ZqO0olORXnfj1g4=
|
||||
maunium.net/go/mauview v0.0.0-20190325223341-4c387be4b686/go.mod h1:Uw1CaNoCs9id/rKBF3Eg9KhhFVg+3akJTebZomFKW+4=
|
||||
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009 h1:4lojuJmNSun1nUB67m3DGg+RkYg1MUO6aUxgKQU5iZk=
|
||||
maunium.net/go/tcell v0.0.0-20190111223412-5e74142cb009/go.mod h1:U+akxk8CP6vAWV74r2NOqEMMHw6kPGWTyvjzCtemxtM=
|
||||
maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0 h1:xHG0S9ExKp+6dkhasnK/fgO9mLHSSSqVoAymjyUtDdI=
|
||||
maunium.net/go/tview v0.0.0-20190111211351-2f23a5129af0/go.mod h1:ypYT6Dn71E7sVv6NxCjNo2cBJWJa257VSHCGOssGbV0=
|
||||
maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a h1:f4JVX4GHJH/wMcL9VACMVXT0eaWhxx9a7OT/NLcxsw8=
|
||||
maunium.net/go/tview v0.0.0-20190111212720-d6aa1eac1b9a/go.mod h1:CTOF8OnDeK31Wl25GXdbYzTDvvZoiazmKNZdwkVUmzE=
|
||||
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b h1:misvyPolT0TVGAtjc9Lr+oEYqGsV4YDByJVqZHxuu70=
|
||||
maunium.net/go/tview v0.0.0-20190111223510-de38190b095b/go.mod h1:Oi2eW32B8/cE7ZYXL6jyHMrXJL8ARDOQk/3aDvLEyVs=
|
||||
|
@ -23,19 +23,18 @@ import (
|
||||
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/matrix/rooms"
|
||||
"maunium.net/go/gomuks/ui/widget"
|
||||
)
|
||||
|
||||
type FuzzySearchModal struct {
|
||||
tview.Primitive
|
||||
mauview.Component
|
||||
|
||||
search *tview.InputField
|
||||
results *tview.TextView
|
||||
search *mauview.InputField
|
||||
results *mauview.TextView
|
||||
|
||||
matches fuzzy.Ranks
|
||||
selected int
|
||||
@ -43,39 +42,39 @@ type FuzzySearchModal struct {
|
||||
roomList []*rooms.Room
|
||||
roomTitles []string
|
||||
|
||||
parent *GomuksUI
|
||||
mainView *MainView
|
||||
parent *MainView
|
||||
}
|
||||
|
||||
func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal {
|
||||
fs := &FuzzySearchModal{
|
||||
parent: mainView.parent,
|
||||
mainView: mainView,
|
||||
parent: mainView,
|
||||
}
|
||||
|
||||
fs.InitList(mainView.rooms)
|
||||
|
||||
fs.search = tview.NewInputField().
|
||||
SetLabel("Room: ")
|
||||
fs.search.
|
||||
SetChangedFunc(fs.changeHandler).
|
||||
SetInputCapture(fs.keyHandler)
|
||||
fs.search = mauview.NewInputField().SetChangedFunc(fs.changeHandler)
|
||||
wrappedSearch := mauview.NewBox(fs.search).SetKeyCaptureFunc(fs.keyHandler)
|
||||
searchLabel := mauview.NewTextField().SetText("Room")
|
||||
combinedSearch := mauview.NewFlex().
|
||||
SetDirection(mauview.FlexColumn).
|
||||
AddFixedComponent(searchLabel, 5).
|
||||
AddProportionalComponent(wrappedSearch, 1)
|
||||
|
||||
fs.results = tview.NewTextView().
|
||||
SetRegions(true)
|
||||
fs.results.SetBorderPadding(1, 0, 0, 0)
|
||||
fs.results = mauview.NewTextView().SetRegions(true)
|
||||
|
||||
// Flex widget containing input box and results
|
||||
container := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(fs.search, 1, 0, true).
|
||||
AddItem(fs.results, 0, 1, false)
|
||||
container.
|
||||
container := mauview.NewBox(mauview.NewFlex().
|
||||
SetDirection(mauview.FlexRow).
|
||||
AddFixedComponent(combinedSearch, 1).
|
||||
AddProportionalComponent(fs.results, 1)).
|
||||
SetBorder(true).
|
||||
SetBorderPadding(1, 1, 1, 1).
|
||||
SetTitle("Quick Room Switcher")
|
||||
SetTitle("Quick Room Switcher").
|
||||
SetBlurCaptureFunc(func() bool {
|
||||
fs.parent.HideModal()
|
||||
return true
|
||||
})
|
||||
|
||||
fs.Primitive = widget.TransparentCenter(width, height, container)
|
||||
fs.Component = mauview.Center(container, width, height)
|
||||
|
||||
return fs
|
||||
}
|
||||
@ -96,7 +95,7 @@ func (fs *FuzzySearchModal) changeHandler(str string) {
|
||||
for _, match := range fs.matches {
|
||||
fmt.Fprintf(fs.results, `["%d"]%s[""]%s`, match.OriginalIndex, match.Target, "\n")
|
||||
}
|
||||
fs.parent.Render()
|
||||
fs.parent.parent.Render()
|
||||
fs.results.Highlight(strconv.Itoa(fs.matches[0].OriginalIndex))
|
||||
fs.results.ScrollToBeginning()
|
||||
} else {
|
||||
@ -105,13 +104,12 @@ func (fs *FuzzySearchModal) changeHandler(str string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey {
|
||||
func (fs *FuzzySearchModal) keyHandler(event mauview.KeyEvent) mauview.KeyEvent {
|
||||
highlights := fs.results.GetHighlights()
|
||||
switch event.Key() {
|
||||
case tcell.KeyEsc:
|
||||
// Close room finder
|
||||
fs.parent.views.RemovePage("fuzzy-search-modal")
|
||||
fs.parent.app.SetFocus(fs.parent.views)
|
||||
fs.parent.HideModal()
|
||||
return nil
|
||||
case tcell.KeyTab:
|
||||
// Cycle highlighted area to next match
|
||||
@ -125,10 +123,9 @@ func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey {
|
||||
// Switch room to currently selected room
|
||||
if len(highlights) > 0 {
|
||||
debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].OriginalIndex].GetTitle())
|
||||
fs.mainView.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex])
|
||||
fs.parent.SwitchRoom(fs.roomList[fs.matches[fs.selected].OriginalIndex].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].OriginalIndex])
|
||||
}
|
||||
fs.parent.views.RemovePage("fuzzy-search-modal")
|
||||
fs.parent.app.SetFocus(fs.parent.views)
|
||||
fs.parent.HideModal()
|
||||
fs.results.Clear()
|
||||
fs.search.SetText("")
|
||||
return nil
|
||||
|
@ -25,8 +25,8 @@ import (
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
@ -38,8 +38,6 @@ import (
|
||||
)
|
||||
|
||||
type MessageView struct {
|
||||
*tview.Box
|
||||
|
||||
parent *RoomView
|
||||
config *config.Config
|
||||
|
||||
@ -51,6 +49,8 @@ type MessageView struct {
|
||||
LoadingMessages bool
|
||||
|
||||
widestSender int
|
||||
width int
|
||||
height int
|
||||
prevWidth int
|
||||
prevHeight int
|
||||
prevMsgCount int
|
||||
@ -65,7 +65,6 @@ type MessageView struct {
|
||||
|
||||
func NewMessageView(parent *RoomView) *MessageView {
|
||||
return &MessageView{
|
||||
Box: tview.NewBox(),
|
||||
parent: parent,
|
||||
config: parent.config,
|
||||
|
||||
@ -174,7 +173,7 @@ func (view *MessageView) AddMessage(ifcMessage ifc.Message, direction ifc.Messag
|
||||
|
||||
view.updateWidestSender(message.Sender())
|
||||
|
||||
_, _, width, _ := view.GetRect()
|
||||
width := view.width
|
||||
bare := view.config.Preferences.BareMessageView
|
||||
if !bare {
|
||||
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
|
||||
@ -267,15 +266,15 @@ func (view *MessageView) replaceBuffer(original messages.UIMessage, new messages
|
||||
}
|
||||
|
||||
func (view *MessageView) recalculateBuffers() {
|
||||
_, _, width, height := view.GetRect()
|
||||
prefs := view.config.Preferences
|
||||
if !prefs.BareMessageView {
|
||||
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
|
||||
}
|
||||
recalculateMessageBuffers := width != view.prevWidth ||
|
||||
recalculateMessageBuffers := view.width != view.prevWidth ||
|
||||
view.prevPrefs.BareMessageView != prefs.BareMessageView ||
|
||||
view.prevPrefs.DisableImages != prefs.DisableImages
|
||||
if recalculateMessageBuffers || len(view.messages) != view.prevMsgCount {
|
||||
width := view.width
|
||||
if !prefs.BareMessageView {
|
||||
width -= view.TimestampWidth + TimestampSenderGap + view.widestSender + SenderMessageGap
|
||||
}
|
||||
view.textBuffer = []tstring.TString{}
|
||||
view.metaBuffer = []ifc.MessageMeta{}
|
||||
view.prevMsgCount = 0
|
||||
@ -290,8 +289,8 @@ func (view *MessageView) recalculateBuffers() {
|
||||
view.appendBuffer(message)
|
||||
}
|
||||
}
|
||||
view.prevHeight = height
|
||||
view.prevWidth = width
|
||||
view.prevHeight = view.height
|
||||
view.prevWidth = view.width
|
||||
view.prevPrefs = prefs
|
||||
}
|
||||
|
||||
@ -343,50 +342,56 @@ func (view *MessageView) handleUsernameClick(message ifc.MessageMeta, prevMessag
|
||||
return true
|
||||
}
|
||||
|
||||
func (view *MessageView) HandleClick(x, y int, button tcell.ButtonMask) bool {
|
||||
if button != tcell.Button1 {
|
||||
return false
|
||||
func (view *MessageView) OnMouseEvent(event mauview.MouseEvent) bool {
|
||||
switch event.Buttons() {
|
||||
case tcell.WheelUp:
|
||||
if view.IsAtTop() {
|
||||
go view.parent.parent.LoadHistory(view.parent.Room.ID)
|
||||
} else {
|
||||
view.AddScrollOffset(WheelScrollOffsetDiff)
|
||||
return true
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
view.AddScrollOffset(-WheelScrollOffsetDiff)
|
||||
view.parent.parent.MarkRead(view.parent)
|
||||
return true
|
||||
case tcell.Button1:
|
||||
x, y := event.Position()
|
||||
line := view.TotalHeight() - view.ScrollOffset - view.height + y
|
||||
if line < 0 || line >= view.TotalHeight() {
|
||||
return false
|
||||
}
|
||||
|
||||
message := view.metaBuffer[line]
|
||||
var prevMessage ifc.MessageMeta
|
||||
if y != 0 && line > 0 {
|
||||
prevMessage = view.metaBuffer[line-1]
|
||||
}
|
||||
|
||||
usernameX := view.TimestampWidth + TimestampSenderGap
|
||||
messageX := usernameX + view.widestSender + SenderMessageGap
|
||||
|
||||
if x >= messageX {
|
||||
return view.handleMessageClick(message)
|
||||
} else if x >= usernameX {
|
||||
return view.handleUsernameClick(message, prevMessage)
|
||||
}
|
||||
}
|
||||
|
||||
_, _, _, height := view.GetRect()
|
||||
line := view.TotalHeight() - view.ScrollOffset - height + y
|
||||
if line < 0 || line >= view.TotalHeight() {
|
||||
return false
|
||||
}
|
||||
|
||||
message := view.metaBuffer[line]
|
||||
var prevMessage ifc.MessageMeta
|
||||
if y != 0 && line > 0 {
|
||||
prevMessage = view.metaBuffer[line-1]
|
||||
}
|
||||
|
||||
usernameX := view.TimestampWidth + TimestampSenderGap
|
||||
messageX := usernameX + view.widestSender + SenderMessageGap
|
||||
|
||||
shouldRerender := false
|
||||
if x >= messageX {
|
||||
shouldRerender = view.handleMessageClick(message)
|
||||
} else if x >= usernameX {
|
||||
shouldRerender = view.handleUsernameClick(message, prevMessage)
|
||||
}
|
||||
|
||||
return shouldRerender
|
||||
return false
|
||||
}
|
||||
|
||||
const PaddingAtTop = 5
|
||||
|
||||
func (view *MessageView) AddScrollOffset(diff int) {
|
||||
_, _, _, height := view.GetRect()
|
||||
|
||||
totalHeight := view.TotalHeight()
|
||||
if diff >= 0 && view.ScrollOffset+diff >= totalHeight-height+PaddingAtTop {
|
||||
view.ScrollOffset = totalHeight - height + PaddingAtTop
|
||||
if diff >= 0 && view.ScrollOffset+diff >= totalHeight-view.height+PaddingAtTop {
|
||||
view.ScrollOffset = totalHeight - view.height + PaddingAtTop
|
||||
} else {
|
||||
view.ScrollOffset += diff
|
||||
}
|
||||
|
||||
if view.ScrollOffset > totalHeight-height+PaddingAtTop {
|
||||
view.ScrollOffset = totalHeight - height + PaddingAtTop
|
||||
if view.ScrollOffset > totalHeight-view.height+PaddingAtTop {
|
||||
view.ScrollOffset = totalHeight - view.height + PaddingAtTop
|
||||
}
|
||||
if view.ScrollOffset < 0 {
|
||||
view.ScrollOffset = 0
|
||||
@ -394,8 +399,7 @@ func (view *MessageView) AddScrollOffset(diff int) {
|
||||
}
|
||||
|
||||
func (view *MessageView) Height() int {
|
||||
_, _, _, height := view.GetRect()
|
||||
return height
|
||||
return view.height
|
||||
}
|
||||
|
||||
func (view *MessageView) TotalHeight() int {
|
||||
@ -403,9 +407,8 @@ func (view *MessageView) TotalHeight() int {
|
||||
}
|
||||
|
||||
func (view *MessageView) IsAtTop() bool {
|
||||
_, _, _, height := view.GetRect()
|
||||
totalHeight := len(view.textBuffer)
|
||||
return view.ScrollOffset >= totalHeight-height+PaddingAtTop
|
||||
return view.ScrollOffset >= totalHeight-view.height+PaddingAtTop
|
||||
}
|
||||
|
||||
const (
|
||||
@ -449,15 +452,14 @@ func (view *MessageView) calculateScrollBar(height int) (scrollBarHeight, scroll
|
||||
return
|
||||
}
|
||||
|
||||
func (view *MessageView) getIndexOffset(screen tcell.Screen, height, messageX int) (indexOffset int) {
|
||||
func (view *MessageView) getIndexOffset(screen mauview.Screen, height, messageX int) (indexOffset int) {
|
||||
indexOffset = view.TotalHeight() - view.ScrollOffset - height
|
||||
if indexOffset <= -PaddingAtTop {
|
||||
message := "Scroll up to load more messages."
|
||||
if view.LoadingMessages {
|
||||
message = "Loading more messages..."
|
||||
}
|
||||
_, y, _, _ := view.GetRect()
|
||||
widget.WriteLineSimpleColor(screen, message, messageX, y, tcell.ColorGreen)
|
||||
widget.WriteLineSimpleColor(screen, message, messageX, 0, tcell.ColorGreen)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -488,25 +490,25 @@ func (view *MessageView) CapturePlaintext(height int) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (view *MessageView) Draw(screen tcell.Screen) {
|
||||
x, y, _, height := view.GetRect()
|
||||
func (view *MessageView) Draw(screen mauview.Screen) {
|
||||
view.width, view.height = screen.Size()
|
||||
view.recalculateBuffers()
|
||||
|
||||
if view.TotalHeight() == 0 {
|
||||
widget.WriteLineSimple(screen, "It's quite empty in here.", x, y+height)
|
||||
widget.WriteLineSimple(screen, "It's quite empty in here.", 0, view.height)
|
||||
return
|
||||
}
|
||||
|
||||
usernameX := x + view.TimestampWidth + TimestampSenderGap
|
||||
usernameX := view.TimestampWidth + TimestampSenderGap
|
||||
messageX := usernameX + view.widestSender + SenderMessageGap
|
||||
separatorX := usernameX + view.widestSender + SenderSeparatorGap
|
||||
|
||||
bareMode := view.config.Preferences.BareMessageView
|
||||
if bareMode {
|
||||
messageX = x
|
||||
messageX = 0
|
||||
}
|
||||
|
||||
indexOffset := view.getIndexOffset(screen, height, messageX)
|
||||
indexOffset := view.getIndexOffset(screen, view.height, messageX)
|
||||
|
||||
if len(view.textBuffer) != len(view.metaBuffer) {
|
||||
debug.Printf("Unexpected text/meta buffer length mismatch: %d != %d.", len(view.textBuffer), len(view.metaBuffer))
|
||||
@ -514,13 +516,13 @@ func (view *MessageView) Draw(screen tcell.Screen) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollBarHeight, scrollBarPos := view.calculateScrollBar(height)
|
||||
scrollBarHeight, scrollBarPos := view.calculateScrollBar(view.height)
|
||||
|
||||
var prevMeta ifc.MessageMeta
|
||||
firstLine := true
|
||||
skippedLines := 0
|
||||
|
||||
for line := 0; line < height; line++ {
|
||||
for line := 0; line < view.height; line++ {
|
||||
index := indexOffset + line
|
||||
if index < 0 {
|
||||
skippedLines++
|
||||
@ -530,31 +532,32 @@ func (view *MessageView) Draw(screen tcell.Screen) {
|
||||
}
|
||||
|
||||
showScrollbar := line-skippedLines >= scrollBarPos-scrollBarHeight && line-skippedLines < scrollBarPos
|
||||
isTop := firstLine && view.ScrollOffset+height >= view.TotalHeight()
|
||||
isBottom := line == height-1 && view.ScrollOffset == 0
|
||||
isTop := firstLine && view.ScrollOffset+view.height >= view.TotalHeight()
|
||||
isBottom := line == view.height-1 && view.ScrollOffset == 0
|
||||
|
||||
borderChar, borderStyle := getScrollbarStyle(showScrollbar, isTop, isBottom)
|
||||
|
||||
firstLine = false
|
||||
|
||||
if !bareMode {
|
||||
screen.SetContent(separatorX, y+line, borderChar, nil, borderStyle)
|
||||
screen.SetContent(separatorX, line, borderChar, nil, borderStyle)
|
||||
}
|
||||
|
||||
text, meta := view.textBuffer[index], view.metaBuffer[index]
|
||||
if meta != prevMeta {
|
||||
if len(meta.FormatTime()) > 0 {
|
||||
widget.WriteLineSimpleColor(screen, meta.FormatTime(), x, y+line, meta.TimestampColor())
|
||||
widget.WriteLineSimpleColor(screen, meta.FormatTime(), 0, line, meta.TimestampColor())
|
||||
}
|
||||
if !bareMode && (prevMeta == nil || meta.Sender() != prevMeta.Sender()) {
|
||||
widget.WriteLineColor(
|
||||
screen, tview.AlignRight, meta.Sender(),
|
||||
usernameX, y+line, view.widestSender,
|
||||
screen, mauview.AlignRight, meta.Sender(),
|
||||
usernameX, line, view.widestSender,
|
||||
meta.SenderColor())
|
||||
}
|
||||
prevMeta = meta
|
||||
}
|
||||
|
||||
text.Draw(screen, messageX, y+line)
|
||||
text.Draw(screen, messageX, line)
|
||||
}
|
||||
debug.Print(screen)
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ func (msg *ImageMessage) updateData() {
|
||||
debug.Print("Loading image:", msg.Homeserver, msg.FileID)
|
||||
data, _, _, err := msg.matrix.Download(fmt.Sprintf("mxc://%s/%s", msg.Homeserver, msg.FileID))
|
||||
if err != nil {
|
||||
debug.Print("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
|
||||
debug.Printf("Failed to download image %s/%s: %v", msg.Homeserver, msg.FileID, err)
|
||||
return
|
||||
}
|
||||
debug.Print("Image", msg.Homeserver, msg.FileID, "loaded.")
|
||||
|
@ -102,14 +102,24 @@ func ParseMessage(matrix ifc.MatrixContainer, room *rooms.Room, evt *mautrix.Eve
|
||||
}
|
||||
if len(evt.Content.GetReplyTo()) > 0 {
|
||||
evt.Content.RemoveReplyFallback()
|
||||
replyToEvt, _ := matrix.Client().GetEvent(room.ID, evt.Content.GetReplyTo())
|
||||
replyToEvt.Content.RemoveReplyFallback()
|
||||
if len(replyToEvt.Content.FormattedBody) == 0 {
|
||||
replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
|
||||
roomID := evt.Content.RelatesTo.InReplyTo.RoomID
|
||||
if len(roomID) == 0 {
|
||||
roomID = room.ID
|
||||
}
|
||||
replyToEvt, _ := matrix.Client().GetEvent(roomID, evt.Content.GetReplyTo())
|
||||
if replyToEvt != nil {
|
||||
replyToEvt.Content.RemoveReplyFallback()
|
||||
if len(replyToEvt.Content.FormattedBody) == 0 {
|
||||
replyToEvt.Content.FormattedBody = html.EscapeString(replyToEvt.Content.Body)
|
||||
}
|
||||
evt.Content.FormattedBody = fmt.Sprintf(
|
||||
"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
|
||||
replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
|
||||
} else {
|
||||
evt.Content.FormattedBody = fmt.Sprintf(
|
||||
"In reply to unknown event https://matrix.to/#/%[1]s/%[2]s<br/>%[3]s",
|
||||
roomID, evt.Content.GetReplyTo(), evt.Content.FormattedBody)
|
||||
}
|
||||
evt.Content.FormattedBody = fmt.Sprintf(
|
||||
"In reply to <a href='https://matrix.to/#/%[1]s'>%[1]s</a><blockquote>%[2]s</blockquote><br/>%[3]s",
|
||||
replyToEvt.Sender, replyToEvt.Content.FormattedBody, evt.Content.FormattedBody)
|
||||
}
|
||||
ts := unixToTime(evt.Timestamp)
|
||||
switch evt.Content.MsgType {
|
||||
|
@ -18,6 +18,7 @@ package tstring
|
||||
|
||||
import (
|
||||
"github.com/mattn/go-runewidth"
|
||||
"maunium.net/go/mauview"
|
||||
|
||||
"maunium.net/go/tcell"
|
||||
)
|
||||
@ -43,7 +44,7 @@ func (cell Cell) RuneWidth() int {
|
||||
return runewidth.RuneWidth(cell.Char)
|
||||
}
|
||||
|
||||
func (cell Cell) Draw(screen tcell.Screen, x, y int) (chWidth int) {
|
||||
func (cell Cell) Draw(screen mauview.Screen, x, y int) (chWidth int) {
|
||||
chWidth = cell.RuneWidth()
|
||||
for runeWidthOffset := 0; runeWidthOffset < chWidth; runeWidthOffset++ {
|
||||
screen.SetContent(x+runeWidthOffset, y, cell.Char, nil, cell.Style)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"maunium.net/go/mauview"
|
||||
|
||||
"maunium.net/go/tcell"
|
||||
)
|
||||
@ -181,7 +182,7 @@ func (str TString) AdjustStyleFull(fn func(tcell.Style) tcell.Style) {
|
||||
str.AdjustStyle(0, len(str), fn)
|
||||
}
|
||||
|
||||
func (str TString) Draw(screen tcell.Screen, x, y int) {
|
||||
func (str TString) Draw(screen mauview.Screen, x, y int) {
|
||||
offsetX := 0
|
||||
for _, cell := range str {
|
||||
offsetX += cell.Draw(screen, x+offsetX, y)
|
||||
|
@ -21,15 +21,15 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/matrix/rooms"
|
||||
)
|
||||
|
||||
type RoomList struct {
|
||||
*tview.Box
|
||||
parent *MainView
|
||||
|
||||
// The list of tags in display order.
|
||||
tags []string
|
||||
@ -40,6 +40,8 @@ type RoomList struct {
|
||||
selectedTag string
|
||||
|
||||
scrollOffset int
|
||||
height int
|
||||
width int
|
||||
|
||||
// The item main text color.
|
||||
mainTextColor tcell.Color
|
||||
@ -49,9 +51,10 @@ type RoomList struct {
|
||||
selectedBackgroundColor tcell.Color
|
||||
}
|
||||
|
||||
func NewRoomList() *RoomList {
|
||||
func NewRoomList(parent *MainView) *RoomList {
|
||||
list := &RoomList{
|
||||
Box: tview.NewBox(),
|
||||
parent: parent,
|
||||
|
||||
items: make(map[string]*TagRoomList),
|
||||
tags: []string{"m.favourite", "net.maunium.gomuks.fake.direct", "", "m.lowpriority"},
|
||||
|
||||
@ -182,11 +185,10 @@ func (list *RoomList) SetSelected(tag string, room *rooms.Room) {
|
||||
list.selected = room
|
||||
list.selectedTag = tag
|
||||
pos := list.index(tag, room)
|
||||
_, _, _, height := list.GetRect()
|
||||
if pos <= list.scrollOffset {
|
||||
list.scrollOffset = pos - 1
|
||||
} else if pos >= list.scrollOffset+height {
|
||||
list.scrollOffset = pos - height + 1
|
||||
} else if pos >= list.scrollOffset+list.height {
|
||||
list.scrollOffset = pos - list.height + 1
|
||||
}
|
||||
if list.scrollOffset < 0 {
|
||||
list.scrollOffset = 0
|
||||
@ -208,10 +210,9 @@ func (list *RoomList) SelectedRoom() *rooms.Room {
|
||||
|
||||
func (list *RoomList) AddScrollOffset(offset int) {
|
||||
list.scrollOffset += offset
|
||||
_, _, _, viewHeight := list.GetRect()
|
||||
contentHeight := list.ContentHeight()
|
||||
if list.scrollOffset > contentHeight-viewHeight {
|
||||
list.scrollOffset = contentHeight - viewHeight
|
||||
if list.scrollOffset > contentHeight-list.height {
|
||||
list.scrollOffset = contentHeight - list.height
|
||||
}
|
||||
if list.scrollOffset < 0 {
|
||||
list.scrollOffset = 0
|
||||
@ -372,10 +373,39 @@ func (list *RoomList) ContentHeight() (height int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Room) {
|
||||
func (list *RoomList) OnKeyEvent(event mauview.KeyEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (list *RoomList) OnPasteEvent(event mauview.PasteEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (list *RoomList) OnMouseEvent(event mauview.MouseEvent) bool {
|
||||
switch event.Buttons() {
|
||||
case tcell.WheelUp:
|
||||
list.AddScrollOffset(-WheelScrollOffsetDiff)
|
||||
case tcell.WheelDown:
|
||||
list.AddScrollOffset(WheelScrollOffsetDiff)
|
||||
case tcell.Button1:
|
||||
x, y := event.Position()
|
||||
return list.clickRoom(y, x, event.Modifiers() == tcell.ModCtrl)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (list *RoomList) Focus() {
|
||||
|
||||
}
|
||||
|
||||
func (list *RoomList) Blur() {
|
||||
|
||||
}
|
||||
|
||||
func (list *RoomList) clickRoom(line, column int, mod bool) bool {
|
||||
line += list.scrollOffset
|
||||
if line < 0 {
|
||||
return "", nil
|
||||
return false
|
||||
}
|
||||
for _, tag := range list.tags {
|
||||
trl := list.items[tag]
|
||||
@ -391,7 +421,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
|
||||
if line < 0 {
|
||||
break
|
||||
} else if line < trl.Length() {
|
||||
return tag, trl.Visible()[trl.Length()-1-line].Room
|
||||
list.parent.SwitchRoom(tag, trl.Visible()[trl.Length()-1-line].Room)
|
||||
return true
|
||||
}
|
||||
|
||||
// Tag items
|
||||
@ -405,10 +436,9 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
|
||||
if mod {
|
||||
diff = 100
|
||||
}
|
||||
_, _, width, _ := list.GetRect()
|
||||
if column <= 6 && hasLess {
|
||||
trl.maxShown -= diff
|
||||
} else if column >= width-6 && hasMore {
|
||||
} else if column >= list.width-6 && hasMore {
|
||||
trl.maxShown += diff
|
||||
}
|
||||
if trl.maxShown < 10 {
|
||||
@ -420,9 +450,8 @@ func (list *RoomList) HandleClick(column, line int, mod bool) (string, *rooms.Ro
|
||||
// Tag footer
|
||||
line--
|
||||
}
|
||||
return "", nil
|
||||
return false
|
||||
}
|
||||
|
||||
var nsRegex = regexp.MustCompile("^[a-z]\\.[a-z](?:\\.[a-z])*$")
|
||||
|
||||
func (list *RoomList) GetTagDisplayName(tag string) string {
|
||||
@ -445,11 +474,10 @@ func (list *RoomList) GetTagDisplayName(tag string) string {
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (list *RoomList) Draw(screen tcell.Screen) {
|
||||
list.Box.Draw(screen)
|
||||
|
||||
x, y, width, height := list.GetRect()
|
||||
yLimit := y + height
|
||||
func (list *RoomList) Draw(screen mauview.Screen) {
|
||||
list.width, list.height = screen.Size()
|
||||
y := 0
|
||||
yLimit := y + list.height
|
||||
y -= list.scrollOffset
|
||||
|
||||
// Draw the list items.
|
||||
@ -464,8 +492,7 @@ func (list *RoomList) Draw(screen tcell.Screen) {
|
||||
if y+renderHeight >= yLimit {
|
||||
renderHeight = yLimit - y
|
||||
}
|
||||
trl.SetRect(x, y, width, renderHeight)
|
||||
trl.Draw(screen)
|
||||
trl.Draw(mauview.NewProxyScreen(screen, 0, y, list.width, renderHeight))
|
||||
y += renderHeight
|
||||
if y >= yLimit {
|
||||
break
|
||||
|
177
ui/room-view.go
177
ui/room-view.go
@ -26,9 +26,10 @@ import (
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/interface"
|
||||
@ -39,16 +40,23 @@ import (
|
||||
)
|
||||
|
||||
type RoomView struct {
|
||||
*tview.Box
|
||||
|
||||
topic *tview.TextView
|
||||
topic *mauview.TextView
|
||||
content *MessageView
|
||||
status *tview.TextView
|
||||
userList *tview.TextView
|
||||
status *mauview.TextField
|
||||
userList *mauview.TextView
|
||||
ulBorder *widget.Border
|
||||
input *widget.AdvancedInputField
|
||||
input *mauview.InputArea
|
||||
Room *rooms.Room
|
||||
|
||||
topicScreen *mauview.ProxyScreen
|
||||
contentScreen *mauview.ProxyScreen
|
||||
statusScreen *mauview.ProxyScreen
|
||||
inputScreen *mauview.ProxyScreen
|
||||
ulBorderScreen *mauview.ProxyScreen
|
||||
ulScreen *mauview.ProxyScreen
|
||||
|
||||
prevScreen mauview.Screen
|
||||
|
||||
parent *MainView
|
||||
config *config.Config
|
||||
|
||||
@ -63,27 +71,34 @@ type RoomView struct {
|
||||
|
||||
func NewRoomView(parent *MainView, room *rooms.Room) *RoomView {
|
||||
view := &RoomView{
|
||||
Box: tview.NewBox(),
|
||||
topic: tview.NewTextView(),
|
||||
status: tview.NewTextView(),
|
||||
userList: tview.NewTextView(),
|
||||
topic: mauview.NewTextView(),
|
||||
status: mauview.NewTextField(),
|
||||
userList: mauview.NewTextView(),
|
||||
ulBorder: widget.NewBorder(),
|
||||
input: widget.NewAdvancedInputField(),
|
||||
input: mauview.NewInputArea(),
|
||||
Room: room,
|
||||
|
||||
topicScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: 0, Height: TopicBarHeight},
|
||||
contentScreen: &mauview.ProxyScreen{OffsetX: 0, OffsetY: StatusBarHeight},
|
||||
statusScreen: &mauview.ProxyScreen{OffsetX: 0, Height: StatusBarHeight},
|
||||
inputScreen: &mauview.ProxyScreen{OffsetX: 0},
|
||||
ulBorderScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListBorderWidth},
|
||||
ulScreen: &mauview.ProxyScreen{OffsetY: StatusBarHeight, Width: UserListWidth},
|
||||
|
||||
parent: parent,
|
||||
config: parent.config,
|
||||
}
|
||||
view.content = NewMessageView(view)
|
||||
|
||||
view.input.
|
||||
SetFieldBackgroundColor(tcell.ColorDefault).
|
||||
SetBackgroundColor(tcell.ColorDefault).
|
||||
SetPlaceholder("Send a message...").
|
||||
SetPlaceholderExtColor(tcell.ColorGray).
|
||||
SetPlaceholderTextColor(tcell.ColorGray).
|
||||
SetTabCompleteFunc(view.InputTabComplete)
|
||||
|
||||
view.topic.
|
||||
SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)).
|
||||
SetBackgroundColor(tcell.ColorDarkGreen)
|
||||
SetTextColor(tcell.ColorDarkGreen)
|
||||
|
||||
view.status.SetBackgroundColor(tcell.ColorDimGray)
|
||||
|
||||
@ -106,26 +121,13 @@ func (view *RoomView) LoadHistory(matrix ifc.MatrixContainer, dir string) (int,
|
||||
return view.MessageView().LoadHistory(matrix, view.logPath(dir))
|
||||
}
|
||||
|
||||
func (view *RoomView) SetInputCapture(fn func(room *RoomView, event *tcell.EventKey) *tcell.EventKey) *RoomView {
|
||||
view.input.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
return fn(view, event)
|
||||
})
|
||||
return view
|
||||
}
|
||||
|
||||
func (view *RoomView) SetMouseCapture(fn func(room *RoomView, event *tcell.EventMouse) *tcell.EventMouse) *RoomView {
|
||||
view.input.SetMouseCapture(func(event *tcell.EventMouse) *tcell.EventMouse {
|
||||
return fn(view, event)
|
||||
})
|
||||
return view
|
||||
}
|
||||
|
||||
func (view *RoomView) SetInputSubmitFunc(fn func(room *RoomView, text string)) *RoomView {
|
||||
view.input.SetDoneFunc(func(key tcell.Key) {
|
||||
// FIXME
|
||||
/*view.input.SetDoneFunc(func(key tcell.Key) {
|
||||
if key == tcell.KeyEnter {
|
||||
fn(view, view.input.GetText())
|
||||
}
|
||||
})
|
||||
})*/
|
||||
return view
|
||||
}
|
||||
|
||||
@ -137,7 +139,7 @@ func (view *RoomView) SetInputChangedFunc(fn func(room *RoomView, text string))
|
||||
}
|
||||
|
||||
func (view *RoomView) SetInputText(newText string) *RoomView {
|
||||
view.input.SetText(newText)
|
||||
view.input.SetTextAndMoveCursor(newText)
|
||||
return view
|
||||
}
|
||||
|
||||
@ -145,12 +147,12 @@ func (view *RoomView) GetInputText() string {
|
||||
return view.input.GetText()
|
||||
}
|
||||
|
||||
func (view *RoomView) GetInputField() *widget.AdvancedInputField {
|
||||
return view.input
|
||||
func (view *RoomView) Focus() {
|
||||
view.input.Focus()
|
||||
}
|
||||
|
||||
func (view *RoomView) Focus(delegate func(p tview.Primitive)) {
|
||||
delegate(view.input)
|
||||
func (view *RoomView) Blur() {
|
||||
view.input.Blur()
|
||||
}
|
||||
|
||||
func (view *RoomView) GetStatus() string {
|
||||
@ -169,7 +171,7 @@ func (view *RoomView) GetStatus() string {
|
||||
buf.WriteString("Typing: " + view.typing[0])
|
||||
buf.WriteString(" - ")
|
||||
} else if len(view.typing) > 1 {
|
||||
fmt.Fprintf(&buf,
|
||||
_, _ = fmt.Fprintf(&buf,
|
||||
"Typing: %s and %s - ",
|
||||
strings.Join(view.typing[:len(view.typing)-1], ", "), view.typing[len(view.typing)-1])
|
||||
}
|
||||
@ -177,64 +179,91 @@ func (view *RoomView) GetStatus() string {
|
||||
return strings.TrimSuffix(buf.String(), " - ")
|
||||
}
|
||||
|
||||
func (view *RoomView) Draw(screen tcell.Screen) {
|
||||
x, y, width, height := view.GetRect()
|
||||
// Constants defining the size of the room view grid.
|
||||
const (
|
||||
UserListBorderWidth = 1
|
||||
UserListWidth = 20
|
||||
StaticHorizontalSpace = UserListBorderWidth + UserListWidth
|
||||
|
||||
TopicBarHeight = 1
|
||||
StatusBarHeight = 1
|
||||
InputBarHeight = 1
|
||||
StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight
|
||||
|
||||
MaxInputHeight
|
||||
)
|
||||
|
||||
func (view *RoomView) Draw(screen mauview.Screen) {
|
||||
width, height := screen.Size()
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Constants defining the size of the room view grid.
|
||||
const (
|
||||
UserListBorderWidth = 1
|
||||
UserListWidth = 20
|
||||
StaticHorizontalSpace = UserListBorderWidth + UserListWidth
|
||||
|
||||
TopicBarHeight = 1
|
||||
StatusBarHeight = 1
|
||||
InputBarHeight = 1
|
||||
StaticVerticalSpace = TopicBarHeight + StatusBarHeight + InputBarHeight
|
||||
)
|
||||
|
||||
// Calculate actual grid based on view rectangle and constants defined above.
|
||||
var (
|
||||
contentHeight = height - StaticVerticalSpace
|
||||
contentWidth = width - StaticHorizontalSpace
|
||||
|
||||
userListBorderColumn = x + contentWidth
|
||||
userListColumn = userListBorderColumn + UserListBorderWidth
|
||||
|
||||
topicRow = y
|
||||
contentRow = topicRow + TopicBarHeight
|
||||
statusRow = contentRow + contentHeight
|
||||
inputRow = statusRow + StatusBarHeight
|
||||
)
|
||||
if view.config.Preferences.HideUserList {
|
||||
contentWidth = width
|
||||
}
|
||||
|
||||
// Update the rectangles of all the children.
|
||||
view.topic.SetRect(x, topicRow, width, TopicBarHeight)
|
||||
view.content.SetRect(x, contentRow, contentWidth, contentHeight)
|
||||
view.status.SetRect(x, statusRow, width, StatusBarHeight)
|
||||
if !view.config.Preferences.HideUserList && userListColumn > x {
|
||||
view.userList.SetRect(userListColumn, contentRow, UserListWidth, contentHeight)
|
||||
view.ulBorder.SetRect(userListBorderColumn, contentRow, UserListBorderWidth, contentHeight)
|
||||
if view.prevScreen != screen {
|
||||
view.topicScreen.Parent = screen
|
||||
view.contentScreen.Parent = screen
|
||||
view.statusScreen.Parent = screen
|
||||
view.inputScreen.Parent = screen
|
||||
view.ulBorderScreen.Parent = screen
|
||||
view.ulScreen.Parent = screen
|
||||
view.prevScreen = screen
|
||||
}
|
||||
view.input.SetRect(x, inputRow, width, InputBarHeight)
|
||||
|
||||
view.input.PrepareDraw(width)
|
||||
inputHeight := view.input.GetTextHeight()
|
||||
if inputHeight > MaxInputHeight {
|
||||
inputHeight = MaxInputHeight
|
||||
} else if inputHeight < 1 {
|
||||
inputHeight = 1
|
||||
}
|
||||
contentHeight -= inputHeight
|
||||
|
||||
view.topicScreen.Width = width
|
||||
view.contentScreen.Width = contentWidth
|
||||
view.contentScreen.Height = contentHeight
|
||||
view.statusScreen.OffsetY = view.contentScreen.YEnd()
|
||||
view.statusScreen.Width = width
|
||||
view.inputScreen.Width = width
|
||||
view.inputScreen.OffsetY = view.statusScreen.YEnd()
|
||||
view.inputScreen.Height = inputHeight
|
||||
view.ulBorderScreen.OffsetX = view.contentScreen.XEnd()
|
||||
view.ulBorderScreen.Height = contentHeight
|
||||
view.ulScreen.OffsetX = view.ulBorderScreen.XEnd()
|
||||
view.ulScreen.Height = contentHeight
|
||||
|
||||
// Draw everything
|
||||
view.Box.Draw(screen)
|
||||
view.topic.Draw(screen)
|
||||
view.content.Draw(screen)
|
||||
view.topic.Draw(view.topicScreen)
|
||||
view.content.Draw(view.contentScreen)
|
||||
view.status.SetText(view.GetStatus())
|
||||
view.status.Draw(screen)
|
||||
view.input.Draw(screen)
|
||||
view.status.Draw(view.statusScreen)
|
||||
view.input.Draw(view.inputScreen)
|
||||
if !view.config.Preferences.HideUserList {
|
||||
view.ulBorder.Draw(screen)
|
||||
view.userList.Draw(screen)
|
||||
view.ulBorder.Draw(view.ulBorderScreen)
|
||||
view.userList.Draw(view.ulScreen)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *RoomView) OnKeyEvent(event mauview.KeyEvent) bool {
|
||||
return view.input.OnKeyEvent(event)
|
||||
}
|
||||
|
||||
func (view *RoomView) OnPasteEvent(event mauview.PasteEvent) bool {
|
||||
return view.input.OnPasteEvent(event)
|
||||
}
|
||||
|
||||
func (view *RoomView) OnMouseEvent(event mauview.MouseEvent) bool {
|
||||
return view.content.OnMouseEvent(event)
|
||||
}
|
||||
|
||||
func (view *RoomView) SetCompletions(completions []string) {
|
||||
view.completions.list = completions
|
||||
view.completions.textCache = view.input.GetText()
|
||||
|
@ -21,8 +21,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/matrix/rooms"
|
||||
"maunium.net/go/gomuks/ui/widget"
|
||||
@ -44,7 +44,7 @@ func NewDefaultOrderedRoom(room *rooms.Room) *OrderedRoom {
|
||||
return NewOrderedRoom("0.5", room)
|
||||
}
|
||||
|
||||
func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineWidth int, isSelected bool) {
|
||||
func (or *OrderedRoom) Draw(roomList *RoomList, screen mauview.Screen, x, y, lineWidth int, isSelected bool) {
|
||||
style := tcell.StyleDefault.
|
||||
Foreground(roomList.mainTextColor).
|
||||
Bold(or.HasNewMessages())
|
||||
@ -56,7 +56,7 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
|
||||
|
||||
unreadCount := or.UnreadCount()
|
||||
|
||||
widget.WriteLinePadded(screen, tview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
|
||||
widget.WriteLinePadded(screen, mauview.AlignLeft, or.GetTitle(), x, y, lineWidth, style)
|
||||
|
||||
if unreadCount > 0 {
|
||||
unreadMessageCount := "99+"
|
||||
@ -67,13 +67,13 @@ func (or *OrderedRoom) Draw(roomList *RoomList, screen tcell.Screen, x, y, lineW
|
||||
unreadMessageCount += "!"
|
||||
}
|
||||
unreadMessageCount = fmt.Sprintf("(%s)", unreadMessageCount)
|
||||
widget.WriteLine(screen, tview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
|
||||
widget.WriteLine(screen, mauview.AlignRight, unreadMessageCount, x+lineWidth-7, y, 7, style)
|
||||
lineWidth -= len(unreadMessageCount)
|
||||
}
|
||||
}
|
||||
|
||||
type TagRoomList struct {
|
||||
*tview.Box
|
||||
mauview.NoopEventHandler
|
||||
rooms []*OrderedRoom
|
||||
maxShown int
|
||||
name string
|
||||
@ -83,7 +83,6 @@ type TagRoomList struct {
|
||||
|
||||
func NewTagRoomList(parent *RoomList, name string, rooms ...*OrderedRoom) *TagRoomList {
|
||||
return &TagRoomList{
|
||||
Box: tview.NewBox(),
|
||||
maxShown: 10,
|
||||
rooms: rooms,
|
||||
name: name,
|
||||
@ -246,41 +245,40 @@ func (trl *TagRoomList) RenderHeight() int {
|
||||
return height
|
||||
}
|
||||
|
||||
func (trl *TagRoomList) DrawHeader(screen tcell.Screen) {
|
||||
x, y, width, _ := trl.GetRect()
|
||||
func (trl *TagRoomList) DrawHeader(screen mauview.Screen) {
|
||||
width, _ := screen.Size()
|
||||
roomCount := strconv.Itoa(trl.TotalLength())
|
||||
|
||||
// Draw tag name
|
||||
displayNameWidth := width - 1 - len(roomCount)
|
||||
widget.WriteLine(screen, tview.AlignLeft, trl.displayname, x, y, displayNameWidth, TagDisplayNameStyle)
|
||||
widget.WriteLine(screen, mauview.AlignLeft, trl.displayname, 0, 0, displayNameWidth, TagDisplayNameStyle)
|
||||
|
||||
// Draw tag room count
|
||||
roomCountX := x + len(trl.displayname) + 1
|
||||
roomCountX := len(trl.displayname) + 1
|
||||
roomCountWidth := width - 2 - len(trl.displayname)
|
||||
widget.WriteLine(screen, tview.AlignLeft, roomCount, roomCountX, y, roomCountWidth, TagRoomCountStyle)
|
||||
widget.WriteLine(screen, mauview.AlignLeft, roomCount, roomCountX, 0, roomCountWidth, TagRoomCountStyle)
|
||||
}
|
||||
|
||||
func (trl *TagRoomList) Draw(screen tcell.Screen) {
|
||||
func (trl *TagRoomList) Draw(screen mauview.Screen) {
|
||||
if len(trl.displayname) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
trl.DrawHeader(screen)
|
||||
|
||||
x, y, width, height := trl.GetRect()
|
||||
yLimit := y + height
|
||||
width, height := screen.Size()
|
||||
|
||||
items := trl.Visible()
|
||||
|
||||
if trl.IsCollapsed() {
|
||||
screen.SetCell(x+width-1, y, tcell.StyleDefault, '▶')
|
||||
screen.SetCell(width-1, 0, tcell.StyleDefault, '▶')
|
||||
return
|
||||
}
|
||||
screen.SetCell(x+width-1, y, tcell.StyleDefault, '▼')
|
||||
screen.SetCell(width-1, 0, tcell.StyleDefault, '▼')
|
||||
|
||||
offsetY := 1
|
||||
y := 1
|
||||
for i := trl.Length() - 1; i >= 0; i-- {
|
||||
if y+offsetY >= yLimit {
|
||||
if y >= height {
|
||||
return
|
||||
}
|
||||
|
||||
@ -288,18 +286,18 @@ func (trl *TagRoomList) Draw(screen tcell.Screen) {
|
||||
|
||||
lineWidth := width
|
||||
isSelected := trl.name == trl.parent.selectedTag && item.Room == trl.parent.selected
|
||||
item.Draw(trl.parent, screen, x, y+offsetY, lineWidth, isSelected)
|
||||
offsetY++
|
||||
item.Draw(trl.parent, screen, 0, y, lineWidth, isSelected)
|
||||
y++
|
||||
}
|
||||
hasLess := trl.maxShown > 10
|
||||
hasMore := trl.HasInvisibleRooms()
|
||||
if (hasLess || hasMore) && y+offsetY < yLimit {
|
||||
if (hasLess || hasMore) && y < height {
|
||||
if hasMore {
|
||||
widget.WriteLine(screen, tview.AlignRight, "More ↓", x, y+offsetY, width, tcell.StyleDefault)
|
||||
widget.WriteLine(screen, mauview.AlignRight, "More ↓", 0, y, width, tcell.StyleDefault)
|
||||
}
|
||||
if hasLess {
|
||||
widget.WriteLine(screen, tview.AlignLeft, "↑ Less", x, y+offsetY, width, tcell.StyleDefault)
|
||||
widget.WriteLine(screen, mauview.AlignLeft, "↑ Less", 0, y, width, tcell.StyleDefault)
|
||||
}
|
||||
offsetY++
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
47
ui/ui.go
47
ui/ui.go
@ -19,8 +19,8 @@ package ui
|
||||
import (
|
||||
"os"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/interface"
|
||||
)
|
||||
@ -34,17 +34,18 @@ const (
|
||||
)
|
||||
|
||||
type GomuksUI struct {
|
||||
gmx ifc.Gomuks
|
||||
app *tview.Application
|
||||
views *tview.Pages
|
||||
gmx ifc.Gomuks
|
||||
app *mauview.Application
|
||||
|
||||
mainView *MainView
|
||||
loginView *LoginView
|
||||
|
||||
views map[View]mauview.Component
|
||||
}
|
||||
|
||||
func init() {
|
||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
tview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
|
||||
mauview.Styles.PrimitiveBackgroundColor = tcell.ColorDefault
|
||||
mauview.Styles.ContrastBackgroundColor = tcell.ColorDarkGreen
|
||||
if tcellDB := os.Getenv("TCELLDB"); len(tcellDB) == 0 {
|
||||
if info, err := os.Stat("/usr/share/tcell/database"); err == nil && info.IsDir() {
|
||||
os.Setenv("TCELLDB", "/usr/share/tcell/database")
|
||||
@ -54,20 +55,22 @@ func init() {
|
||||
|
||||
func NewGomuksUI(gmx ifc.Gomuks) ifc.GomuksUI {
|
||||
ui := &GomuksUI{
|
||||
gmx: gmx,
|
||||
app: tview.NewApplication(),
|
||||
views: tview.NewPages(),
|
||||
gmx: gmx,
|
||||
app: mauview.NewApplication(),
|
||||
}
|
||||
ui.views.SetChangedFunc(ui.Render)
|
||||
return ui
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) Init() {
|
||||
ui.app.SetRoot(ui.InitViews(), true)
|
||||
ui.views = map[View]mauview.Component{
|
||||
ViewLogin: ui.NewLoginView(),
|
||||
ViewMain: ui.NewMainView(),
|
||||
}
|
||||
ui.app.Root = ui.views[ViewLogin]
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) Start() error {
|
||||
return ui.app.Run()
|
||||
return ui.app.Start()
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) Stop() {
|
||||
@ -75,23 +78,21 @@ func (ui *GomuksUI) Stop() {
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) Finish() {
|
||||
if ui.app.GetScreen() != nil {
|
||||
ui.app.GetScreen().Fini()
|
||||
if ui.app.Screen() != nil {
|
||||
ui.app.Screen().Fini()
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) Render() {
|
||||
ui.app.Draw()
|
||||
ui.app.Redraw()
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) OnLogin() {
|
||||
ui.SetView(ViewMain)
|
||||
ui.app.SetFocus(ui.mainView)
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) OnLogout() {
|
||||
ui.SetView(ViewLogin)
|
||||
ui.app.SetFocus(ui.loginView)
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) HandleNewPreferences() {
|
||||
@ -99,13 +100,11 @@ func (ui *GomuksUI) HandleNewPreferences() {
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) SetView(name View) {
|
||||
ui.views.SwitchToPage(string(name))
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) InitViews() tview.Primitive {
|
||||
ui.views.AddPage(string(ViewLogin), ui.NewLoginView(), true, true)
|
||||
ui.views.AddPage(string(ViewMain), ui.NewMainView(), true, false)
|
||||
return ui.views
|
||||
ui.app.Root = ui.views[name]
|
||||
focusable, ok := ui.app.Root.(mauview.Focusable)
|
||||
if ok {
|
||||
focusable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) MainView() ifc.MainView {
|
||||
|
@ -17,66 +17,97 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"maunium.net/go/tcell"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/tview"
|
||||
"maunium.net/go/mauview"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
"maunium.net/go/gomuks/interface"
|
||||
"maunium.net/go/gomuks/ui/widget"
|
||||
)
|
||||
|
||||
type LoginView struct {
|
||||
*tview.Form
|
||||
*mauview.Form
|
||||
|
||||
homeserver *widget.AdvancedInputField
|
||||
username *widget.AdvancedInputField
|
||||
password *widget.AdvancedInputField
|
||||
error *widget.FormTextView
|
||||
container *mauview.Centerer
|
||||
|
||||
homeserverLabel *mauview.TextField
|
||||
usernameLabel *mauview.TextField
|
||||
passwordLabel *mauview.TextField
|
||||
|
||||
homeserver *mauview.InputField
|
||||
username *mauview.InputField
|
||||
password *mauview.InputField
|
||||
error *mauview.TextField
|
||||
|
||||
loginButton *mauview.Button
|
||||
quitButton *mauview.Button
|
||||
|
||||
matrix ifc.MatrixContainer
|
||||
config *config.Config
|
||||
parent *GomuksUI
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) NewLoginView() tview.Primitive {
|
||||
func (ui *GomuksUI) NewLoginView() mauview.Component {
|
||||
view := &LoginView{
|
||||
Form: tview.NewForm(),
|
||||
Form: mauview.NewForm(),
|
||||
|
||||
homeserver: widget.NewAdvancedInputField(),
|
||||
username: widget.NewAdvancedInputField(),
|
||||
password: widget.NewAdvancedInputField(),
|
||||
usernameLabel: mauview.NewTextField().SetText("Username"),
|
||||
passwordLabel: mauview.NewTextField().SetText("Password"),
|
||||
homeserverLabel: mauview.NewTextField().SetText("Homeserver"),
|
||||
|
||||
username: mauview.NewInputField(),
|
||||
password: mauview.NewInputField(),
|
||||
homeserver: mauview.NewInputField(),
|
||||
|
||||
loginButton: mauview.NewButton("Login"),
|
||||
quitButton: mauview.NewButton("Quit"),
|
||||
|
||||
matrix: ui.gmx.Matrix(),
|
||||
config: ui.gmx.Config(),
|
||||
parent: ui,
|
||||
}
|
||||
|
||||
hs := ui.gmx.Config().HS
|
||||
if len(hs) == 0 {
|
||||
hs = "https://matrix.org"
|
||||
}
|
||||
view.homeserver.SetLabel("Homeserver").SetText(hs).SetFieldWidth(30)
|
||||
view.username.SetLabel("Username").SetText(ui.gmx.Config().UserID).SetFieldWidth(30)
|
||||
view.password.SetLabel("Password").SetMaskCharacter('*').SetFieldWidth(30)
|
||||
view.homeserver.SetText(hs)
|
||||
view.username.SetText(ui.gmx.Config().UserID)
|
||||
view.password.SetMaskCharacter('*')
|
||||
|
||||
view.
|
||||
AddFormItem(view.homeserver).AddFormItem(view.username).AddFormItem(view.password).
|
||||
AddButton("Log in", view.Login).
|
||||
AddButton("Quit", ui.gmx.Stop).
|
||||
SetButtonsAlign(tview.AlignCenter).
|
||||
SetBorder(true).SetTitle("Log in to Matrix")
|
||||
view.quitButton.SetOnClick(ui.gmx.Stop).SetBackgroundColor(tcell.ColorBlue)
|
||||
view.loginButton.SetOnClick(view.Login).SetBackgroundColor(tcell.ColorBlue)
|
||||
|
||||
view.SetColumns([]int{1, 10, 1, 9, 1, 9, 1, 10, 1})
|
||||
view.SetRows([]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
||||
view.AddFormItem(view.username, 3, 1, 5, 1).
|
||||
AddFormItem(view.password, 3, 3, 5, 1).
|
||||
AddFormItem(view.homeserver, 3, 5, 5, 1).
|
||||
AddFormItem(view.loginButton, 5, 7, 3, 1).
|
||||
AddFormItem(view.quitButton, 1, 7, 3, 1).
|
||||
AddComponent(view.usernameLabel, 1, 1, 1, 1).
|
||||
AddComponent(view.passwordLabel, 1, 3, 1, 1).
|
||||
AddComponent(view.homeserverLabel, 1, 5, 1, 1)
|
||||
ui.loginView = view
|
||||
|
||||
return widget.Center(45, 13, ui.loginView)
|
||||
view.container = mauview.Center(mauview.NewBox(view).SetTitle("Log in to Matrix"), 45, 13)
|
||||
return view.container
|
||||
}
|
||||
|
||||
func (view *LoginView) Error(err string) {
|
||||
if len(err) == 0 {
|
||||
debug.Print("Hiding error")
|
||||
view.RemoveComponent(view.error)
|
||||
view.error = nil
|
||||
return
|
||||
}
|
||||
debug.Print("Showing error", err)
|
||||
if view.error == nil {
|
||||
view.error = &widget.FormTextView{TextView: tview.NewTextView()}
|
||||
view.AddFormItem(view.error)
|
||||
view.error = mauview.NewTextField().SetTextColor(tcell.ColorRed)
|
||||
view.AddComponent(view.error, 1, 9, 7, 1)
|
||||
}
|
||||
view.error.SetText(err)
|
||||
|
||||
view.parent.Render()
|
||||
}
|
||||
|
||||
func (view *LoginView) Login() {
|
||||
@ -91,8 +122,8 @@ func (view *LoginView) Login() {
|
||||
err = view.matrix.Login(mxid, password)
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(mautrix.HTTPError); ok {
|
||||
if respErr, ok := httpErr.WrappedError.(mautrix.RespError); ok {
|
||||
view.Error(respErr.Err)
|
||||
if httpErr.RespError != nil {
|
||||
view.Error(httpErr.RespError.Err)
|
||||
} else {
|
||||
view.Error(httpErr.Message)
|
||||
}
|
||||
|
239
ui/view-main.go
239
ui/view-main.go
@ -26,8 +26,8 @@ import (
|
||||
"github.com/kyokomi/emoji"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
|
||||
"maunium.net/go/gomuks/config"
|
||||
"maunium.net/go/gomuks/debug"
|
||||
@ -40,12 +40,16 @@ import (
|
||||
)
|
||||
|
||||
type MainView struct {
|
||||
*tview.Flex
|
||||
flex *mauview.Flex
|
||||
|
||||
roomList *RoomList
|
||||
roomView *tview.Pages
|
||||
roomView *mauview.Box
|
||||
currentRoom *RoomView
|
||||
rooms map[string]*RoomView
|
||||
cmdProcessor *CommandProcessor
|
||||
focused mauview.Focusable
|
||||
|
||||
modal mauview.Component
|
||||
|
||||
lastFocusTime time.Time
|
||||
|
||||
@ -55,11 +59,10 @@ type MainView struct {
|
||||
parent *GomuksUI
|
||||
}
|
||||
|
||||
func (ui *GomuksUI) NewMainView() tview.Primitive {
|
||||
func (ui *GomuksUI) NewMainView() mauview.Component {
|
||||
mainView := &MainView{
|
||||
Flex: tview.NewFlex(),
|
||||
roomList: NewRoomList(),
|
||||
roomView: tview.NewPages(),
|
||||
flex: mauview.NewFlex().SetDirection(mauview.FlexColumn),
|
||||
roomView: mauview.NewBox(nil).SetBorder(false),
|
||||
rooms: make(map[string]*RoomView),
|
||||
|
||||
matrix: ui.gmx.Matrix(),
|
||||
@ -67,13 +70,13 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
|
||||
config: ui.gmx.Config(),
|
||||
parent: ui,
|
||||
}
|
||||
mainView.roomList = NewRoomList(mainView)
|
||||
mainView.cmdProcessor = NewCommandProcessor(mainView)
|
||||
|
||||
mainView.
|
||||
SetDirection(tview.FlexColumn).
|
||||
AddItem(mainView.roomList, 25, 0, false).
|
||||
AddItem(widget.NewBorder(), 1, 0, false).
|
||||
AddItem(mainView.roomView, 0, 1, true)
|
||||
mainView.flex.
|
||||
AddFixedComponent(mainView.roomList, 25).
|
||||
AddFixedComponent(widget.NewBorder(), 1).
|
||||
AddProportionalComponent(mainView.roomView, 1)
|
||||
mainView.BumpFocus(nil)
|
||||
|
||||
ui.mainView = mainView
|
||||
@ -81,18 +84,37 @@ func (ui *GomuksUI) NewMainView() tview.Primitive {
|
||||
return mainView
|
||||
}
|
||||
|
||||
func (view *MainView) Draw(screen tcell.Screen) {
|
||||
func (view *MainView) ShowModal(modal mauview.Component) {
|
||||
view.modal = modal
|
||||
var ok bool
|
||||
view.focused, ok = modal.(mauview.Focusable)
|
||||
if !ok {
|
||||
view.focused = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) HideModal() {
|
||||
view.modal = nil
|
||||
view.focused = view.roomView
|
||||
}
|
||||
|
||||
func (view *MainView) Draw(screen mauview.Screen) {
|
||||
if view.config.Preferences.HideRoomList {
|
||||
view.roomView.SetRect(view.GetRect())
|
||||
view.roomView.Draw(screen)
|
||||
} else {
|
||||
view.Flex.Draw(screen)
|
||||
view.flex.Draw(screen)
|
||||
}
|
||||
|
||||
if view.modal != nil {
|
||||
view.modal.Draw(screen)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) BumpFocus(roomView *RoomView) {
|
||||
view.lastFocusTime = time.Now()
|
||||
view.MarkRead(roomView)
|
||||
if roomView != nil {
|
||||
view.lastFocusTime = time.Now()
|
||||
view.MarkRead(roomView)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) MarkRead(roomView *RoomView) {
|
||||
@ -167,7 +189,10 @@ func (view *MainView) sendTempMessage(roomView *RoomView, tempMessage ifc.Messag
|
||||
}
|
||||
|
||||
func (view *MainView) ShowBare(roomView *RoomView) {
|
||||
_, height := view.parent.app.GetScreen().Size()
|
||||
if roomView == nil {
|
||||
return
|
||||
}
|
||||
_, height := view.parent.app.Screen().Size()
|
||||
view.parent.app.Suspend(func() {
|
||||
print("\033[2J\033[0;0H")
|
||||
// We don't know how much space there exactly is. Too few messages looks weird,
|
||||
@ -181,37 +206,54 @@ func (view *MainView) ShowBare(roomView *RoomView) {
|
||||
})
|
||||
}
|
||||
|
||||
func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *tcell.EventKey {
|
||||
view.BumpFocus(roomView)
|
||||
func (view *MainView) OnKeyEvent(event mauview.KeyEvent) bool {
|
||||
view.BumpFocus(view.currentRoom)
|
||||
|
||||
k := key.Key()
|
||||
c := key.Rune()
|
||||
if key.Modifiers() == tcell.ModCtrl || key.Modifiers() == tcell.ModAlt {
|
||||
if view.modal != nil {
|
||||
return view.modal.OnKeyEvent(event)
|
||||
}
|
||||
|
||||
k := event.Key()
|
||||
c := event.Rune()
|
||||
if event.Modifiers() == tcell.ModCtrl || event.Modifiers() == tcell.ModAlt {
|
||||
switch {
|
||||
case k == tcell.KeyDown:
|
||||
view.SwitchRoom(view.roomList.Next())
|
||||
case k == tcell.KeyUp:
|
||||
view.SwitchRoom(view.roomList.Previous())
|
||||
case k == tcell.KeyEnter:
|
||||
searchModal := NewFuzzySearchModal(view, 42, 12)
|
||||
view.parent.views.AddPage("fuzzy-search-modal", searchModal, true, true)
|
||||
view.parent.app.SetFocus(searchModal)
|
||||
view.ShowModal(NewFuzzySearchModal(view, 42, 12))
|
||||
case k == tcell.KeyHome:
|
||||
msgView := view.currentRoom.MessageView()
|
||||
msgView.AddScrollOffset(msgView.TotalHeight())
|
||||
case k == tcell.KeyEnd:
|
||||
msgView := view.currentRoom.MessageView()
|
||||
msgView.AddScrollOffset(-msgView.TotalHeight())
|
||||
case k == tcell.KeyCtrlN:
|
||||
return view.flex.OnKeyEvent(tcell.NewEventKey(tcell.KeyEnter, '\n', event.Modifiers()))
|
||||
case c == 'a':
|
||||
view.SwitchRoom(view.roomList.NextWithActivity())
|
||||
case c == 'l':
|
||||
view.ShowBare(roomView)
|
||||
view.ShowBare(view.currentRoom)
|
||||
default:
|
||||
return key
|
||||
goto defaultHandler
|
||||
}
|
||||
return true
|
||||
} else if k == tcell.KeyAltDown || k == tcell.KeyCtrlDown {
|
||||
view.SwitchRoom(view.roomList.Next())
|
||||
return true
|
||||
} else if k == tcell.KeyAltUp || k == tcell.KeyCtrlUp {
|
||||
view.SwitchRoom(view.roomList.Previous())
|
||||
} else if k == tcell.KeyPgUp || k == tcell.KeyPgDn || k == tcell.KeyUp || k == tcell.KeyDown || k == tcell.KeyEnd || k == tcell.KeyHome {
|
||||
msgView := roomView.MessageView()
|
||||
return true
|
||||
} else if view.currentRoom != nil &&
|
||||
(k == tcell.KeyPgUp || k == tcell.KeyPgDn ||
|
||||
k == tcell.KeyUp || k == tcell.KeyDown ||
|
||||
k == tcell.KeyEnd || k == tcell.KeyHome) {
|
||||
// TODO these should be in the RoomView key handler
|
||||
msgView := view.currentRoom.MessageView()
|
||||
|
||||
if msgView.IsAtTop() && (k == tcell.KeyPgUp || k == tcell.KeyUp) {
|
||||
go view.LoadHistory(roomView.Room.ID)
|
||||
go view.LoadHistory(view.currentRoom.Room.ID)
|
||||
}
|
||||
|
||||
switch k {
|
||||
@ -219,80 +261,72 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) *
|
||||
msgView.AddScrollOffset(msgView.Height() / 2)
|
||||
case tcell.KeyPgDn:
|
||||
msgView.AddScrollOffset(-msgView.Height() / 2)
|
||||
case tcell.KeyUp:
|
||||
msgView.AddScrollOffset(1)
|
||||
case tcell.KeyDown:
|
||||
msgView.AddScrollOffset(-1)
|
||||
case tcell.KeyHome:
|
||||
msgView.AddScrollOffset(msgView.TotalHeight())
|
||||
case tcell.KeyEnd:
|
||||
msgView.AddScrollOffset(-msgView.TotalHeight())
|
||||
default:
|
||||
goto defaultHandler
|
||||
}
|
||||
} else {
|
||||
return key
|
||||
return true
|
||||
} else if k == tcell.KeyEnter {
|
||||
view.InputSubmit(view.currentRoom, view.currentRoom.input.GetText())
|
||||
return true
|
||||
}
|
||||
return nil
|
||||
defaultHandler:
|
||||
if view.config.Preferences.HideRoomList {
|
||||
debug.Print("Key event going to default handler (direct to roomview)", event)
|
||||
return view.roomView.OnKeyEvent(event)
|
||||
}
|
||||
debug.Print("Key event going to default handler (flex)", event)
|
||||
return view.flex.OnKeyEvent(event)
|
||||
}
|
||||
|
||||
const WheelScrollOffsetDiff = 3
|
||||
|
||||
func isInArea(x, y int, p tview.Primitive) bool {
|
||||
rx, ry, rw, rh := p.GetRect()
|
||||
return x >= rx && y >= ry && x < rx+rw && y < ry+rh
|
||||
}
|
||||
|
||||
func (view *MainView) MouseEventHandler(roomView *RoomView, event *tcell.EventMouse) *tcell.EventMouse {
|
||||
if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
|
||||
return event
|
||||
func (view *MainView) OnMouseEvent(event mauview.MouseEvent) bool {
|
||||
if view.config.Preferences.HideRoomList {
|
||||
return view.roomView.OnMouseEvent(event)
|
||||
}
|
||||
return view.flex.OnMouseEvent(event)
|
||||
/*if event.Buttons() == tcell.ButtonNone || event.HasMotion() {
|
||||
return false
|
||||
}
|
||||
view.BumpFocus(roomView)
|
||||
|
||||
msgView := roomView.MessageView()
|
||||
view.BumpFocus(view.currentRoom)
|
||||
|
||||
x, y := event.Position()
|
||||
|
||||
switch {
|
||||
case isInArea(x, y, msgView):
|
||||
mx, my, _, _ := msgView.GetRect()
|
||||
switch event.Buttons() {
|
||||
case tcell.WheelUp:
|
||||
if msgView.IsAtTop() {
|
||||
go view.LoadHistory(roomView.Room.ID)
|
||||
} else {
|
||||
msgView.AddScrollOffset(WheelScrollOffsetDiff)
|
||||
|
||||
view.parent.Render()
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
msgView.AddScrollOffset(-WheelScrollOffsetDiff)
|
||||
view.parent.Render()
|
||||
view.MarkRead(roomView)
|
||||
default:
|
||||
if msgView.HandleClick(x-mx, y-my, event.Buttons()) {
|
||||
view.parent.Render()
|
||||
}
|
||||
}
|
||||
case isInArea(x, y, view.roomList):
|
||||
switch event.Buttons() {
|
||||
case tcell.WheelUp:
|
||||
view.roomList.AddScrollOffset(-WheelScrollOffsetDiff)
|
||||
view.parent.Render()
|
||||
case tcell.WheelDown:
|
||||
view.roomList.AddScrollOffset(WheelScrollOffsetDiff)
|
||||
view.parent.Render()
|
||||
case tcell.Button1:
|
||||
_, rly, _, _ := msgView.GetRect()
|
||||
line := y - rly + 1
|
||||
switchToTag, switchToRoom := view.roomList.HandleClick(x, line, event.Modifiers() == tcell.ModCtrl)
|
||||
if switchToRoom != nil {
|
||||
view.SwitchRoom(switchToTag, switchToRoom)
|
||||
} else {
|
||||
view.parent.Render()
|
||||
}
|
||||
}
|
||||
case x >= 27:
|
||||
view.roomView.OnMouseEvent(mauview.OffsetMouseEvent(event, -27, 0))
|
||||
view.roomView.Focus()
|
||||
view.focused = view.roomView
|
||||
case x <= 25:
|
||||
view.roomList.OnMouseEvent(event)
|
||||
view.roomList.Focus()
|
||||
view.focused = view.roomList
|
||||
default:
|
||||
debug.Print("Unhandled mouse event:", event.Buttons(), event.Modifiers(), x, y)
|
||||
}
|
||||
return event
|
||||
return false*/
|
||||
}
|
||||
|
||||
func (view *MainView) OnPasteEvent(event mauview.PasteEvent) bool {
|
||||
if view.modal != nil {
|
||||
return view.modal.OnPasteEvent(event)
|
||||
} else if view.config.Preferences.HideRoomList {
|
||||
return view.roomView.OnPasteEvent(event)
|
||||
}
|
||||
return view.flex.OnPasteEvent(event)
|
||||
}
|
||||
|
||||
func (view *MainView) Focus() {
|
||||
if view.focused != nil {
|
||||
view.focused.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) Blur() {
|
||||
if view.focused != nil {
|
||||
view.focused.Blur()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
|
||||
@ -300,29 +334,19 @@ func (view *MainView) SwitchRoom(tag string, room *rooms.Room) {
|
||||
return
|
||||
}
|
||||
|
||||
view.roomView.SwitchToPage(room.ID)
|
||||
roomView := view.rooms[room.ID]
|
||||
if roomView == nil {
|
||||
debug.Print("Tried to switch to non-nil room with nil roomView!")
|
||||
debug.Print(tag, room)
|
||||
return
|
||||
}
|
||||
view.roomView.SetInnerComponent(roomView)
|
||||
view.currentRoom = roomView
|
||||
view.MarkRead(roomView)
|
||||
view.roomList.SetSelected(tag, room)
|
||||
view.parent.app.SetFocus(view)
|
||||
view.parent.Render()
|
||||
}
|
||||
|
||||
func (view *MainView) Focus(delegate func(p tview.Primitive)) {
|
||||
room := view.roomList.SelectedRoom()
|
||||
if room != nil {
|
||||
roomView, ok := view.rooms[room.ID]
|
||||
if ok {
|
||||
delegate(roomView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *MainView) SaveAllHistory() {
|
||||
for _, room := range view.rooms {
|
||||
err := room.SaveHistory(view.config.HistoryDir)
|
||||
@ -333,14 +357,11 @@ func (view *MainView) SaveAllHistory() {
|
||||
}
|
||||
|
||||
func (view *MainView) addRoomPage(room *rooms.Room) {
|
||||
if !view.roomView.HasPage(room.ID) {
|
||||
if _, ok := view.rooms[room.ID]; !ok {
|
||||
roomView := NewRoomView(view, room).
|
||||
SetInputSubmitFunc(view.InputSubmit).
|
||||
SetInputChangedFunc(view.InputChanged).
|
||||
SetInputCapture(view.KeyEventHandler).
|
||||
SetMouseCapture(view.MouseEventHandler)
|
||||
SetInputChangedFunc(view.InputChanged)
|
||||
view.rooms[room.ID] = roomView
|
||||
view.roomView.AddPage(room.ID, roomView, true, false)
|
||||
roomView.UpdateUserList()
|
||||
|
||||
_, err := roomView.LoadHistory(view.matrix, view.config.HistoryDir)
|
||||
@ -387,7 +408,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
|
||||
view.roomList.Remove(room)
|
||||
view.SwitchRoom(view.roomList.Selected())
|
||||
|
||||
view.roomView.RemovePage(room.ID)
|
||||
delete(view.rooms, room.ID)
|
||||
|
||||
view.parent.Render()
|
||||
@ -395,7 +415,6 @@ func (view *MainView) RemoveRoom(room *rooms.Room) {
|
||||
|
||||
func (view *MainView) SetRooms(rooms map[string]*rooms.Room) {
|
||||
view.roomList.Clear()
|
||||
view.roomView.Clear()
|
||||
view.rooms = make(map[string]*RoomView)
|
||||
for _, room := range rooms {
|
||||
if room.HasLeft {
|
||||
|
@ -1,562 +0,0 @@
|
||||
// gomuks - A terminal Matrix client written in Go.
|
||||
// Copyright (C) 2019 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Based on https://github.com/rivo/tview/blob/master/inputfield.go
|
||||
|
||||
package widget
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/clipboard"
|
||||
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
// AdvancedInputField is a multi-line user-editable text area.
|
||||
//
|
||||
// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
|
||||
// input).
|
||||
type AdvancedInputField struct {
|
||||
*tview.Box
|
||||
|
||||
// Cursor position
|
||||
cursorOffset int
|
||||
viewOffset int
|
||||
|
||||
// The text that was entered.
|
||||
text string
|
||||
|
||||
// The text to be displayed before the input area.
|
||||
label string
|
||||
|
||||
// The text to be displayed in the input area when "text" is empty.
|
||||
placeholder string
|
||||
|
||||
// The label color.
|
||||
labelColor tcell.Color
|
||||
|
||||
// The background color of the input area.
|
||||
fieldBackgroundColor tcell.Color
|
||||
|
||||
// The text color of the input area.
|
||||
fieldTextColor tcell.Color
|
||||
|
||||
// The text color of the placeholder.
|
||||
placeholderTextColor tcell.Color
|
||||
|
||||
// The screen width of the label area. A value of 0 means use the width of
|
||||
// the label text.
|
||||
labelWidth int
|
||||
|
||||
// The screen width of the input area. A value of 0 means extend as much as
|
||||
// possible.
|
||||
fieldWidth int
|
||||
|
||||
// A character to mask entered text (useful for password fields). A value of 0
|
||||
// disables masking.
|
||||
maskCharacter rune
|
||||
|
||||
// Whether or not to enable vim-style keybindings.
|
||||
vimBindings bool
|
||||
|
||||
// An optional function which may reject the last character that was entered.
|
||||
accept func(text string, ch rune) bool
|
||||
|
||||
// An optional function which is called when the input has changed.
|
||||
changed func(text string)
|
||||
|
||||
// An optional function which is called when the user indicated that they
|
||||
// are done entering text. The key which was pressed is provided (enter, tab, backtab or escape).
|
||||
done func(tcell.Key)
|
||||
|
||||
// An optional function which is called when the user presses tab.
|
||||
tabComplete func(text string, cursorOffset int)
|
||||
}
|
||||
|
||||
// NewAdvancedInputField returns a new input field.
|
||||
func NewAdvancedInputField() *AdvancedInputField {
|
||||
return &AdvancedInputField{
|
||||
Box: tview.NewBox(),
|
||||
labelColor: tview.Styles.SecondaryTextColor,
|
||||
fieldBackgroundColor: tview.Styles.ContrastBackgroundColor,
|
||||
fieldTextColor: tview.Styles.PrimaryTextColor,
|
||||
placeholderTextColor: tview.Styles.ContrastSecondaryTextColor,
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets the current text of the input field.
|
||||
func (field *AdvancedInputField) SetText(text string) *AdvancedInputField {
|
||||
field.text = text
|
||||
if field.changed != nil {
|
||||
field.changed(text)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference.
|
||||
func (field *AdvancedInputField) SetTextAndMoveCursor(text string) *AdvancedInputField {
|
||||
oldWidth := runewidth.StringWidth(field.text)
|
||||
field.text = text
|
||||
newWidth := runewidth.StringWidth(field.text)
|
||||
if oldWidth != newWidth {
|
||||
field.cursorOffset += newWidth - oldWidth
|
||||
}
|
||||
if field.changed != nil {
|
||||
field.changed(field.text)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// GetText returns the current text of the input field.
|
||||
func (field *AdvancedInputField) GetText() string {
|
||||
return field.text
|
||||
}
|
||||
|
||||
// SetLabel sets the text to be displayed before the input area.
|
||||
func (field *AdvancedInputField) SetLabel(label string) *AdvancedInputField {
|
||||
field.label = label
|
||||
return field
|
||||
}
|
||||
|
||||
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
|
||||
// primitive to use the width of the label string.
|
||||
func (field *AdvancedInputField) SetLabelWidth(width int) *AdvancedInputField {
|
||||
field.labelWidth = width
|
||||
return field
|
||||
}
|
||||
|
||||
// GetLabel returns the text to be displayed before the input area.
|
||||
func (field *AdvancedInputField) GetLabel() string {
|
||||
return field.label
|
||||
}
|
||||
|
||||
// SetPlaceholder sets the text to be displayed when the input text is empty.
|
||||
func (field *AdvancedInputField) SetPlaceholder(text string) *AdvancedInputField {
|
||||
field.placeholder = text
|
||||
return field
|
||||
}
|
||||
|
||||
// SetLabelColor sets the color of the label.
|
||||
func (field *AdvancedInputField) SetLabelColor(color tcell.Color) *AdvancedInputField {
|
||||
field.labelColor = color
|
||||
return field
|
||||
}
|
||||
|
||||
// SetFieldBackgroundColor sets the background color of the input area.
|
||||
func (field *AdvancedInputField) SetFieldBackgroundColor(color tcell.Color) *AdvancedInputField {
|
||||
field.fieldBackgroundColor = color
|
||||
return field
|
||||
}
|
||||
|
||||
// SetFieldTextColor sets the text color of the input area.
|
||||
func (field *AdvancedInputField) SetFieldTextColor(color tcell.Color) *AdvancedInputField {
|
||||
field.fieldTextColor = color
|
||||
return field
|
||||
}
|
||||
|
||||
// SetPlaceholderExtColor sets the text color of placeholder text.
|
||||
func (field *AdvancedInputField) SetPlaceholderExtColor(color tcell.Color) *AdvancedInputField {
|
||||
field.placeholderTextColor = color
|
||||
return field
|
||||
}
|
||||
|
||||
// SetFormAttributes sets attributes shared by all form items.
|
||||
func (field *AdvancedInputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
|
||||
field.labelWidth = labelWidth
|
||||
field.labelColor = labelColor
|
||||
field.SetBackgroundColor(bgColor)
|
||||
field.fieldTextColor = fieldTextColor
|
||||
field.fieldBackgroundColor = fieldBgColor
|
||||
return field
|
||||
}
|
||||
|
||||
// SetFieldWidth sets the screen width of the input area. A value of 0 means
|
||||
// extend as much as possible.
|
||||
func (field *AdvancedInputField) SetFieldWidth(width int) *AdvancedInputField {
|
||||
field.fieldWidth = width
|
||||
return field
|
||||
}
|
||||
|
||||
// GetFieldWidth returns this primitive's field width.
|
||||
func (field *AdvancedInputField) GetFieldWidth() int {
|
||||
return field.fieldWidth
|
||||
}
|
||||
|
||||
// SetMaskCharacter sets a character that masks user input on a screen. A value
|
||||
// of 0 disables masking.
|
||||
func (field *AdvancedInputField) SetMaskCharacter(mask rune) *AdvancedInputField {
|
||||
field.maskCharacter = mask
|
||||
return field
|
||||
}
|
||||
|
||||
// SetAcceptanceFunc sets a handler which may reject the last character that was
|
||||
// entered (by returning false).
|
||||
//
|
||||
// This package defines a number of variables Prefixed with AdvancedInputField which may
|
||||
// be used for common input (e.g. numbers, maximum text length).
|
||||
func (field *AdvancedInputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *AdvancedInputField {
|
||||
field.accept = handler
|
||||
return field
|
||||
}
|
||||
|
||||
// SetChangedFunc sets a handler which is called whenever the text of the input
|
||||
// field has changed. It receives the current text (after the change).
|
||||
func (field *AdvancedInputField) SetChangedFunc(handler func(text string)) *AdvancedInputField {
|
||||
field.changed = handler
|
||||
return field
|
||||
}
|
||||
|
||||
// SetDoneFunc sets a handler which is called when the user is done entering
|
||||
// text. The callback function is provided with the key that was pressed, which
|
||||
// is one of the following:
|
||||
//
|
||||
// - KeyEnter: Done entering text.
|
||||
// - KeyEscape: Abort text input.
|
||||
// - KeyTab: Tab
|
||||
// - KeyBacktab: Shift + Tab
|
||||
func (field *AdvancedInputField) SetDoneFunc(handler func(key tcell.Key)) *AdvancedInputField {
|
||||
field.done = handler
|
||||
return field
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *AdvancedInputField {
|
||||
field.tabComplete = handler
|
||||
return field
|
||||
}
|
||||
|
||||
// SetFinishedFunc calls SetDoneFunc().
|
||||
func (field *AdvancedInputField) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
|
||||
return field.SetDoneFunc(handler)
|
||||
}
|
||||
|
||||
// drawInput calculates the field width and draws the background.
|
||||
func (field *AdvancedInputField) drawInput(screen tcell.Screen, rightLimit, x, y int) (fieldWidth int) {
|
||||
fieldWidth = field.fieldWidth
|
||||
if fieldWidth == 0 {
|
||||
fieldWidth = math.MaxInt32
|
||||
}
|
||||
if rightLimit-x < fieldWidth {
|
||||
fieldWidth = rightLimit - x
|
||||
}
|
||||
fieldStyle := tcell.StyleDefault.Background(field.fieldBackgroundColor)
|
||||
for index := 0; index < fieldWidth; index++ {
|
||||
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// prepareText prepares the text to be displayed and recalculates the view and cursor offsets.
|
||||
func (field *AdvancedInputField) prepareText(screen tcell.Screen, fieldWidth, x, y int) (text string) {
|
||||
text = field.text
|
||||
if text == "" && field.placeholder != "" {
|
||||
tview.Print(screen, field.placeholder, x, y, fieldWidth, tview.AlignLeft, field.placeholderTextColor)
|
||||
}
|
||||
|
||||
if field.maskCharacter > 0 {
|
||||
text = strings.Repeat(string(field.maskCharacter), utf8.RuneCountInString(field.text))
|
||||
}
|
||||
textWidth := runewidth.StringWidth(text)
|
||||
if field.cursorOffset >= textWidth {
|
||||
fieldWidth--
|
||||
}
|
||||
|
||||
if field.cursorOffset < field.viewOffset {
|
||||
field.viewOffset = field.cursorOffset
|
||||
} else if field.cursorOffset > field.viewOffset+fieldWidth {
|
||||
field.viewOffset = field.cursorOffset - fieldWidth
|
||||
} else if textWidth-field.viewOffset < fieldWidth {
|
||||
field.viewOffset = textWidth - fieldWidth
|
||||
}
|
||||
|
||||
if field.viewOffset < 0 {
|
||||
field.viewOffset = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// drawText draws the text and the cursor.
|
||||
func (field *AdvancedInputField) drawText(screen tcell.Screen, fieldWidth, x, y int, text string) {
|
||||
runes := []rune(text)
|
||||
relPos := 0
|
||||
for pos := field.viewOffset; pos <= fieldWidth+field.viewOffset && pos < len(runes); pos++ {
|
||||
ch := runes[pos]
|
||||
w := runewidth.RuneWidth(ch)
|
||||
_, _, style, _ := screen.GetContent(x+relPos, y)
|
||||
style = style.Foreground(field.fieldTextColor)
|
||||
for w > 0 {
|
||||
screen.SetContent(x+relPos, y, ch, nil, style)
|
||||
relPos++
|
||||
w--
|
||||
}
|
||||
}
|
||||
|
||||
// Set cursor.
|
||||
if field.GetFocusable().HasFocus() {
|
||||
field.setCursor(screen)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (field *AdvancedInputField) Draw(screen tcell.Screen) {
|
||||
field.Box.Draw(screen)
|
||||
|
||||
x, y, width, height := field.GetInnerRect()
|
||||
rightLimit := x + width
|
||||
if height < 1 || rightLimit <= x {
|
||||
return
|
||||
}
|
||||
|
||||
// Draw label.
|
||||
if field.labelWidth > 0 {
|
||||
labelWidth := field.labelWidth
|
||||
if labelWidth > rightLimit-x {
|
||||
labelWidth = rightLimit - x
|
||||
}
|
||||
tview.Print(screen, field.label, x, y, labelWidth, tview.AlignLeft, field.labelColor)
|
||||
x += labelWidth
|
||||
} else {
|
||||
_, drawnWidth := tview.Print(screen, field.label, x, y, rightLimit-x, tview.AlignLeft, field.labelColor)
|
||||
x += drawnWidth
|
||||
}
|
||||
|
||||
fieldWidth := field.drawInput(screen, rightLimit, x, y)
|
||||
text := field.prepareText(screen, fieldWidth, x, y)
|
||||
field.drawText(screen, fieldWidth, x, y, text)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) GetCursorOffset() int {
|
||||
return field.cursorOffset
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) SetCursorOffset(offset int) *AdvancedInputField {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
} else {
|
||||
width := runewidth.StringWidth(field.text)
|
||||
if offset >= width {
|
||||
offset = width
|
||||
}
|
||||
}
|
||||
field.cursorOffset = offset
|
||||
return field
|
||||
}
|
||||
|
||||
// setCursor sets the cursor position.
|
||||
func (field *AdvancedInputField) setCursor(screen tcell.Screen) {
|
||||
x, y, width, _ := field.GetRect()
|
||||
origX, origY := x, y
|
||||
rightLimit := x + width
|
||||
if field.HasBorder() {
|
||||
x++
|
||||
y++
|
||||
rightLimit -= 2
|
||||
}
|
||||
labelWidth := field.labelWidth
|
||||
if labelWidth == 0 {
|
||||
labelWidth = tview.StringWidth(field.label)
|
||||
}
|
||||
x = x + labelWidth + field.cursorOffset - field.viewOffset
|
||||
if x >= rightLimit {
|
||||
x = rightLimit - 1
|
||||
} else if x < origX {
|
||||
x = origY
|
||||
}
|
||||
screen.ShowCursor(x, y)
|
||||
}
|
||||
|
||||
var (
|
||||
lastWord = regexp.MustCompile(`\S+\s*$`)
|
||||
firstWord = regexp.MustCompile(`^\s*\S+`)
|
||||
)
|
||||
|
||||
func SubstringBefore(s string, w int) string {
|
||||
return runewidth.Truncate(s, w, "")
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) TypeRune(ch rune) {
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
newText := leftPart + string(ch) + field.text[len(leftPart):]
|
||||
if field.accept != nil {
|
||||
if !field.accept(newText, ch) {
|
||||
return
|
||||
}
|
||||
}
|
||||
field.text = newText
|
||||
field.cursorOffset += runewidth.RuneWidth(ch)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) PasteClipboard() {
|
||||
clip, _ := clipboard.ReadAll("clipboard")
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
field.text = leftPart + clip + field.text[len(leftPart):]
|
||||
field.cursorOffset += runewidth.StringWidth(clip)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) MoveCursorLeft(moveWord bool) {
|
||||
before := SubstringBefore(field.text, field.cursorOffset)
|
||||
if moveWord {
|
||||
found := lastWord.FindString(before)
|
||||
field.cursorOffset -= runewidth.StringWidth(found)
|
||||
} else if len(before) > 0 {
|
||||
beforeRunes := []rune(before)
|
||||
char := beforeRunes[len(beforeRunes)-1]
|
||||
field.cursorOffset -= runewidth.RuneWidth(char)
|
||||
}
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) MoveCursorRight(moveWord bool) {
|
||||
before := SubstringBefore(field.text, field.cursorOffset)
|
||||
after := field.text[len(before):]
|
||||
if moveWord {
|
||||
found := firstWord.FindString(after)
|
||||
field.cursorOffset += runewidth.StringWidth(found)
|
||||
} else if len(after) > 0 {
|
||||
char := []rune(after)[0]
|
||||
field.cursorOffset += runewidth.RuneWidth(char)
|
||||
}
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) RemoveNextCharacter() {
|
||||
if field.cursorOffset >= runewidth.StringWidth(field.text) {
|
||||
return
|
||||
}
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
// Take everything after the left part minus the first character.
|
||||
rightPart := string([]rune(field.text[len(leftPart):])[1:])
|
||||
|
||||
field.text = leftPart + rightPart
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) Clear() {
|
||||
field.text = ""
|
||||
field.cursorOffset = 0
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) RemovePreviousWord() {
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
rightPart := field.text[len(leftPart):]
|
||||
replacement := lastWord.ReplaceAllString(leftPart, "")
|
||||
field.text = replacement + rightPart
|
||||
|
||||
field.cursorOffset -= runewidth.StringWidth(leftPart) - runewidth.StringWidth(replacement)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) RemovePreviousCharacter() {
|
||||
if field.cursorOffset == 0 {
|
||||
return
|
||||
}
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
rightPart := field.text[len(leftPart):]
|
||||
|
||||
// Take everything before the right part minus the last character.
|
||||
leftPartRunes := []rune(leftPart)
|
||||
leftPartRunes = leftPartRunes[0 : len(leftPartRunes)-1]
|
||||
leftPart = string(leftPartRunes)
|
||||
|
||||
// Figure out what character was removed to correctly decrease cursorOffset.
|
||||
removedChar := field.text[len(leftPart) : len(field.text)-len(rightPart)]
|
||||
|
||||
field.text = leftPart + rightPart
|
||||
|
||||
field.cursorOffset -= runewidth.StringWidth(removedChar)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) TriggerTabComplete() bool {
|
||||
if field.tabComplete != nil {
|
||||
field.tabComplete(field.text, field.cursorOffset)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) handleInputChanges(originalText string) {
|
||||
// Trigger changed events.
|
||||
if field.text != originalText && field.changed != nil {
|
||||
field.changed(field.text)
|
||||
}
|
||||
|
||||
// Make sure cursor offset is valid
|
||||
if field.cursorOffset < 0 {
|
||||
field.cursorOffset = 0
|
||||
}
|
||||
width := runewidth.StringWidth(field.text)
|
||||
if field.cursorOffset > width {
|
||||
field.cursorOffset = width
|
||||
}
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (field *AdvancedInputField) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return field.WrapInputHandler(field.inputHandler)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) PasteHandler() func(event *tcell.EventPaste) {
|
||||
return field.WrapPasteHandler(field.pasteHandler)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) pasteHandler(event *tcell.EventPaste) {
|
||||
defer field.handleInputChanges(field.text)
|
||||
clip := event.Text()
|
||||
leftPart := SubstringBefore(field.text, field.cursorOffset)
|
||||
field.text = leftPart + clip + field.text[len(leftPart):]
|
||||
field.cursorOffset += runewidth.StringWidth(clip)
|
||||
}
|
||||
|
||||
func (field *AdvancedInputField) inputHandler(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
defer field.handleInputChanges(field.text)
|
||||
|
||||
// Process key event.
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyRune:
|
||||
field.TypeRune(event.Rune())
|
||||
case tcell.KeyCtrlV:
|
||||
field.PasteClipboard()
|
||||
case tcell.KeyLeft:
|
||||
field.MoveCursorLeft(event.Modifiers() == tcell.ModCtrl)
|
||||
case tcell.KeyRight:
|
||||
field.MoveCursorRight(event.Modifiers() == tcell.ModCtrl)
|
||||
case tcell.KeyDelete:
|
||||
field.RemoveNextCharacter()
|
||||
case tcell.KeyCtrlU:
|
||||
if field.vimBindings {
|
||||
field.Clear()
|
||||
}
|
||||
case tcell.KeyCtrlW:
|
||||
if field.vimBindings {
|
||||
field.RemovePreviousWord()
|
||||
}
|
||||
case tcell.KeyBackspace:
|
||||
field.RemovePreviousWord()
|
||||
case tcell.KeyBackspace2:
|
||||
field.RemovePreviousCharacter()
|
||||
case tcell.KeyTab:
|
||||
if field.TriggerTabComplete() {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyBacktab:
|
||||
if field.done != nil {
|
||||
field.done(key)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
// Border is a simple tview widget that renders a horizontal or vertical bar.
|
||||
@ -27,24 +27,37 @@ import (
|
||||
// If the height is 1, the bar will be horizontal.
|
||||
// If the width nor the height are 1, nothing will be rendered.
|
||||
type Border struct {
|
||||
*tview.Box
|
||||
Style tcell.Style
|
||||
}
|
||||
|
||||
// NewBorder wraps a new tview Box into a new Border.
|
||||
func NewBorder() *Border {
|
||||
return &Border{tview.NewBox()}
|
||||
return &Border{
|
||||
Style: tcell.StyleDefault.Foreground(mauview.Styles.BorderColor),
|
||||
}
|
||||
}
|
||||
|
||||
func (border *Border) Draw(screen tcell.Screen) {
|
||||
background := tcell.StyleDefault.Background(border.GetBackgroundColor()).Foreground(border.GetBorderColor())
|
||||
x, y, width, height := border.GetRect()
|
||||
func (border *Border) Draw(screen mauview.Screen) {
|
||||
width, height := screen.Size()
|
||||
if width == 1 {
|
||||
for borderY := y; borderY < y+height; borderY++ {
|
||||
screen.SetContent(x, borderY, tview.Borders.Vertical, nil, background)
|
||||
for borderY := 0; borderY < height; borderY++ {
|
||||
screen.SetContent(0, borderY, mauview.Borders.Vertical, nil, border.Style)
|
||||
}
|
||||
} else if height == 1 {
|
||||
for borderX := x; borderX < x+width; borderX++ {
|
||||
screen.SetContent(borderX, y, tview.Borders.Horizontal, nil, background)
|
||||
for borderX := 0; borderX < width; borderX++ {
|
||||
screen.SetContent(borderX, 0, mauview.Borders.Horizontal, nil, border.Style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (border *Border) OnKeyEvent(event mauview.KeyEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (border *Border) OnPasteEvent(event mauview.PasteEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (border *Border) OnMouseEvent(event mauview.MouseEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
// gomuks - A terminal Matrix client written in Go.
|
||||
// Copyright (C) 2019 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package widget
|
||||
|
||||
import (
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
// Center wraps the given tview primitive into a Flex element in order to
|
||||
// vertically and horizontally center the given primitive.
|
||||
func Center(width, height int, p tview.Primitive) tview.Primitive {
|
||||
return tview.NewFlex().
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(nil, 0, 1, false).
|
||||
AddItem(p, height, 1, true).
|
||||
AddItem(nil, 0, 1, false), width, 1, true).
|
||||
AddItem(nil, 0, 1, false)
|
||||
}
|
||||
|
||||
type transparentCenter struct {
|
||||
*tview.Box
|
||||
prefWidth, prefHeight int
|
||||
p tview.Primitive
|
||||
}
|
||||
|
||||
func TransparentCenter(width, height int, p tview.Primitive) tview.Primitive {
|
||||
return &transparentCenter{
|
||||
Box: tview.NewBox(),
|
||||
prefWidth: width,
|
||||
prefHeight: height,
|
||||
p: p,
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *transparentCenter) Draw(screen tcell.Screen) {
|
||||
x, y, width, height := tc.GetRect()
|
||||
if width > tc.prefWidth {
|
||||
x += (width - tc.prefWidth) / 2
|
||||
width = tc.prefWidth
|
||||
}
|
||||
if height > tc.prefHeight {
|
||||
y += (height - tc.prefHeight) / 2
|
||||
height = tc.prefHeight
|
||||
}
|
||||
tc.p.SetRect(x, y, width, height)
|
||||
tc.p.Draw(screen)
|
||||
}
|
||||
|
||||
func (tc *transparentCenter) Focus(delegate func(p tview.Primitive)) {
|
||||
if delegate != nil {
|
||||
delegate(tc.p)
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// gomuks - A terminal Matrix client written in Go.
|
||||
// Copyright (C) 2019 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package widget
|
||||
|
||||
import (
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
)
|
||||
|
||||
type FormTextView struct {
|
||||
*tview.TextView
|
||||
}
|
||||
|
||||
func (ftv *FormTextView) GetLabel() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ftv *FormTextView) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
|
||||
return ftv
|
||||
}
|
||||
|
||||
func (ftv *FormTextView) GetFieldWidth() int {
|
||||
_, _, w, _ := ftv.TextView.GetRect()
|
||||
return w
|
||||
}
|
||||
|
||||
func (ftv *FormTextView) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
|
||||
ftv.SetDoneFunc(handler)
|
||||
return ftv
|
||||
}
|
@ -18,28 +18,31 @@ package widget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"maunium.net/go/tcell"
|
||||
"maunium.net/go/tview"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
|
||||
"maunium.net/go/mauview"
|
||||
"maunium.net/go/tcell"
|
||||
)
|
||||
|
||||
func WriteLineSimple(screen tcell.Screen, line string, x, y int) {
|
||||
WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
|
||||
func WriteLineSimple(screen mauview.Screen, line string, x, y int) {
|
||||
WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault)
|
||||
}
|
||||
|
||||
func WriteLineSimpleColor(screen tcell.Screen, line string, x, y int, color tcell.Color) {
|
||||
WriteLine(screen, tview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
|
||||
func WriteLineSimpleColor(screen mauview.Screen, line string, x, y int, color tcell.Color) {
|
||||
WriteLine(screen, mauview.AlignLeft, line, x, y, 1<<30, tcell.StyleDefault.Foreground(color))
|
||||
}
|
||||
|
||||
func WriteLineColor(screen tcell.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
|
||||
func WriteLineColor(screen mauview.Screen, align int, line string, x, y, maxWidth int, color tcell.Color) {
|
||||
WriteLine(screen, align, line, x, y, maxWidth, tcell.StyleDefault.Foreground(color))
|
||||
}
|
||||
|
||||
func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
|
||||
func WriteLine(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
|
||||
offsetX := 0
|
||||
if align == tview.AlignRight {
|
||||
offsetX = maxWidth - runewidth.StringWidth(line)
|
||||
if align == mauview.AlignRight {
|
||||
// TODO is mauview.StringWidth correct here?
|
||||
offsetX = maxWidth - mauview.StringWidth(line)
|
||||
}
|
||||
if offsetX < 0 {
|
||||
offsetX = 0
|
||||
@ -60,12 +63,12 @@ func WriteLine(screen tcell.Screen, align int, line string, x, y, maxWidth int,
|
||||
}
|
||||
}
|
||||
|
||||
func WriteLinePadded(screen tcell.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
|
||||
func WriteLinePadded(screen mauview.Screen, align int, line string, x, y, maxWidth int, style tcell.Style) {
|
||||
padding := strconv.Itoa(maxWidth)
|
||||
if align == tview.AlignRight {
|
||||
if align == mauview.AlignRight {
|
||||
line = fmt.Sprintf("%"+padding+"s", line)
|
||||
} else {
|
||||
line = fmt.Sprintf("%-"+padding+"s", line)
|
||||
}
|
||||
WriteLine(screen, tview.AlignLeft, line, x, y, maxWidth, style)
|
||||
WriteLine(screen, mauview.AlignLeft, line, x, y, maxWidth, style)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user