fckeuspy-go/vendor/fyne.io/fyne/v2/widget/selectable.go

415 lines
11 KiB
Go

package widget
import (
"math"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/theme"
)
type selectable struct {
BaseWidget
cursorRow, cursorColumn int
// selectRow and selectColumn represent the selection start location
// The selection will span from selectRow/Column to CursorRow/Column -- note that the cursor
// position may occur before or after the select start position in the text.
selectRow, selectColumn int
focussed, selecting, selectEnded, password bool
sizeName fyne.ThemeSizeName
style fyne.TextStyle
provider *RichText
theme fyne.Theme
focus fyne.Focusable
// doubleTappedAtUnixMillis stores the time the entry was last DoubleTapped
// used for deciding whether the next MouseDown/TouchDown is a triple-tap or not
doubleTappedAtUnixMillis int64
}
func (s *selectable) CreateRenderer() fyne.WidgetRenderer {
return &selectableRenderer{sel: s}
}
func (s *selectable) Cursor() desktop.Cursor {
return desktop.TextCursor
}
func (s *selectable) DoubleTapped(p *fyne.PointEvent) {
s.doubleTappedAtUnixMillis = time.Now().UnixMilli()
s.updateMousePointer(p.Position)
row := s.provider.row(s.cursorRow)
start, end := getTextWhitespaceRegion(row, s.cursorColumn, false)
if start == -1 || end == -1 {
return
}
s.selectRow = s.cursorRow
s.selectColumn = start
s.cursorColumn = end
s.selecting = true
s.grabFocus()
s.Refresh()
}
func (s *selectable) DragEnd() {
if s.cursorColumn == s.selectColumn && s.cursorRow == s.selectRow {
s.selecting = false
}
shouldRefresh := !s.selecting
if shouldRefresh {
s.Refresh()
}
s.selectEnded = true
}
func (s *selectable) Dragged(d *fyne.DragEvent) {
s.dragged(d, true)
}
func (s *selectable) dragged(d *fyne.DragEvent, focus bool) {
if !s.selecting || s.selectEnded {
s.selectEnded = false
s.updateMousePointer(d.Position)
startPos := d.Position.Subtract(d.Dragged)
s.selectRow, s.selectColumn = s.getRowCol(startPos)
s.selecting = true
s.grabFocus()
}
s.updateMousePointer(d.Position)
s.Refresh()
}
func (s *selectable) MouseDown(m *desktop.MouseEvent) {
if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) {
s.selectCurrentRow(false)
return
}
s.grabFocus()
if s.selecting && m.Button == desktop.MouseButtonPrimary {
s.selecting = false
}
}
func (s *selectable) MouseUp(ev *desktop.MouseEvent) {
if ev.Button == desktop.MouseButtonSecondary {
return
}
start, _ := s.selection()
if (start == -1 || (s.selectRow == s.cursorRow && s.selectColumn == s.cursorColumn)) && s.selecting {
s.selecting = false
}
s.Refresh()
}
// SelectedText returns the text currently selected in this Entry.
// If there is no selection it will return the empty string.
func (s *selectable) SelectedText() string {
if s == nil || !s.selecting {
return ""
}
start, stop := s.selection()
if start == stop {
return ""
}
r := ([]rune)(s.provider.String())
return string(r[start:stop])
}
func (s *selectable) Tapped(*fyne.PointEvent) {
if !fyne.CurrentDevice().IsMobile() {
return
}
if s.doubleTappedAtUnixMillis != 0 {
s.doubleTappedAtUnixMillis = 0
return // was a triple (TappedDouble plus Tapped)
}
s.selecting = false
s.Refresh()
}
func (s *selectable) TappedSecondary(ev *fyne.PointEvent) {
c := fyne.CurrentApp().Driver().CanvasForObject(s.focus.(fyne.CanvasObject))
if c == nil {
return
}
m := fyne.NewMenu("",
fyne.NewMenuItem(lang.L("Copy"), func() {
fyne.CurrentApp().Clipboard().SetContent(s.SelectedText())
}))
ShowPopUpMenuAtPosition(m, c, ev.AbsolutePosition)
}
func (s *selectable) TouchCancel(m *mobile.TouchEvent) {
s.TouchUp(m)
}
func (s *selectable) TouchDown(m *mobile.TouchEvent) {
if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) {
s.selectCurrentRow(true)
return
}
}
func (s *selectable) TouchUp(*mobile.TouchEvent) {
}
func (s *selectable) TypedShortcut(sh fyne.Shortcut) {
switch sh.(type) {
case *fyne.ShortcutCopy:
fyne.CurrentApp().Clipboard().SetContent(s.SelectedText())
}
}
func (s *selectable) cursorColAt(text []rune, pos fyne.Position) int {
th := s.theme
textSize := th.Size(s.getSizeName())
innerPad := th.Size(theme.SizeNameInnerPadding)
for i := 0; i < len(text); i++ {
str := string(text[0:i])
wid := fyne.MeasureText(str, textSize, s.style).Width
charWid := fyne.MeasureText(string(text[i]), textSize, s.style).Width
if pos.X < innerPad+wid+(charWid/2) {
return i
}
}
return len(text)
}
func (s *selectable) getRowCol(p fyne.Position) (int, int) {
th := s.theme
textSize := th.Size(s.getSizeName())
innerPad := th.Size(theme.SizeNameInnerPadding)
rowHeight := s.provider.charMinSize(false, s.style, textSize).Height // TODO handle Password
row := int(math.Floor(float64(p.Y-innerPad+th.Size(theme.SizeNameLineSpacing)) / float64(rowHeight)))
col := 0
if row < 0 {
row = 0
} else if row >= s.provider.rows() {
row = s.provider.rows() - 1
col = s.provider.rowLength(row)
} else {
col = s.cursorColAt(s.provider.row(row), p)
}
return row, col
}
// Selects the row where the cursorColumn is currently positioned
func (s *selectable) selectCurrentRow(focus bool) {
s.grabFocus()
provider := s.provider
s.selectRow = s.cursorRow
s.selectColumn = 0
s.cursorColumn = provider.rowLength(s.cursorRow)
s.Refresh()
}
// selection returns the start and end text positions for the selected span of text
// Note: this functionality depends on the relationship between the selection start row/col and
// the current cursor row/column.
// eg: (whitespace for clarity, '_' denotes cursor)
//
// "T e s [t i]_n g" == 3, 5
// "T e s_[t i] n g" == 3, 5
// "T e_[s t i] n g" == 2, 5
func (s *selectable) selection() (int, int) {
noSelection := !s.selecting || (s.cursorRow == s.selectRow && s.cursorColumn == s.selectColumn)
if noSelection {
return -1, -1
}
// Find the selection start
rowA, colA := s.cursorRow, s.cursorColumn
rowB, colB := s.selectRow, s.selectColumn
// Reposition if the cursors row is more than select start row, or if the row is the same and
// the cursors col is more that the select start column
if rowA > s.selectRow || (rowA == s.selectRow && colA > s.selectColumn) {
rowA, colA = s.selectRow, s.selectColumn
rowB, colB = s.cursorRow, s.cursorColumn
}
return textPosFromRowCol(rowA, colA, s.provider), textPosFromRowCol(rowB, colB, s.provider)
}
// Obtains textual position from a given row and col
// expects a read or write lock to be held by the caller
func textPosFromRowCol(row, col int, prov *RichText) int {
b := prov.rowBoundary(row)
if b == nil {
return col
}
return b.begin + col
}
func (s *selectable) updateMousePointer(p fyne.Position) {
row, col := s.getRowCol(p)
s.cursorRow, s.cursorColumn = row, col
if !s.selecting {
s.selectRow = row
s.selectColumn = col
}
}
func (s *selectable) getSizeName() fyne.ThemeSizeName {
if s.sizeName != "" {
return s.sizeName
}
return theme.SizeNameText
}
type selectableRenderer struct {
sel *selectable
selections []fyne.CanvasObject
}
func (r *selectableRenderer) Destroy() {
}
func (r *selectableRenderer) Layout(fyne.Size) {
}
func (r *selectableRenderer) MinSize() fyne.Size {
return fyne.Size{}
}
func (r *selectableRenderer) Objects() []fyne.CanvasObject {
return r.selections
}
func (r *selectableRenderer) Refresh() {
r.buildSelection()
selections := r.selections
v := fyne.CurrentApp().Settings().ThemeVariant()
selectionColor := r.sel.theme.Color(theme.ColorNameSelection, v)
for _, selection := range selections {
rect := selection.(*canvas.Rectangle)
rect.FillColor = selectionColor
if r.sel.focussed {
rect.Show()
} else {
rect.Hide()
}
}
canvas.Refresh(r.sel.impl)
}
// This process builds a slice of rectangles:
// - one entry per row of text
// - ordered by row order as they occur in multiline text
// This process could be optimized in the scenario where the user is selecting upwards:
// If the upwards case instead produces an order-reversed slice then only the newest rectangle would
// require movement and resizing. The existing solution creates a new rectangle and then moves/resizes
// all rectangles to comply with the occurrence order as stated above.
func (r *selectableRenderer) buildSelection() {
th := r.sel.theme
v := fyne.CurrentApp().Settings().ThemeVariant()
textSize := th.Size(r.sel.getSizeName())
cursorRow, cursorCol := r.sel.cursorRow, r.sel.cursorColumn
selectRow, selectCol := -1, -1
if r.sel.selecting {
selectRow = r.sel.selectRow
selectCol = r.sel.selectColumn
}
if selectRow == -1 || (cursorRow == selectRow && cursorCol == selectCol) {
r.selections = r.selections[:0]
return
}
provider := r.sel.provider
innerPad := th.Size(theme.SizeNameInnerPadding)
// Convert column, row into x,y
getCoordinates := func(column int, row int) (float32, float32) {
sz := provider.lineSizeToColumn(column, row, textSize, innerPad)
return sz.Width, sz.Height*float32(row) - th.Size(theme.SizeNameInputBorder) + innerPad
}
lineHeight := r.sel.provider.charMinSize(r.sel.password, r.sel.style, textSize).Height
minmax := func(a, b int) (int, int) {
if a < b {
return a, b
}
return b, a
}
// The remainder of the function calculates the set of boxes and add them to r.selection
selectStartRow, selectEndRow := minmax(selectRow, cursorRow)
selectStartCol, selectEndCol := minmax(selectCol, cursorCol)
if selectRow < cursorRow {
selectStartCol, selectEndCol = selectCol, cursorCol
}
if selectRow > cursorRow {
selectStartCol, selectEndCol = cursorCol, selectCol
}
rowCount := selectEndRow - selectStartRow + 1
// trim r.selection to remove unwanted old rectangles
if len(r.selections) > rowCount {
r.selections = r.selections[:rowCount]
}
// build a rectangle for each row and add it to r.selection
for i := 0; i < rowCount; i++ {
if len(r.selections) <= i {
box := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v))
r.selections = append(r.selections, box)
}
// determine starting/ending columns for this rectangle
row := selectStartRow + i
startCol, endCol := selectStartCol, selectEndCol
if selectStartRow < row {
startCol = 0
}
if selectEndRow > row {
endCol = provider.rowLength(row)
}
// translate columns and row into draw coordinates
x1, y1 := getCoordinates(startCol, row)
x2, _ := getCoordinates(endCol, row)
// resize and reposition each rectangle
r.selections[i].Resize(fyne.NewSize(x2-x1+1, lineHeight))
r.selections[i].Move(fyne.NewPos(x1-1, y1))
}
}
func (s *selectable) grabFocus() {
if c := fyne.CurrentApp().Driver().CanvasForObject(s.focus.(fyne.CanvasObject)); c != nil {
c.Focus(s.focus)
}
}
func isTripleTap(double, nowMilli int64) bool {
return nowMilli-double <= fyne.CurrentApp().Driver().DoubleTapDelay().Milliseconds()
}